Git - Kapitola 9. Nízkoúrovňové git operace

Přístup k objektům a manipulace s nimi / Pracovní postup (Workflow) / Pracovní adresář -> index / Index -> databáze objektů / Databáze objektů -> index / Index -> pracovní adresář / Spojujeme všechno dohromady / Zkoumání dat / Spojování několika stromů / Slučování několika stromů, pokračování

Obsah

Přístup k objektům a manipulace s nimi
Pracovní postup (Workflow)
Pracovní adresář -> index
Index -> databáze objektů
Databáze objektů -> index
Index -> pracovní adresář
Spojujeme všechno dohromady
Zkoumání dat
Spojování několika stromů
Slučování několika stromů, pokračování

Mnoho vysokoúrovňových příkazů bylo původně implementováno ve formě shellových skriptů využívajících menší jádro nízkoúrovňových příkazů. I ty ale mohou být užitečné pokud s gitem potřebujete provést neobvyklé věci, nebo pokud chcete pochopit jak git uvnitř funguje.

Přístup k objektům a manipulace s nimi

Příkaz git-cat-file(1) může zobrazit obsah libovolného objektu, i když výše postavený příkaz git-show(1) je obvykle užitečnější.

Příkaz git-commit-tree(1) vám dává možnost vytvářet commity s libovolnými rodiči a stromy.

Strom může být vytvořen příkazem git-write-tree(1) a k jeho datům lze přistupovat příkazem git-ls-tree(1). Dva stromy lze porovnávat příkazem git-diff-tree(1).

Tagy jsou vytvářeny příkazem git-mktag(1), a jeho signatura může být ověřena příkazem git-verify-tag(1), ačkoliv pro oboje je obvykle jednodušší použít příkaz  git-tag(1).

Pracovní postup (Workflow)

Vysokoúrovňové operace jako například git-commit(1), git-checkout(1) a git-reset(1) fungují tak že přesouvají data mezi pracovním stromem, indexem a databází objektů. Git nabízí nízkoúrovňové operace které tyto kroky provádí jednotlivě.

Obecně vzato, všechny "git" operace pracují na indexu. Některé operace pracují pouze s indexem (zobrazují aktuální stav indexu), ale většina operací přesouvá data mezi indexem a buď databází nebo pracovním stromem. Takže existují čtyři hlavní kombinace:

Pracovní adresář -> index

Příkaz git-update-index(1) aktualizuje index informacemi z pracovního adresáře. Stručně řečeno index aktualizujete zadáním jména souboru který chcete aktualizovat, asi takhle:

$ git update-index filename

ale kvůli vyhnutí se běžným problémům s "filename globbing" (zadávání jmen pomocí zástupných symbolů) etc, příkaz za normálních okolností nebude přidávat úplně nové položky nebo odstraňovat existující, t.j. pouze aktualizuje již existující položky v cache.

Pokud chcete gitu sdělit že ano, že si skutečně uvědomujete že některé soubory již neexistují, nebo že by měl přidat nové soubory, měli byste použít přepínače —remove a —add.

POZOR! Přepínač —remove neznamená že následující soubory nutně budou odstraněny: pokud soubory ve vaší adresářové struktuře stále existují, bude index aktualizován jejich novým stavem, nebudou odstraněny. Jediné co přepínač —remove říká je že příkaz update-index bude odstranění souboru považovat za platnou operaci, a pokud soubor skutečně neexistuje, odpovídajícím způsobem opraví index.

Jako zvláštní případ můžete také zavolat příkaz git update-index —refresh, který aktualizuje "stat" informace každého souboru tak aby odpovídaly aktuálnímu stavu. Neprovede aktualizaci stavu objektu jako takového, a aktualizuje pouze položky používané pro rychlý test zda objekt stále odpovídá svému původnímu store objektu (old backing store object).

Dříve představený příkaz git-add(1) je pouhý wrapper příkazu git-update-index(1).

Index -> databáze objektů

Svůj aktuální index přepíšete do "stromu" příkazem

$ git write-tree

který nemá žádné přepínače - pouze přepíše aktuální index do množiny stromu popisujících daný stav, a vrátí jméno nejvýše postaveného stromu. Daný strom můžete kdykoliv použít v opačném směru pro přegenerování indexu:

Databáze objektů -> index

Načte "strom" z databáze objektů, a využije ho k naplnění (a přepsání - nepoužívejte ho pokud v indexu máte neuložená data který byste později mohli potřebovat!) vašeho aktuálního indexu. Normální použití je prostě

$ git read-tree <SHA-1 of tree>

a váš index bude ekvivalentní dříve uloženému stromu. Nicméně, to je pouze váš index: obsah vašeho pracovního adresáře modifikován nebyl.

Index -> pracovní adresář

