Za krásnou 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é:

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é vznikly uvědomění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:

Merge:

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í git reflog, ale potřeboval jsem to asi dvakrát v životě.)

Jenže já samozřejmě chci přepsat tu historii, o tom to přeci je!

Mám tyto možnosti::

git push --force

Přepíše vzdálenou větev bez ohledu na to, co se na ní mezitím změnilo. Je to destruktivní operace – pokud někdo jiný mezitím pushoval do stejné větve, jeho commity budou ztraceny.

git push --force-with-lease

Přepíše vzdálenou větev, ale pouze pokud se shoduje s tím, co je naposledy fetchnuté. Pokud někdo jiný mezitím pushoval, Git operaci odmítne.

git push --force-with-lease --force-if-includes

Přepíše vzdálenou větev, ale pouze pokud se shoduje s tím, co je naposledy fetchnuté. Pokud někdo jiný mezitím pushoval, Git operaci odmítne. Navíc ale ověří, že nové remote commity jsou součástí lokální historie

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é branchi 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:

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ádaly 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
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 řadu 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ý.

Také píšou: