Git - Kapitola 10. Hackování gitu
Formát ukládání objektů / Pohled na zdrojové kódy gitu z ptačí perspektivy
Obsah
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é vbuiltin-<bla>.c, a deklarované vbuiltin.h, - položka v poli
commands[]array v souborugit.c, a - položka v
BUILTIN_OBJECTSvMakefile.
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.cpřejmenován nabuiltin-cat-file.cbě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á
sha1v signatuře funkceget_sha1()je typuunsigned char *, ale ve skutečnosti se očekává že bude odkazem naunsigned 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 jakounsigned char *, jedná se o binární reprezentaci, což je rozdíl oproti předávání ASCII reprezentace která je předávána jakochar *.
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!




