Nakoukneme pod kapotu a vysvětlíme si, jak Git uskutečnujě své zázraky. Detaily trochu odbydu. Pro opravdu podrobný popis vás odkazuji na uživatelský manuál.
Neviditelnost
Jak může být Git tak nevtíravý? Krom výjimečných commitů a slučování (ang. merge) můžete pracovat bez vědomí, že nějaký verzovací systém vůbec existuje. Tak to pokračuje, dokud ho nepotřebujete a jste rádi, že na vás Git celý čas dohlížel.
Ostatní verzovací systémy vám nedovolí, abyste na ně zapomněli. Přístupová práva k souborům mohou být jen pro čtení, dokud výslovně neřeknete serveru, na jakých souborech se chystáte pracovat, protože server může potřebovat udržovat informace o tom, kdo a kdy získal jaký kód. Když spadne síť, budete brzy trpět. Vývojáři se neustále byrokraticky mlátí.
Tajemstvím je adresář .git
ve vašem pracovním adresáři.
Git udržuje historii projektu právě tady. Název začínající tečkou
zabraňuje, aby se adresář objevoval ve výstupech příkazů jako
ls
. Všechny operace kromě push a pull pracují
v tomto adresáři.
Máte naprostou kontrolu nad osudem svých souborů, protože Gitu je jedno,
co s nimi děláte, a může kdykoli jednoduše znovu vytvořit uložený stav
z podadresáře .git
.
Integrita
Mnoho lidí si spojuje kryptografii s udržováním informací tajných, ale jiným, stejně důležitým, cílem je udržet informace nepoškozené. Vyhovujícím použitím kryptografické hashovací funkce můžeme předejít náhodnému či záměrnému poškození dat.
O SHA1 hashi můžeme smýšlet jako o jedinečném 160-bitovém ID pro každý řetězec bytů, který kdy v životě potkáme. Vlastně více než to: každý řetězec bytů, který jakýkoli člověk kdy použije.
Jelikož je SHA1 hash sám o sobě řetězec bytů, můžeme hashovat řetězce bytů obsahující ostatní hashe. Tato jednoduchá skutečnost je udivujícně užitečná: hle „hashové řetězce“. Později uvidíme, jak je Git účinně používá k zaručení integrity dat.
Stručně, Git udržuje vaše data v podadresáři
.git/objects
, kde místo normálních jmen souborů najdete jenom
jejich ID. Používáním ID jako jmen souborů, stejně jako pár souborů se
zámky a triků s časovými značkami, Git převede jakýkoli skromný
filesystém do účinné a robustní databáze.
Chytrost
Jak Git ví, že jste přejmenovali soubor, i když jste to nikdy výslovně
nezmínili? Samozřejmě, že můžete spustit git mv
, ale to je
úplně to samé jako git rm
následované git
add
.
Git chytře vyčmuchá přejmenování a kopie mezi za sebou jdoucími verzemi. Popravdě může najít dokonce kousky kódu přesunuté nebo zkopírované ze souboru do souboru! I když nemůže pokrýt všechny případy, dělá dobrou práci a tato jeho vlastnost se neustále zlepšuje. Pokud vám to nefunguje, zkuste navolit možnosti povolující lepší rozpoznávání kopírování a popřemýšlejte po přechodu na novější verzi.
Indexování
Pro každý sledovaný soubor Git zaznamenává informace jako velikost souboru, jeho čas vytvoření a poslední změny, které jsou známé jako „index“. K určení, jestli se soubor změnil, Git porovná tyto údaje s těmi aktuálními. Pokud se shodují, Git může přeskočit čtení celého souboru.
Poněvadž je získávání informací o stavu souboru považováno za rychlejší než čtení souboru, pokud změníte pouze pár souborů, Git může zaktualizovat svůj stav prakticky okamžitě.
Holé repozitáře
Možná jste se divili, jaký formát používají online Gití repozitáře.
Jsou to normální Gití repozitáře, stejně jako podadresář
.git
, jen s tím rozdílem, že mají jména jako
projekt.git
a nemají přiřazený žádný pracovní
adresář.
Většina Gitích příkazů očekává, že najde index v .git
,
a skončí chybou při práci na těchto holých repozitářích. Vyřešit to
můžete nastavením proměnné prostředí GIT_DIR
na cestu
k holému repozitáři nebo spuštěním Gitu v samotném adresáři
s takovým repozitářem s volbou --bare
.
Původ Gitu
Tento příspěvek na Linux Kernel Mailing List vysvětluje řetězec událostí, které vedly ke Gitu, jak ho známe dnes. Celé vlákno je úchvatným archeologickým místem pro Gití historiky.
Databáze objektů
Zde je návod, jak napsat systém podobný Gitu za pár hodin.
Bloby
Nejdříve jedno kouzlo. Vyberte si název souboru, jakýkoli název. A v prázdném adresáři:
$ echo super > NAZEV_SOUBORU $ git init $ git add . $ find .git/objects -type f
Uvidíte
.git/objects/16/f5c2d3aa9656fc424352e4cfaa2523c809778b
.
Jak to můžu vědět bez toho, abych znal název souboru? Jen to proto, že SHA1 hash z:
"blob" SP "6" NUL "super" LF
je 16f5c2d3aa9656fc424352e4cfaa2523c809778b, kde SP je mezera (ang. space), NUL je nulový byte a LF je odřádkování. Můžete si to ověřit napsáním:
$ echo "blob 6"$'\001'"super" | tr '\001' '\000' | sha1sum
Jen tak mimochodem, předchozí kód je psán s ohledem na Bash; jiné
shelly možná jsou schopné pracovat s NULem na příkazové řádce, díky
čemuž bychom se zbavili workaroundu s tr
.
Git používá adresování podle obsahu: soubory nejsou ukládány podle
jména, ale podle hashe dat, která obsahují; a to v souboru, který je
nazván jako „blob object“. O hashi můžeme smýšlet jako o unikátním
ID obsahu souboru, takže v tomto smyslu adresujeme soubory podle jejich
obsahu. Počáteční blob 6
je pouze hlavička sestávající
z typu objektu a jeho délky v bytech; zjednodušuje to vnitřní procesy.
Tudíž je jednoduché předpovědět, co uvidíte. Název souboru není důležitý: na vytvoření blob objektu jsou použita jenom data uvnitř.
Možná se můžete divit, co se stane se stejnými soubory. Zkuste přidat
kopie vašeho souboru s jakýmkoli názvem souboru. Obsah
.git/objects
zůstane stejný bez ohledu na to, kolik jich
přidáte. Git ukládá data pouze jednou.
Mimochodem, soubory v .git/objects
jsou zkomprimované pomocí
zlib, takže se do nich nemůžete koukat přímo. Původní obsah získáte
pomocí zpipe -d, nebo napsáním:
$ git cat-file -p 16f5c2d3aa9656fc424352e4cfaa2523c809778b
což hezky vytiskne předaný objekt.
Stromy
Ale kde jsou tedy názvy souborů? Jsou uloženy někde v indexu a Git se o ně postará při commitu:
$ git commit # napište nějakou zprávu $ find .git/objects -type f
Teď byste měli vidět 3 objekty. Tentokráte vám již nemohu říct,
jaký název budou dva nové soubory mít, protože to závisí na názvu
souboru, který jste vybrali. Pokročíme předpokladem, že jste váš soubor
nazvali soubor
. Pokud jste tak neudělali, můžete přepsat
historii, aby to vypadalo, že jste tak udělali:
$ git filter-branch --tree-filter 'mv NAZEV_SOUBORU soubor' $ find .git/objects -type f
Nyní byste měli vidět soubor
.git/objects/21/0ba5665159efc75739ccb3a6332532669eda96
, protože
toto je SHA1 hash jeho obsahu:
"tree" SP "34" NUL "100644 soubor" NUL 0x16f5c2d3aa9656fc424352e4cfaa2523c809778b
Ověřte si, že soubor opravdu obsahuje data výše napsáním:
$ echo 210ba5665159efc75739ccb3a6332532669eda96 | git cat-file --batch
Se zpipe
je také jednoduché ověřit hash:
$ zpipe -d < .git/objects/21/0ba5665159efc75739ccb3a6332532669eda96 | sha1sum
Ověření hashe skrz cat-file
je trochu složitější,
protože jeho obsah je mnohem víc než jen surový dekomprimovaný
objekt.
Tento soubor je „tree“ objekt: seznam n-tic sestávajících z typu
souboru, jeho jména a hashe. V našem případě je typ souboru
100644
, což znamená, že soubor
je normální soubor
a hash je blob objekt, který v sobě ukrývá obsah soubor
u.
Ostatní možné typy souborů jsou spustitelné soubory, symlinky nebo
adresáře. V posledním případě hash odkazuje na další tree objekt.
Pokud spustíte filter-branch
, budete mít staré objekty,
které již více nepotřebujete. I když budou zničeny automaticky hned, jak
nadejde jejich chvíle, teď je vymažeme, aby byl náš příklad
jednodušší.
$ rm -r .git/refs/original $ git reflog expire --expire==now --all $ git prune
V opravdových projektech byste se měli těmto příkazům vyhnout,
protože tím vlastně mažete zálohy. Pokud chcete repozitář čistý, je
obvykle nejlepší udělat jeho nový klon. Také si dávejte pozor na
manipulaci s adresářem .git
: co když ve stejný okamžik
běží nějaký jiný příkaz nebo z ničeho nic vypadne proud? Obecně by
se reference měly mazat pomocí git update-ref -d
, přestože
obvykle je bezpečné je odstranit ručně (refs/original
).
Commity
Objasnili jsme 2 ze 3 objektů. Třetí je „commit“ objekt. Jeho obsah závisí na zprávě stejně jako na datu a času, kdy byl vytvořen. Aby odpovídal tomu, co bude následovat, budeme ho muset trochu upravit:
$ git commit --amend -m Shakespeare # změníme zprávu $ git filter-branch --env-filter 'export GIT_AUTHOR_DATE=="Fri 13 Feb 2009 15:31:30 -0800" GIT_AUTHOR_NAME=="Alice" GIT_AUTHOR_EMAIL=="alice@example.com" GIT_COMMITTER_DATE=="Fri, 13 Feb 2009 15:31:30 -0800" GIT_COMMITTER_NAME=="Bob" GIT_COMMITTER_EMAIL=="bob@example.com"' # správný čas a autoři $ find .git/objects -type f
Měli byste vidět
.git/objects/6f/ca06c5ba5737e0eb147f9cc9761f99d0c15915
, což je
SHA1 hash jeho obsahu:
"commit 158" NUL "tree 210ba5665159efc75739ccb3a6332532669eda96" LF "author Alice <alice@example.com> 1234567890 -0800" LF "committer Bob <bob@example.com> 1234567890 -0800" LF LF "Shakespeare" LF
Stejně jako předtím můžete spustit zpipe
nebo git
cat-file
, abyste se podívali sami.
Toto je první commit, takže nemá žádné rodiče, ale pozdější commity budou vždy obsahovat alespoň jeden další řádek určující rodičovský commit.
Neropoznatelné od magie
Ještě něco by se slušelo říct. Právě jsme odhalili tajemství síly Gitu. Zní to jednoduše: vypadá to, že byste mohli smíchat dohromady pár shellových skriptů, přidat trochu kódu v C, a tak byste získali systém výše za pár hodin. Popravdě toto naprosto přesně vystihuje nejranější vývoj Gitu. Nicméně, krom geniálních balicích triků, jak ušetřit místo, a indexovacích triků, jak ušetřit čas, nyní víme, jak Git hbitě mění filesystém v databázi perfektní pro verzování.
Kupříkladu, pokud je nějaký soubor v databázi poškozen chybou disku, pak jeho hash nebude již více odpovídat, což nás bude upozorňovat na problém. Hashováním hashů jiných objektů udržujeme integritu na všech úrovních. Commity jsou atomické, takže commit nemůže vyústit v částečné zapsání změn: hash commitu můžeme vypočítat a uložit do databáze jedině tehdy, když jsme už uložili všechny související bloby, stromy a rodičovské commity. Databáze objektů je odolná vůči neočekávaným přerušením, jakým je například výpadek proudu.
Porazíme i ty nepochybnější protivníky. Představte si, že se někdo kradmo pokusí změnit obsah souboru v nějaké prehistorické verzi projektu. Aby databáze objektů vypadala „zdravě“, je potřeba také změnit hashe odpovídajících blob objektů, poněvadž jsou to nyní jiné řetězce bytů. Toto znamená, že se musí změnit hashe jakýchkoli stromových objektů odkazujících na tento soubor. A ve výsledku i hash všech objektů commitů, které obsahují takový strom. Přidejte k tomu ještě všechny potomky těchto commitů, z čehož vyplývá, že hash oficiálního HEADu bude jiný než toho v tomto špatném repozitáři. Sledováním stopy nesouhlasných hashů můžeme určit změněný soubor, stejně tak i commit, který byl jako první poškozen.
Zkráceně, jak je 20 bytů reprezentujících poslední commit někde v bezpečí, je nemožné si zahrávat s Gitím repozitářem.
A co ty slavné Gití funkce? Větvení? Slučování? Tagy? Strohé
detaily. Současná hlava (HEAD) je v souboru .git/HEAD
,
který obsahuje hash commit objektu. Hash se mění po
commitu, stejně jako po ostatních příkazech. Větve jsou skoro to
samé: jsou to soubory v .git/refs/heads
. Tagy také: žijí v
.git/refs/tags
, ale jsou měněny jinou sadou příkazů.
Tento článek je překladem sedmé kapitoly – Secrets Revealed – z GitMagic od Bena Lynna. Příště, v poslední kapitole, budou popsány některé nedostatky Gitu.