Git - Kapitola 10. Hackování gitu

Formát ukládání objektů / Pohled na zdrojové kódy gitu z ptačí perspektivy

Obsah

Formát ukládání objektů
Pohled na zdrojové kódy gitu z ptačí perspektivy

Tato kapitola se zabývá interními detaily implementace gitu, kterým pravděpodobně potřebují porozumět pouze vývojáři gitu.

Formát ukládání objektů

Všechny objekty mají staticky přiřazen "typ" identifikující formát objektu (tj. jak je použit a jak může odkazovat na další objekty). V současnosti existují čtyři různé typy objektů: "blob", "tree", "commit", a "tag".

Bez ohledu na typ objektu, všechny objekty sdílí následující vlastnosti: jsou komprimované pomocí knihovny zlib, a mají hlavičku která nejen že určuje jejich typ, ale také poskytuje informaci o velikosti dat v objektu. Stojí za zmínku že SHA-1 hash používaný k odkazování na objekt je hash původních dat a hlavičky, takže výsledek příkazu sha1sum file se neshoduje jménu objektu vytvořeného pro file. (Historická poznámka: za úsvitu věků gitu byl SHA-1 počítán na komprimovaném objektu.)

Výsledkem je že obecná konzistence objektu může být otestována kdykoliv bez ohledu na obsah nebo typ objektu: všechny objekty mohou být zkontrolovány ověřením (a) že jejich hashe odpovídají obsahu souboru a (b) objekt se úspěšně rozbalí do posloupnosti bytů <ascii typ bez mezery> + <mezera> + <ascii velikost> + <byte\0> + <binární data objektu>.

U strukturovaných objektů lze dále ověřovat jejich strukturu a napojení na další objekty. To je obecně prováděno příkazem git fsck, který generuje plný graf závislostí všech objektů, a ověřuje jejich interní konzistenci (navíc k prosté kontrole jejich celkové závislosti pomocí hashe).

Pohled na zdrojové kódy gitu z ptačí perspektivy

Ne vždy je pro nové vývojáře jednoduché proklestit si cestu zdrojovými kódy Gitu. Tento odstavec vám trošku pomůže najít kde začít.

Dobré místo kde začít je obsah výchozího commitu:

$ git checkout e83c5163

Výchozí revize pokládá základy pro téměř cokoliv co git obsahuje dnes, ale je dostatečně malý na přečtení na jedno posezení.

Všimněte si že terminologie se od této revize změnila. Například, README v dané revizi používá slovo "changeset" pro popis toho co dnes nazýváme commit.

Také už nepoužíváme označení "cahe" ale "index"; nicméně soubor se stále nazývá cache.h. Poznámka: Není příliš důvod ho nyní měnit, zejména když stejně žádné dobré jméno neexistuje,  protože toto je _ten_ hlavičkový soubor vkládaný do _všech_  Gitových C zdrojáků.

Pokud pochopíte myšlenky zachycené ve výchozím commitu, měli byste načíst a rychle prolétnout novější revizi souborů cache.h, object.h a commit.h.

V ranných dobách, Git (dle UNIXové tradice) sestával z mnoha extrémně jednoduchých příkazů, které jste používali ve skriptech, a výstup jednoho do druhého předávali pomocí roury. To se ukázalo jako vhodné pro úvodní fázi vývoje, protože bylo jednodušší testovat nové věci. V poslední době se ale mnoho z těchto součástí změnilo na zabudované příkazy, a část jádra byla "libified", t.j. přesunuta do libgit.a z důvodů výkonu a přenositelnosti, a pro zamezení duplikace kódu.

Nyní už víte co je index (a dokážete nalézt odpovídající datové struktury v souboru cache.h), a že existuje několik typů objektů (bloby, stromy, commity a tagy) které dědí společnou strukturu ze strukturu struct object, která je jejich první složkou (a tudíž můžete provést přetypování např. (struct object *)commit čímž dosáhnete toho _samého_ jako když použijete &commit->object, tj. získáte jméno objektu a flagy).

Nyní je ta správná chvíle udělat si přestávku a všechny informace pěkně vstřebat.

Další krok: seznamte se s pojmenováním objektů. Přečtěte si odstavec nazvaný “Pojmenovávání commity”. Existuje několik způsobů jak pojmenovat objekt (a to ne pouze revize!). Všechny jsou ošetřeny v souboru sha1_name.c. Satčí se zběžně podívat na funkci get_sha1(). Spousta základní funkčnosti je prováděna funkcemi jako get_sha1_basic() a podobnými.

To ale byla pouze příprava na zkoumání "most libified" části Gitu: procházení revizí.

Výchozí verze příkazu git log byl v podstatě shell script:

$ git-rev-list --pretty $(git-rev-parse --default HEAD "$@") | \
        LESS=-S ${PAGER:-less}

Co to znamená?

git rev-list je původní verze "procházeče revizí" (revision walke), která _vždy_ vytiskla seznam revizí na stdout (standardní výstup). Je stále funkční, což je nutné protože většina dnešních programů v Gitu začala jako skript využívající příkaz git rev-list.

git rev-parse již tak důležitý není; byl používán pouze k odfiltrování voleb které byly relevantní pro ostatní pomocné příkazy spouštěné skriptem.

Většina toho co git rev-list provádí je zahrnuto do souborů revision.c a revision.h. Volby jsou uloženy ve struktuře pojmenované rev_info, která kontroluje jak a které revize jsou procházeny, a podobně.