Váš pracovní adresář z indexu aktualizujete "načítáním" (checking out) souborů. To není příliš častá operace, protože za normálních okolností byste soubory prostě udržovali aktuální, a namísto zapisování do vašeho pracovního adresáře byste index informovali o změnách ve vašem pracovním adresáři provedených. (tj. příkazem git update-index).

Nicméně, pokud se rozhodnete přeskočit na novou verzi, nebo načíst verzi někoho jiného, nebo prostě obnovit předchozí strom, nejdříve byste index naplnili příkazem read-tree, a potom potřebujete výsledek načíst příkazem

$ git checkout-index filename

nebo, pokud chcete načíst kompletní obsah indexu, použijte přepínač -a.

POZNÁMKA! Příkaz git checkout-index standardně odmítá přepisovat staré soubory, takže pokud už máte načtenu starou verzi stromu, budete muset použít přepínač "-f" (před přepínačem "-a" nebo před jménem) pro vynucení načtení (checkout).

Konečně, existuje několik maličkostí které nespočívají v pouhém přesouvání z jedné reprezentace do druhé:

Spojujeme všechno dohromady

Pro commit stromu který jste vytvořili příkazem "git write-tree" potřebujete vytvořit "commit" objekt odkazující na daný strom a historii za ním - zejména na "rodičovské" commity které mu v historii předcházejí.

Za normálních okolností má "commit" jednoho rodiče: předchozí stav stromu před tím než byla provedena změna. Nicméně čas od času může mít jednoho nebo více rodičovských commitů, a v tomto případě mu říkáme "merge", protože takovýc commit spojuje (merge) dohromady dva nebo více předcházejících stavů reprezentovaných dalšími commity.

Jinými slovy, zatímco "strom" reprezentuje konkrétní stav pracovního adresáře, "commit" reprezentuje "stav v daném okamžiku", a vysvětluje jak jsme k němu dospěli.

Commit objekt vytvoříte tak že zadáte strom popisující stav v okamžiku commitu, a seznam rodičů:

$ git commit-tree <tree> -p <parent> [-p <parent2> ..]

a poté mu zadáte důvod commitu přes stdin (buď přesměrováním z roury nebo souboru, nebo prostě zápisem na konzoli).

Příkaz git commit-tree vrátí jméno objektu reprezentujícího daný commit, a vy byste si ho měli  uložit pro pozdější použití. Obvykle byste commitovali nový stav pro HEAD, a zatímco gitu je jedno kam ukládáte informace o tomto stavu, v praxi je většinou uchováváme do souboru na který ukazuje .git/HEAD, díky čemuž můžeme vždy vidět jaký stav byl commitnutý naposledy.

Tady je ASCII art diagram od Jona Loeligera ilustrující jak jednotlivé kousky zapadají do sebe.


                     commit-tree
                      commit obj
                       +----+
                       |    |
                       |    |
                       V    V
                    +-----------+
                    | Object DB |
                    |  Backing  |
                    |   Store   |
                    +-----------+
                       ^
           write-tree  |     |
             tree obj  |     |
                       |     |  read-tree
                       |     |  tree obj
                             V
                    +-----------+
                    |   Index   |
                    |  "cache"  |
                    +-----------+
         update-index  ^
             blob obj  |     |
                       |     |
    checkout-index -u  |     |  checkout-index
             stat      |     |  blob obj
                             V
                    +-----------+
                    |  Working  |
                    | Directory |
                    +-----------+

Zkoumání dat

Data uložená v databázi objektů a v indexu můžete zkoumat díky různým pomocným nástrojlům. Pro každý objekt můžete použít příkaz git-cat-file(1) kterým prozkoumáte detaily o objektu:

$ git cat-file -t <objectname>

zobrazí typ objektu, a jakmile zjistíte typ type (což obvykle vyplývá z toho kde jste objekt nalezli), můžete použít příkaz

$ git cat-file blob|tree|commit|tag <objectname>

pro zobrazení jeho obsahu. POZNÁMKA! Stromy mají binární obsah, a tak pro zobrazení jejich obsahu existuje speciální příkaz, nazývaný git ls-tree, který převádí binární obsah do čitelnější podoby.

Poučné je zejména podívat se na "commit" objekty, protože ty mají tendenci být malé a poměrně sebevysvětlující. Konkrétně, pokud dodržujete zvyklost že jméno posledního commitu je udržováno v souboru .git/HEAD, můžete spustit

$ git cat-file commit HEAD

a uvidíte o jaký commit se jednalo.

Spojování několika stromů

Git vám pomůže provádět třísměrný merge, který můžete rozšířit na n-směrný opakováním  procedury mergování libovolně-krát až do chvíle kdy "commitnete" stav. Obvyklá situace je že provádíte pouze třísměrný merge (dva předci), a commitnete ho, ale pokud chcete můžete v jednom kroku sloučit několik rodičů najednou.

