Za krásnout timeline v gitu
Práce s Gitem: Rebase vs. Merge a udržování čitelné historie
Git je mocný nástroj pro správu verzí, který na rozdíl od konkurenčních systémů jako Mercurial či Bazaar umožňuje přepisovat historii. Tato funkce je extrémně užitečná, ale také vyžaduje opatrnost – přirovnání k ostrému noži je zde zcela na místě.
Proč dbát na čitelnou historii commitů?
Pokud je pro vás práce s repozitářem checkout na konkrétní branch a pak už jen kód; bude to pro vás asi zbytečná práce navíc. Je třeba se naučit některé postupy, protože když to budete řešit postaru, tak umřete.
Upravit historii commitů tak, aby byla přehledná, je nejen estetickou záležitostí, ale je to hlavně praktické:
- Historie jasně ukazuje, co z čeho vychází, a usnadňuje code review.
- Oddělení nesouvisejících změn zjednodušuje jejich správu a nasazení.
- Konflikty vznikají dříve a v menším měřítku, což usnadňuje jejich řešení.
- Konflikty řeší autor změn, který jim nejlépe rozumí.
Při práci na featurách obvykle začnu s hrubým rozložením commitů, které posléze zpětně upravím, aby reflektovaly logickou posloupnost změn. Díky tomu se do repozitáře nedostanou zbytečné chyby, dočasné úpravy nebo hesla.
Chci v historii commitů zanést přidávání/odebírání/změny featur. To, že jsem si zrovna v 15:31 aktualizoval base od kolegů nikoho nezajímá.
S commity se dá manipulovat. Když na nějaké featuře pracuji delší dobu, vznikají commity s featurou nesouvisející, které vznikli uvědoměnním si nějakého nedostatku. Tyto commity nemusejí čekat, až konečně featuru dodělám. A rovnou je mohu mergnout či cherry-picknout vejš.
Rebase vs. Merge: Kdy použít který?
Při synchronizaci kódu mezi branchemi existují dvě hlavní možnosti: rebase a merge.
Rebase:
- Používám při průběžném udržování branche v aktuálním stavu proti parent branchi.
- Výsledkem je čistá historie bez nadbytečných merge commitů.
- Vytváří souvislou linii commitů, což je přínosné při debugování.
- Pozor: Přepisuje historii, a proto pozor u sdílené branche.
Merge:
- Používám při integraci hotové feature branch.
- Vytváří merge commit, který jasně ukazuje, že byla feature začleněna. Chci tam kolejničky.
Nejčastější problémy při použití rebase
Ztráta dat při force push
Pokud po rebase provedete git push --force, přepíšete historii na serveru. Pokud mezitím kolega pushnul změny, mohou se nenávratně ztratit. (Ještě se to dá někdy zachránít pomocí bitsec, ale potřeboval jsem to asi dvakrát v životě.)
Tip: Před push --force
si stáhněte aktuální stav repozitáře
pomocí git fetch
.
Problémy při pullu forceované branche
Pokud se pokusím aktualizovat lokální branch z branche která byla rebasována a forceovaná na vzdáleném repozitáři, dojde ke vzniku nadbytečnému merge commitu.
Řešení: Použít git pull --rebase
.
Zastarání feature branche a příiš mnoho konfliktů
Pokud jsem na nějakou branch dlouho nešáhl, mohlo mezi tím v rodičovské branchy přibýt příliš mnoho změn.
Nejdříve to zkusím podobrém:
git checkout <parent> && git pulreb
git checkout <child> && git rebasef <parent>
A nebo se na to vykašlu. Dám git rebase --abort
, a udělám to humpolácky:
- Přejmenuji <child> na <child-backup>
- Vytvořím si novou <child> z HEAD <parent>. Čímž budu mít všechny změny kde potřebuji.
- Všechny commity z <child-backup> nacherry-pickuju do nově vytvořené <child>.
Oni mi tam samozřejmě vzniknou podobné konflikty jako v předchozím případě. Jenže jich vznikne mnohem méně, a jsou srozumitelnější.
Feature branching workflow
Běžně používám feature branching, tedy každou featuru vyvíjím v samostatné branchi. Během práce pravidelně rebasuji proti parent branchi (např. master nebo v1.0):
git checkout v1.0 && git pulreb
git checkout v1.0-query-parser && git rebasef v1.0
Když je feature hotová, sloučím ji merge committem:
git checkout v1.0 && git mergef v1.0-query-parser
Následně branch smažu:
git branch -d v1.0-query-parser
Zatímco u aktualizace feature branche chci, aby se mi všechny nově příchozí commity pěkně přeskládali před ty moje, v případě zařazení do hlavní branche chci, aby to bylo vidět, aby tam byly "kolejničky".
Zkratky, které užívám
Pro časté operace jsem si definoval aliasy:
- Stažení a zaktualizování branche
pulreb = pull --rebase
- Stažení a zaktualizování branche
pulreb = pull --rebase
- Rebase proti zadané branchi
rebasef = rebase --rebase-merges
- Merge s vytvořením kolejniček
mergef = merge --no-ff
- Rebasnout všechny čekající "fixup" commity
rebase-auto = "rebase -i --autosquash"
- Přidání staged změn do konkrétního commitu
ammendto = "!f() { git commit --fixup=$1 && git rebase-auto $1~1; }; f"
Závěr
Git je mocný nástroj, který při správném použití může usnadnit řêdu každodenních operací. Používání rebase a merge na správných místech pomáhá udržet historii čitelnou a usnadnit spolupráci v týmu. Klíčové je rozumět rizikům a vědět, kdy je který postup vhodný.