Původní úkol příkazu git rev-parse je nyní převzat funkcí setup_revisions(), která parsuje revize a a společné command-line volby pro revision walker. Tyto informace jsou uloženy ve struktuře rev_info pro pozdější použití. Vlastní zpracování command-line voleb můžete provést po zavolání setup_revisions(). Potom musíte zavolat prepare_revision_walk() kvůli inicializaci, a následně můžete načítat jeden commit po druhém prostřednictvím funkce get_revision().

Pokud vás zajímá více detailů o procesu procházení revizí, stačí se podívat na první implementaci funkce cmd_log(); zavolejte git show v1.3.0~155^2~4 a skočte dolů na uvedenou funkci (všimněte si že funkci setup_pager() už nepotřebujete volat přímo).

V současnosti je příkaz git log is zabudován do gitu, což znamená že je _obsažen_ v příkazu git. Zdrojové kódy zabudované verze jsou

  • funkce nazvané cmd_<bla>, typicky definované v builtin-<bla>.c, a deklarované v builtin.h,
  • položka v poli commands[] array v souboru git.c, a
  • položka v BUILTIN_OBJECTS v Makefile.

Občas je v jednom zdrojovém souboru obsaženo více zabudovaných příkazů. Například příkazy cmd_whatchanged() a cmd_log() jsou oba uloženy v souboru builtin-log.c, protože sdílejí spoustu kódu. V takovém případě musí být příkazy, které _nejsou_ pojmenovány stejně jako .c soubor ve kterém existují, vyjmenovány v BUILT_INS v Makefile.

Příkaz git log vypadá v C komplikovaněji než byl v původním skriptu, ale to poskytuje daleko větší flexibilitu a výkon.

Na tomto místě je opět správná chvíle dát si pauzu.

Lekce tři je: studujte zdrojové kódy. Vážně, je to nejlepší způsob jak zjistit jak je Git organizován (poté co znáte základní koncepty).

Takže až vás napadne něco co vás zajímá, řekněme, "jak mohu přistoupit k blobu pokud znám jenom jeho objektové jméno?". Prvním krokem je nalezení Git příkazu pomocí kterého to lze provést. V uvedeném případě je to buď git show nebo git cat-file.

V zájmu jednoduchosti se držme git cat-file, protože

  • je to jeden ze základních příkazů
  • existoval již v úvodním commitu (prošel zhruba nějakými 20 revizemi před tím než byl cat-file.c přejmenován na builtin-cat-file.c během zabudováním do Gitu, a poté méně než 10 verzí).

Takže, podívejme se do souboru builtin-cat-file.c, vyhledejte cmd_cat_file() a podívejte se co dělá.

    git_config(git_default_config);
    if (argc != 3)
            usage("git cat-file [-t|-s|-e|-p|<type>] <sha1>");
    if (get_sha1(argv[2], sha1))
            die("Not a valid object name %s", argv[2]);

Přeskočme očividné detaily; jediná skutečně zajímavá část je volání funkce get_sha1(). Snaží se interpretovat argv[2] jako jméno objektu, a pokud odkazuje na objekt přítomný v současném repositáři, a zapíše výsledný SHA-1 do proměnné sha1.

Dvě věci jsou zde zajímavé:

  • get_sha1() vrací 0 v případě _úspěchu_. To by mohlo překvapit některé nové Git hackery, ale je dlouhou UNIXovou tradicí vracet různé záporné hodnoty v případě různých chyb - a 0 v případě úspěchu.
  • proměnná sha1 v signatuře funkce get_sha1() je typu unsigned char *, ale ve skutečnosti se očekává že bude odkazem na unsigned char[20]. Tato proměnná bude obsahovat 160-bit SHA-1 hash daného commitu. Všimněte si že když je jakýkoliv SHA-1 předáván jako unsigned char *, jedná se o binární reprezentaci, což je rozdíl oproti předávání ASCII reprezentace která je předávána jako char *.

Obě tyto věci uvidíte ve zdrojových kódech.

A nyní to hlavní:

   case 0:
       buf = read_object_with_reference(sha1, argv[1], &size, NULL);

Tímto způsobem je načítán blob (vlastně, ne jen blob ale libovolný typ objektu). Pro zjištění jak funkce read_object_with_reference() vlastně pracuje, vyhledejte její zdrojové kódy (něco jako git grep read_object_with | grep ":[a-z]" v git repositáři), a opět si přečtěte zdrojový kód.

Pro zjištění jak může být používán výsledek si prostě přečtěte in cmd_cat_file():

    write_or_die(1, buf, size);

Někdy nevíte kde hledat danou vlastnost. V mnoha takových případech pomáhá hledat ve výstupu příkazu git log, a potom git show odpovídající commit.

Příklad: pokud víte že pro git bundle existoval test case, ale nepamatujete si kde byl (ano, mohl byste provést _could_ git grep bundle t/, ale to neilustruje daný případ!):

$ git log --no-merges t/

Při stránkování (less), pouze vyhledejte "bundle", vraťte se o několik řádek zpět, a zjistíte že se jedná o commit 18449ab0… Nyní pouze zkopírujte toto jméno objektu, a zkopírujte ho na příkazovou řádku

$ git show 18449ab0

Voila.

Další příklad: Zjistěte co udělat pro změnu některého skriptu na zabudovaný:

$ git log --no-merges --diff-filter=A builtin-*.c

Vidíte, Git je nejlepším nástrojem pro zkoumání zdrojových kódů samotného Gitu!

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)