K provedení třísměrného merge potřebujete dvě množiny "commit" objektů které chcete sloučit, použijete je k nalezení nejbližšího společného předka (třetí "commit" objekt), a poté tyto commit objekty použijete k nalezení stavu adresáře ("tree" objekt) v těchto bodech.

Pro nalezení "base" daného merge nejdříve vyhledáte společného předka daných commitů příkazem

$ git merge-base <commit1> <commit2>

který vrátí commit na kterém jsou oba založeni. Nyní byste měli vyhledat "tree" objekty těchto commitů, což můžete jednoduše provést příkazem (například)

$ git cat-file commit <commitname> | head -1

protože informace o tree objektu je vždy první na řádku v commit objektu.

Jakmile znáte tři stromy které budete mergovat ("původní" strom, nazývaný také společný strom, a dva "výsledné" stromy, označované také jako slučované větve), provedete "merge" read do indexu. Ten si bude stěžovat pokud musí zahodit původní obsah indexu, takže byste se měli ujistit že jste všechny změny commitnuli - ve skutečnosti byste merge vždy prováděli oproti svému poslednímu commitu (což by tedy mělo obsahovat aktuálnímu obsahu vašeho indexu).

K provedení merge spusťte příkaz

$ git read-tree -m -u <origtree> <yourtree> <targettree>

který pro vás přímo v indexu provede všechny triviální merge operace, a vy tak výsledek můžete jednoduše zapsat příkazem git write-tree.

Slučování několika stromů, pokračování

Bohužel, mnoho mergů je netriviálních. Pokud existují přidané, přesunuté nebo smazané soubory, nebo pokud byl v obou větvích upraven stejný soubor, získáte index strom obsahující "merge entries". Takový index nemůže být zapsán do tree objektu, a vy budete muset ručně vyřešit všechny takové merge konflikty pomocí dalších nástrojů před tím než vám zapsání indexu do stromu bude umožněno.

Takový index můžete prozkoumat pomocí příkazu git ls-files —unmerged. Příklad:

$ git read-tree -m $orig HEAD $target
$ git ls-files --unmerged
100644 263414f423d0e4d70dae8fe53fa34614ff3e2860 1       hello.c
100644 06fa6a24256dc7e560efa5687fa84b51f0263c3a 2       hello.c
100644 cc44c73eb783565da5831b4d820c962954019b69 3       hello.c

Každý řádek ve výstupu git ls-files —unmerged začíná "blob mode" bity, SHA-1 názvem, stage number, a jménem souboru. Položka stage number je způsob kterým vám git říká ze kterého stromu pochází: stage 1 odpovídá $orig stromu, stage 2 odpovídá HEAD stromu, a stage3 odpovídá $target stromu.

Dříve jsme uvedli že triviální merge jsou prováděny v rámci příkazu git read-tree -m. Například, pokud se soubor nezměnil ani mezi $orig a HEAD ani v $target, nebo pokud se soubor mezi $orig a HEAD a mezi $orig a $target změnil stejným způsobem, je výsledek evidentně totožný s tím co je v HEAD. Výše uvedený příklad ukazuje že soubor hello.c se mezi $orig a HEAD a $orig a $target změnil různými způsoby. To byste mohli vyřešit spuštěním svého oblíbeného 3-směrného mergovacího programu, například diff3, merge, nebo merge-file přímo z gitu, sami na blob objektech z těchto tří fází, například takto:

$ git cat-file blob 263414f... >hello.c~1
$ git cat-file blob 06fa6a2... >hello.c~2
$ git cat-file blob cc44c73... >hello.c~3
$ git merge-file hello.c~2 hello.c~1 hello.c~3

Tento příkaz ponechá výsledek merge v souboru hello.c~2, společně s vymezením konfliktů pokud existují. Po ověření zda merge dává smysl můžete gitu říci jaký je konečný výsledek merge pro tento soubor:

$ mv -f hello.c~2 hello.c
$ git update-index hello.c

When a path is in the "unmerged" state, running git update-index for that path tells git to mark the path resolved.

Pokud je daná cesta ve stavu "unmerged", spuštění příkazu git update-index pro danou cestugitu řekne aby merge označil jako vyřešený.

Výše uvedený popis se zabývá příkazem git merge na nejnižší úrovni, aby vám pomohl porozumět co se koncepčně děje pod povrchem. V praxi nikdo, dokonce ani git samotný, k tomu nespouští git cat-file. Existuje příkaz git merge-index který vytáhne stage do dočasných souborů a zavolá na nich "merge" skript:

$ git merge-index git-merge-one-file hello.c

a právě pomocí něj je implementován i příkaz git merge -s resolve.

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é)