Git - Kapitola 5. Úpravy historie a údržba posloupnosti patchů
Vytváření dokonalé posloupnosti patchů / Udržování posloupnosti patchů aktuální díky git rebase / Přepis jediného commitu / Přeskupení nebo výběr s posloupnosti patchů / Další nástroje / Problémy s přepisem historie / Proč může být pitvání (bisecting) merge commitů složitější než v případě lineární historie
Obsah
- Vytváření dokonalé posloupnosti patchů
- Udržování posloupnosti patchů aktuální díky git rebase
- Přepis jediného commitu
- Přeskupení nebo výběr s posloupnosti patchů
- Další nástroje
- Problémy s přepisem historie
- Proč může být pitvání (bisecting) merge commitů složitější než v případě lineární historie
Za normálních okolností jsou commity do projektu pouze přidávány, nikdy nejsou odebírány nebo nahrazovány. Git je s tímto předpokladem navržen, a jeho porušení způsobí že (například) mergování nebude fungovat správně.
Nicméně, existuje situace kdy může být užitečné tento předpoklad porušit.
Vytváření dokonalé posloupnosti patchů
Předpokládejme že jste přispěvatel do velkého projektu, a chcete přidat komplikovanou vlastnost, a prezentovat ji ostatním vývojářům v takové podobě aby pro ně bylo jednoduché sledovat vámi provedené změny, ověřit že jsou správné, a porozumět proč jste každou z nich provedli.
Pokud všechny své změny provedete v jediném patchi (nebo commitu), nemusí to pro ně být dostatečně stravitelné.
Pokud jim představíte kompletní historii včetně omylů, oprav a slepých uliček, mohou být zahlceni.
Takže ideální je obyvykle vytvořit takovou sérii patchů, že:
- Každý patch může v daném pořadí.
- Každý patch zahrnuje jednu logickou změni, společně s popisem vysvětlující tuto změnu.
- Žádný patch nepředstavuje pohyb zpět (regression): po aplikace jakékoliv části posloupnosti patchů je projekt stále kompilovatelný a funkční, a nemá žádné chyby které neměl předtím.
- Kompletní posloupnost patchů dává stejný výsledek jako dal váš vlastní (pravděpodobně daleko zaneřáděnější) vývoj.
Představíme vám několik nástrojů které vám v tom mohou pomoci, vysvětlíme vám jak je používat, a popíšeme nekolik problémů které z přepisování historie mohou vzniknout.
Udržování posloupnosti patchů aktuální díky git rebase
Předpokládejme že vytvoříte větev "mywork" na remote-tracking větvi "origin" a vložíte do ní několik commitů:
$ git checkout -b mywork origin $ vi file.txt $ git commit $ vi otherfile.txt $ git commit ...
Zatím jste do mywork nic nepřimergovali, takže se jedná pouze o jednoduchou lineární posloupnost patchů aplikovaných na větev "origin":
o--o--o <-- origin
\
o--o--o <-- mywork
V nadřazeném projektu byly provedeny nějaké zajímavé změny, a větev "origin" se tak posunula:
o--o--O--o--o--o <-- origin
\
a--b--c <-- mywork
V tento okamžit byste mohli použít "pull" pro začlenění vašich změn zpět; a výsledek by vytvořil nový "merge commit", asi takto:
o--o--O--o--o--o <-- origin
\ \
a--b--c--m <-- mywork
Nicméně, pokud dáváte přednost udržování historie v mywork v podobě jednoduché posloupnosti bez jakýchkoliv mergů, můžete namísto toho použít příkaz git-rebase(1):
$ git checkout mywork $ git rebase origin
To odstraní všechny vaše commity z mywork větve, dočasně je uloží jako patche (v adresáři pojmenovaném ".git/rebase-apply"), aktualizuje mywork do stavu poslední verze origin, a poté na větev mywork aplikuje jeden po druhém uložené patche. Výsledek bude vypadat takto:
o--o--O--o--o--o <-- origin
\
a'--b'--c' <-- mywork
Během procesu se mohou objevit konflikty. V tom případě bude proces zastaven a dostanete možnost tyto konflikty opravit; po jejich odstranění použijte git add pro přidání změn do indexu, a nakonec namísto git commit spusťte prostě
$ git rebase --continue
a git bude pokračovat v aplikování zbývajících patchů.
V každém okamžiku můžete použít volbu —abort pro přerušení procesu a navrácení větve mywork dostavu ve kterém byla před spuštěním příkazu rebase:
$ git rebase --abort
Přepis jediného commitu
V odstavci nazvaném “Oprava omylu přepisem historie” že poslední commit můžete nahradit příkazem
$ git commit --amend
který nahradí starý commit novým zahrnujícím vaše změny, přičemž vám nejdříve umožní upravit starý popis commitu.
Můžete také použít kombinaci tohoto příkazu a git-rebase(1) pro nahrazení commitu dále v historii a přegenerování zasažených pozdějších změn. Nejdříve otagujme proglematický commit příkazem
$ git tag bad mywork~5
(Buď gitk or git log mohou být užitečné při hledání problematického commitu.)
Pro načtení commitu, jeho úpravu a rebase navazujícího zbytku posloupnosti patchů (všimněte si že bychom mohli načíst commit do dočasné větve, ale namísto toho používáme odpojený vrchol - detached head):
$ git checkout bad $ # make changes here and update the index $ git commit --amend $ git rebase --onto HEAD bad mywork
Až budete hotovi, bude větev mywork načtena (checked out), s posledními patchi opět aplikovanými na váš upravený commit. Poté můžete provést vyčištění příkazem
$ git tag -d bad
Všimněte si že neměnitelná podstata git historie znamená že jste ve skutečnosti neprovedli "změnu" existujících commitů; namísto toho jste staré commity nahradili novými s novými jmény objektu (object name).
Přeskupení nebo výběr s posloupnosti patchů
Uvažme jeden existující commit; příkaz git-cherry-pick(1) vám umožňuje aplikovat změny v něm provedené a vytvořit commit který je obsahuje. Takže například pokud větev "mywork" ukazuje na posloupnost patchů nad větví "origin", můžete udělat něco jako
$ git checkout -b mywork-new origin $ gitk origin..mywork &
a projít seznamem patchů ve větvi mywork pomocí gitk, aplikovat je (možná v jiném pořadí) na větev mywork-new pomocí cherry-pickingu, a možná je ještě během toho modifikovat pomocí příkazu git commit —amend. Příkaz git-gui(1) vám také může pomoci protože vám umožňuje vybírat jednotlivé diff úseky které se mají zahrnout do indexu (stačí kliknout pravým tlačitkem na diff úsek a zvolit "Stage Hunk for Commit").
Další metodou je použití příkazu git format-patch pro vytvoření posloupnosti patchů, ta poté reset do stavu před patchi:
$ git format-patch origin $ git reset --hard origin
Poté modifikujte, přeuspořádejte nebo eliminujte patche dle libosti před jejich opětovnou aplikací pomocí příkazu git-am(1).
Další nástroje
Existuje celá řada dalších nástrojů, jako například StGIT, existujících čistě pro účely údržby série patchů. Tyto nástroje jsou mimo rámec tohoto manuálu.
Problémy s přepisem historie
Primární problém s přepisem historie větve souvisí s mergováním. Předpokládejme že si někdo načte vaši větev a přimerguje ji do své větve, což ve výsledku znamená něco takového:
o--o--O--o--o--o <-- origin
\ \
t--t--t--m <-- their branch:
Předpokládejme že poté upravíte poslední tři commit:
o--o--o <-- new head of origin
/
o--o--O--o--o--o <-- old head of origin
Pokud toto všechno zaznamenáme v rámci jednoho repositáře, bude to vypadat asi takhle:
o--o--o <-- new head of origin
/
o--o--O--o--o--o <-- old head of origin
\ \
t--t--t--m <-- their branch:
Git nemá žádnou možnost jak zjistit že nový vrchol je aktualizovaná verze starého vrcholu; v této situaci se chová stejně jako kdyby dva vývojáři nezávisle na sobě provedli paralelní změny na starém a novém vrcholu. Pokud se nyní někdo pokusí přimergovat nový vrchol do své větve, git se pokusí spojit obě (starou i novou) linie vývoje, namísto toho aby se pokusil nahradit starou linii tou novou. Výsledky nejspíše budou neočekávané.
I přesto se můžete rozhodnout publikovat větve jejichž historie byla přepsána, a pro ostatní může být užitečné mít možnost tyto větve načíst pro jejich vyzkoušení nebo otestování, ale neměli by zkoušet takové větve začleňovat do své práce.
Pro skutečně distribuovaný vývoj podporující správné mergování, by historie publikovaných větví nikdy neměla být upravována.
Proč může být pitvání (bisecting) merge commitů složitější než v případě lineární historie
Příkaz git-bisect(1) korektně funguje i v případě historie zahrnující merge commity. Nicméně, pokud je nalezený commit merge commitem, uživatel může být přinucen k obtížnější práci než obvykle aby zjistil proč daný commit představuje problém.
Představte si tuto historii:
---Z---o---X---...---o---A---C---D
\ /
o---o---Y---...---o---B
Předpokládejme že na horní linií vývoje, je smysl jedné z funkcí existujících v Z je změněnav v commitu X. Commity vedoucí ze Z do A mění jak implementaci funkce tak i místa volání existující v Z, stejně jako přidávají nová místa volání, tak aby zachována konzistence. V commitu A není žádná chyba.
Dejme tomu že mezi tím na druhé linii vývoje někdo přidá v commitu Y nové volání dané funkce. Commity ze Z do B všechny předpokládají starou sémantiku dané funkce a funkce i místa volání jsou navzájem konzistentní. V commitu B také není žádná chyba.
Řekněme že dané dvě linie vývoje se v C čistě změrgují, takže žádné řešení konfliktů není potřeba.
Nicméně kód v C je chybný, protože místa volání přidaná ve spodní linii vývoje nebyla upravena na novou sémantiku zavedenou v horní linii vývoje. Takže pokud vše co víte je že commit D je chybný, že Z je korektní, a že příkaz git-bisect(1) identifikuje C jako viníka, jak zjistíte že daný problém plyne z uvedené změny sémantiky volání?
Pokud je výsledkem příkazu git bisect non-merge commit, měli byste za normálních okolností být schopni zjistit problém zkoumáním pouze daného commitu. Vývojáři to mohou zjednodušit dělením svých změn do malých samostaných commitů. To ve výše uvedeném případě nepomůže, avšak protože problém není ze zkoumání jednotlivých commitů zřejmý; namísto toho je nutný globální pohled na vývoj. Aby to nebylo málo, změna v sémantice problématické funkce může být pouze jednou drobnou součástí v horní linii vývoje.
Na druhou stranu, pokud byste namísto mergování v C probedli rebase historie mezi Z a B na A, dostali byste se k následující lineární historii:
---Z---o---X--...---o---A---o---o---Y*--...---o---B*--D*
Pitvání (bisecting) historie mezi Z a D* by označilo jako jediného viníka commit Y*, a porozumění proč byl commit Y* rozbit by pravděpodobně bylo jednodušší.
Částečně i z tohoto důvodu mnoho zkušených uživatelů gitu, dokonce i když pracují s projekty které jsou obecně "merge-heavy", udržují historii lineární tím že před zveřejněním provádějí rebase oproti poslední upstream verzi .




