Git - Kapitola 7. Koncepty Gitu
Databáze objektů / Commit Objekt / Tree Objekt / Blob Objekt / Trust / Tag Objekt / Jak git efektivně ukládá objekty: pack soubory / Plandající objekty / Obnova poškozeného repositáře / Index
Obsah
Git je postaven na několika málo jednoduchých ale mocných myšlenkách. Je sice možné git používat i bez jejich znalosti, bude pro vás git daleko pochopitelnější pokud je znát budete.
Začneme dvěma nejdůležitějšími, což jsou databáze objektů a index.
Databáze objektů
V odstavci nazvaném “Porozumění historii: Commity” že všechny commity jsou ukládány pod 40-místným "jménem objektu". Vlastně všechny informace potřebné k reprezentaci historii projektu jsou uloženy v objektech s takovými jmény. Ve všech případech je jméno vypočteno jako SHA-1 hash obsahu objektu. SHA-1 hash je kryptografická hashovací funkce. Pro nás to znamená že je nemožné najít dva různé objekty se stejným názvem. To má mnoho výhod; kromě jiných:
- Git může rychle rozhodnout zda dva objekty jsou identické či nikoliv, pouhým srovnáním jmen.
- Protože jména objektů jsou ve všech repositářích počítána stejným způsobem, stejný obsah ve dvou repositářích bude vždy uložen pod stejným jménem.
- Git dokáže odhalit chyby při čtení objektu, a to kontrolou zda jeho jméno je stále SHA-1 hashem jeho aktuálního obsahu.
(Detaily o formátování jmen a výpočtu SHA-1 najdete v odstavci nazvaném “Object storage format”.)
Existují čtyři různé typy objektů: "blob", "tree" (strom), "commit", a "tag".
- "blob" objekt je používán pro ukládání dat souborů.
- "tree" objekt sdružuje jeden nebo více "blob" objektů do adresářové struktury. Navíc, tree objekt může odkazovat na další tree objekty, a vytvářet tak hierarchii adresářů.
- "commit" objekt svazuje takové hierarchie adresářů do orientovaného acyklického grafu revizí - každý commit obashuje jméno objektu právě jednoho stromu (tree) určujícího adresářovou hierarchii v okamžiku commitu. Navíc, commit odkazuje na "rodičovské" (parent) commit objekty, popisující historii jak jsme dospěli k dané hierarchii adresářů.
- "tag" objekt symbolicky identifikuje a může být použit k podpisu jiného objektu. Obsahuje objektové jméno a typ jiného objektu, symbolické jméno (pochopitelně!) a, volitelně, podpis.
Typy objektů trochu detailněji:
Commit Objekt
"Commit" objekt propojuje fyzický stav stromu s popisem jak jsme se k němu dostali a proč. Zkuste svůj oblíbený commit prozkoumat s volbou —pretty=raw příkazů git-show(1) nebo git-log(1):
$ git show -s --pretty=raw 2be7fcb476
commit 2be7fcb4764f2dbcee52635b91fedb1b3dcf7ab4
tree fb3a8bdd0ceddd019615af4d57a53f43d8cee2bf
parent 257a84d9d02e90447b149af58b271c19405edb6a
author Dave Watson <dwatson@mimvista.com> 1187576872 -0400
committer Junio C Hamano <gitster@pobox.com> 1187591163 -0700
Fix misspelling of 'suppress' in docs
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Jak můžete vidět, commit jedefinován:
- stromem: SHA-1 jméno tree objektu (jak je popsáno níže), reprezentující obsah adresáře v určitém časovém okamžiku.
- rodič(i): SHA-1 jména určitého jména commitů reprezentujících bezprostředně předcházející krok(y) v historii projektu. Výše uvedený příklad má jednoho rodiče; merge commity budou mít více než jednoho. Commit bez rodičů se nazývá "root" commit, a reprezentuje výchozí revizi projektu. Každý projekt musí mít alespoň jeden root commit. Projekt může mít i několik root commitů, ačkoliv to není příliš časté (nebo nutně dobrý nápad).
- autor: Jméno osoby zodpovědné za provedení dané změny, společně s datem změny.
- commiter: Jméno osoby která ve skutečnosti provedla commit, s datem commitu. Může se lišit od jména autora, například pokud autor napsal patch a poslal ho e-mailem osobě která následně vytvořila commit.
- komentář popisující commit.
Všimněte si že commit sám o sobě neobsahuje žádné informace o tom co bylo skutečně změněno; všechny změny jsou určovány srovnáním obsahu stromu odkazovaného z commitu se stromy spojenými s jeho rodiči. Zejména, git se nesnaží explicitně zaznamenávat změny názvů souborů, nicméně dokáže identifikovat případy kdy existence souborů se stejným obsahem naznačuje že došlo k přejmenování. (Viz. například volba -M příkazu git-diff(1)).
A commit is usually created by git-commit(1), which creates a commit whose parent is normally the current HEAD, and whose tree is taken from the content currently stored in the index.
Tree Objekt
Všestranný příkaz git-show(1) může být také použit k prozkoumání tree objektů, ale git-ls-tree(1) vám poskytne daleko více detailů:
$ git ls-tree fb3a8bdd0ce 100644 blob 63c918c667fa005ff12ad89437f2fdc80926e21c .gitignore 100644 blob 5529b198e8d14decbe4ad99db3f7fb632de0439d .mailmap 100644 blob 6ff87c4664981e4397625791c8ea3bbb5f2279a3 COPYING 040000 tree 2fb783e477100ce076f6bf57e4a6f026013dc745 Documentation 100755 blob 3c0032cec592a765692234f1cba47dfdcc3a9200 GIT-VERSION-GEN 100644 blob 289b046a443c0647624607d471289b2c7dcd470b INSTALL 100644 blob 4eb463797adc693dc168b926b6932ff53f17d0b1 Makefile 100644 blob 548142c327a6790ff8821d67c2ee1eff7a656b52 README ...
Jak vidíte, tree objekt obsahuje seznam položek, každý s informací o módu, typu objektu, SHA-1 jméně a jméně, setříděný dle jména. Reprezentuje obsah jediného adresářového stromu.
Typ objektu může být blob, reprezentující obsah souboru, nebo další strom (tree), rreprezentující obsah podadresáře. Protože stromy a bloby, stejně jako ostatní objekty, jsou pojmenovány pomocí SHA-1 hashem jejich obsahu, dva stromy mají stejné SHA-1 jméno právě když je identický jejich obsah (včetně, returzivně, obsahu podadresářů). To gitu umožňuje rychle určovat rozdíly mezi souvisejícími tree objekty, protože může ignorovat všechny položky s identickými objektovými jmény.
(Poznámka: v případě submodulů mohou mít stromy jako položky i commity. Dokumentaci najdete v Kapitole 8, Submoduly.)
Všimněte si že všechny soubory mají mód 644 nebo 755: git se ve skutečnosti zajíma pouze o "executable" bit.
Blob Objekt
Příkaz git-show(1) můžete použít k prozkoumání obsahu blobu; vezměme, například, blob v položce pro "COPYING" z výše uvedeného stromu:
$ git show 6ff87c4664 Note that the only valid version of the GPL as far as this project is concerned is _this_ particular version of the license (ie v2, not v2.2 or v3.x or whatever), unless explicitly otherwise stated. ...
"Blob" objekt není nic než binární datový soubor.Neodkazuje na cokoliv dalšího a nemá žádné další atributy.
Protože blob je úplně definován svými daty, pokud dva soubory v adresářovém stromě (nebo v několika různých verzích repositáře) mají stejný obsah, budou sdíílet stejný blob objekt. Objekt je absolutně nezávislý na umístění v adresářovém stromě, a přejmenování souboru nezmění objekt který je s daným názvem asociován.
Uvědomte si že jakýkoliv tree nebo blob objekt může být analyzován pomocí příkazu git-show(1) se syntaxí <revision>:<path>. To může být občas užitečné pro procházení obsahu stromu který aktuálně není načten.
Trust
Pokud SHA-1 jméno blobu získáte z jednoho zdroje, a jeho obsah z jiného (potenciálně nedůvěryhodného) zdroje, můžete přesto důvěřovat že obsah je správný pokud SHA-1 jména souhlasí. To proto že SHA-1 je navržen tak aby nalezení dvou různých obsahů produkujících stejný hash bylo výpočetně neproveditelné.
Podobně, věřit musíte pouze SHA-1 jménu stromu na nejvyšší úrovni abyste následně mohli věřit obsahu celého adresáře na který odkazuje, a pokud získáte SHA-1 jméno commitu z důvěryhodného zdroje, potom můžete jednoduše ověřit kompletní historii commitů dostupných přes rodiče daného commitu, a veškerého obsahu stromů odkazovaných z těchto commitů.
Takže aby do vašeho systému byla zavedena důvěra, jediná věc kterou musíte udělat je digitálně podepsat pouze jedinou poznámku, zahrnující název top-level commitu. Váš digitální podpis odstatním ukazuje že danému commitu důvěřujete, neměnitelnost historie commitů ostatním říká že mohou věřit celé historii.
Jinými slovy, můžete jednoduše ověřit celý archiv pouze odesláním jediného e-mailu který ostatním sděluje jméno (SHA-1 hash) hlavního commitu (top commit), a digitálně ho podepsat pomocí něčeho jako GPG/PGP.
Git jako pomoc poskytuje také tag objekt …
Tag Objekt
Tag objekt obsahuje objekt, typ objektu, jméno tagu, jméno osoby ("tagger") která vytvořila tag, a zprávu která může obsahovat digitální podpis, jak lze zjistit například příkazem git-cat-file(1):
$ git cat-file tag v1.5.0 object 437b1b20df4b356c9342dac8d38849f24ef44f27 type commit tag v1.5.0 tagger Junio C Hamano <junkio@cox.net> 1171411200 +0000 GIT 1.5.0 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.6 (GNU/Linux) iD8DBQBF0lGqwMbZpPMRm5oRAuRiAJ9ohBLd7s2kqjkKlq1qqC57SbnmzQCdG4ui nLE/L9aUXdWeTFPron96DLA= =2E+0 -----END PGP SIGNATURE-----
V dokumentaci k příkazu git-tag(1) najdete informace jak vytvářet a verifikovat tag objekty. (Uvědomte si že příkaz git-tag(1) lze také využít k vytvoření "lightweight tagů", což ve skutečnosti vůbec nejsou tag objekty, ale pouze obyčejné odkazy jejichž jména začínají "refs/tags/").
Jak git efektivně ukládá objekty: pack soubory
Nově vytvářené objekty jsou nejdříve vytvořeny jako soubor s příslušným SHA-1 hashem použitým jako jeho jméno (a uloženy v adresáři .git/objects).
Bohužel, jakmile je v projektu uloženo mnoho souborů, stává se tento systém neefektivním. Zkuste toto spustit na nějakém starém projektu:
$ git count-objects 6930 objects, 47620 kilobytes
První číslo udává počet objektů uložených v jednotlivých souborech. Druhé číslo je prostor zabraný těmito "neuspořádanými" objekty.
Můžete ušetřit prostor a urychlit git tím tak že tyto volné objekty přesunete do "balíku" (pack file), který skupinu souborů ukládá v efektivním komprimovaném formátu; detaily o tom jak jsou pack soubory formátovány najdete v dokumentu technical/pack-format.txt.
Pro přesun volných objektů do balíku prostě spusťte příkaz git repack:
$ git repack Generating pack... Done counting 6020 objects. Deltifying 6020 objects. 100% (6020/6020) done Writing 6020 objects. 100% (6020/6020) done Total 6020, written 6020 (delta 4070), reused 0 (delta 0) Pack pack-3e54ad29d5b2e05838c75df582c65257b8d08e1c created.
Poté můžete spustit
$ git prune
čímž odstraníte tyto "volné" objekty, nyní uložené v balíku. Tento příkaz také odstraní všechny nereferencované objekty (které mohou vznikat, například, při spuštění příkazu "git reset" během odstraňování commitu). To že volné objekty zmizely můžete ověřit tak že se podíváte do adresáře .git/objects nebo tak že spustíte
$ git count-objects 0 objects, 0 kilobytes
Ačkoliv jsou objekty pryč, všechny příkazy pracující s těmito objekty budou pracovat přesně jako pracovaly předtím.
Příkaz git-gc(1) provádí packing, pruning, a také daší činnosti, takže obvykle stačí jediný high-level příkaz který potřebujete.
Plandající objekty
Příkaz git-fsck(1) si čas od času stěžuje na plandající objekty (dangling objects). Nejedná se o žádný problém.
Nejčastější příčinou vzniku plandajících objektů je že jste provedli rebase větve, nebo že jste načetli změny od někoho kdo provedl rebase - viz. Kapitola 5, Úpravy historie a údržba posloupnosti patchů. V tom případě starý vrchol původní větve stále existuje, stejně jako vše na co ukazoval. Ukazatel na větev už ale neexistuje, protože jste ho nahradili jiným.
Existují také další situace kdy vznikají plandající objekty. Například, "plandající blob" může vzniknout tak že jste provedlo "git add" souboru, ale potom, před tím než jste ho skutečně commitnuli, jste v něm provedli nějaké změny a commitnuli jste tuto změněnou verzi - starý stav který jste přidali původně skončil tak že na něj neukazuje žádný commit nebo strom, takže je to plandající blob objekt.
Podobně, když je spuštěna "rekurzivní" merge strategie a najde criss-cross merge (merge křížem) a tudíž více než jeden "merge base" (což je celkem neobvyklé ale stává se to), vygeneruje jeden dočasný strom ležící uprostřed (nebo případně i více dočasných stromů, pokud jste provedli mnoho criss-cross mergů a máte více než dvě "merge base") jako dočasnou interní merge base, a opět, toto jsou reálné objekty, ale ve výsledku na ně nebude nic ukazovat, takže budou "plandat" ve vašem repositáři.
Obecně vzato, plandající objekty nejsou nic s čím byste si měli dělat starosti. Dokonce mohou být velice užitečné: pokud něco poděláte, plandající objekty mohou být způsob jak obnovit váš původní strom (řekněme že jste provedli rebase, a pak si uvědomíte že jste to vlastně dělat nechtěli - můžete se podívat jaké plandající objekty máte, a rozhodnout se resetovat vrchol vaší větve na některý starý plandající stav).
Pro commity můžete prostě použít:
$ gitk <dangling-commit-sha-goes-here> --not --all
Tím získáte kompletní historii dostupnou z daného commit ale ne ze žádné větve, tagu nebo odkazu. Pokud dospějete k názoru že to je commit obsahující něco co chcete, můžete na něj vždy vytvořit nový odkaz, např.
$ git branch recovered-branch <dangling-commit-sha-goes-here>
Pro bloby a stromy (trees), nemůžete udělat to samé, ale stále je můžete zkoumat. Můžete prostě spustit
$ git show <dangling-blob/tree-sha-goes-here>
čímž zobrazíte jaký byl obsah blobu (nebo, pro strom, v podstatě jaký byl výstup "ls" pro daný adresář), a to vám může napovědět po které operaci tento plandající objekt zůstal.
Plandající bloby a stromy obvykle nejsou příliš zajímavé. Většinou jsou to důsledky buď částečného merge (blob dokonce často obsahuje vymezení konfliktu z merge, pokud jste při mergování některé konflikty opravovali ručně), nebo prostě proto že jste příkaz "git fetch" přerušili pomocí ^C nebo tak něco, což zanechalo některé nové objekty sice v databázi objektů, ale plandající a nepoužitelné.
Nicméně, jakmile jste si jisti že vás žádné plandající objekty nezajímají, můžete všechny nedosažitelné objekty vyčistit:
$ git prune
a jsou pryč. Avšak příkaz "git prune" byste měli spouštět pouze na nepoužívaném repositáři - je to obdoba kontroly a obnovy souborového systému: to nechcete provádět když je souborový systém připojen (mounted).
(To samé mimochodem platí i pro příkaz "git fsck" samotný, ale protože git fsck nikdy skutečně nemění repositář, pouze oznamuje co našel, není nebezpečné spouštět příkaz git fsck samotný. Jeho spuštění v době kdy někdo skutečně mění repositář může způsobit matoucí a děsivá varování, ale nikdy neudělá nic špatného. Na rozdíl od toho spuštění "git prune" zatímco někdo další aktivně mění repositář je ŠPATNÝ nápad).
Obnova poškozeného repositáře
Git byl navržen tak aby se svěřenými daty zacházel opatrně. Nicméně i kdyby v gitu nebyly žádné chyby, je stále možné že k poškození dat dojde kvůli hardware nebo operačnímu systému.
První ochrana proti takovým problémům je zálohování. Git repositář můžete zazálohovat pomocí klonování, nebo prostě použitím cp, tar nebo jakéhokoliv jiného zálohovacího mechanismu.
Jako poslední možnost můžete zkusit vyhledat poškozené objekty a pokusit se je nahradit ručně. Zazálohujte repositář než se o to pokusíte, pro případ že byste během toho repositář poškodili ještě víc.
Můžeme předpokládat že problém je v jednom chybějícím nebo poškozeném blobu, což je občas řešitelný problém. (Obnova chybějících stromů a zejména commitů je daleko obtížnější).
Než začnete, pomocí příkazu git-fsck(1) ověřte že repositář je skutečně poškozen a zjistěte jak konkrétně; tento krok může být časově dosti náročný.
Řekněme že výstup vypadá asi takhle:
$ git fsck --full
broken link from tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8
to blob 4b9458b3786228369c63936db65827de3cc06200
missing blob 4b9458b3786228369c63936db65827de3cc06200
(Typicky ve výstupu budou i nějaké hlášky o "plandajících objektech", ale ty nejsou zajímavé.)
Nyní víte že blob 4b9458b3 chybí, a že 2d9263c6 na něj ukazuje. Pokud byste dokázali najít byť jen jedinou kopii tohoto chybějíciho blob objektu, možná v nějakém jiném repositáři, mohli byste ji přesunout do .git/objects/4b/9458b3… a bylo by to. Řekněme že to nejde. I tak můžete pomocí příkazu git-ls-tree(1) prozkomat strom který na tento blob odkazuje, a jeho výstup by mohl vypadat asi nějak takhle:
$ git ls-tree 2d9263c6d23595e7cb2a21e5ebbb53655278dff8 100644 blob 8d14531846b95bfa3564b58ccfb7913a034323b8 .gitignore 100644 blob ebf9bf84da0aab5ed944264a5db2a65fe3a3e883 .mailmap 100644 blob ca442d313d86dc67e0a2e5d584b465bd382cbf5c COPYING ... 100644 blob 4b9458b3786228369c63936db65827de3cc06200 myfile ...
Takže teď víte že chybějící blob obsahoval data pro soubor nazvaný "myfile". A je naděje že dokážete identifikovat také adresář - řekněme že to je v "somedirectory". Když budete mít štěstí, chybějící kopie by mohla být stejná jako kopie kterou máte načtenu do pracovního stromu v cestě "somedirectory/myfile"; odzkoušet to můžete pomocí příkazu git-hash-object(1):
$ git hash-object -w somedirectory/myfile
což vytvoří a uloží blob objekt obsahující data z somedirectory/myfile, a vypíše SHA-1 daného objektu. Pokud máte velké štěstí, může to být 4b9458b3786228369c63936db65827de3cc06200, a v tomto případě jste to odhadli správně, a poškození je opraveno!
V opačném případě potřebujete více informací. Jak zjistíte která verze souboru se ztratila?
Nejjednodušší způsob jak to zjistit je příkazem:
$ git log --raw --all --full-history -- somedirectory/myfile
Protože jste si vyžádali hrubý (raw) výstup, dostanete něco jako
commit abc Author: Date: ... :100644 100644 4b9458b... newsha... M somedirectory/myfile commit xyz Author: Date: ... :100644 100644 oldsha... 4b9458b... M somedirectory/myfile
To říká že bezprostředně předcházející verze souboru byla "newsha" a že bezprostředně následující verze byla "oldsha". Víte také popisy commitů doprovázající změnu z oldsha na 4b9458b a změnu z 4b9458b na newsha.
Pokud jste commitovali dostatečně malé změny, může vám to výrazně pomoci při rekonstrukci obsahu stavu ležícího 4b9458b mezi uvedenými dvěmi revizemi.
Pokud to dokážete, můžete chybějící objekt vytvořit příkazem
$ git hash-object -w <recreated-file>
a repositář je opět v pořádku!
(Btw, mohli jste úplně ignorovat fsck, a začít příkazem
$ git log --raw --all
a hledat data chybějícího objektu (4b9458b..) v kompletním výpisu. Je to na vás - v gitu je spousta informací, pouze mu chybí jedna konkrétní verze blobu.
Index
Index je binární soubor (zpravidla uložený v .git/index) obsahující setříděný seznam cest, každý s informací o přístupových právech a SHA-1 blob objektu; příkaz git-ls-files(1) vám obsah indexu může přehledně vypsat:
$ git ls-files --stage 100644 63c918c667fa005ff12ad89437f2fdc80926e21c 0 .gitignore 100644 5529b198e8d14decbe4ad99db3f7fb632de0439d 0 .mailmap 100644 6ff87c4664981e4397625791c8ea3bbb5f2279a3 0 COPYING 100644 a37b2152bd26be2c2289e1f57a292534a51a93c7 0 Documentation/.gitignore 100644 fbefe9a45b00a54b58d94d06eca48b03d40a50e0 0 Documentation/Makefile ... 100644 2511aef8d89ab52be5ec6a5e46236b4b6bcd07ea 0 xdiff/xtypes.h 100644 2ade97b2574a9f77e7ae4002a4e07a6a38e46d07 0 xdiff/xutils.c 100644 d5de8292e05e7c36c4b68857c1cf9855e3d2f70a 0 xdiff/xutils.h
Všimněte si že ve starší dokumentaci byl index nazýván "current directory cache" (cache aktuálního adresáře) nebo prostě "cache". Má tři důležité vlastnosti:
-
Index obsahuje veškeré informace potřebné k vygenerování jednoho (jednoznačně určeného) tree objektu.
Například spuštění příkazu git-commit(1) tento strom vygeneruje z indexu, uloží ho v databázi objektů, a použije ho jako strom asociovaný s novým commitem.
-
Index umožňuje rychlé srovnávání mezi tree objektem v něm definovaným a pracovním stromem.
To je prováděno ukládáním některých dalších dat pro každou položku (jako například čas poslední modifikace). Tato data nejsou výše zobrazena, ani nejsou ukládána ve vytvořeném tree objektu, ale mohou být použita k rychlému určení které soubory v pracovním adresáři se liší od stavu uloženého v indexu, a tudíž ušetřit gitu čtení všech dat z těchto souborů při hledání změn.
-
Může efektivně reprezentovat informace o merge konfliktech mezi různými tree objekty, protože umožňuje aby s každou cestou bylo spojeno dostatek informací o zúčastněných stromech aby mezi nimi bylo možno provést třísměrný merge.
V odstavci nazvaném “Getting conflict-resolution help during a merge” že během merge může index ukládat několik verzí jediného souboru (nazývaných "stages"). Třetí sloupec ve výše uvedeném výstupu příkazu git-ls-files(1) je číslo stage (stage number), a jiných hodnot než 0 bude nabývat pro pro soubory s merge konflikty.
Takže index je druh dočasného úložného prostoru, do kterého je ukládán strom na kterém aktuálně pracujete.
Pokud index kompletně zrušíte, v podstatě jste neztratili žádné informace pokud znáte název stromu který popisoval.




