Merging vs. rebasing
De opdracht git rebase
heeft de reputatie een magische Git-formule te zijn waarbij beginners weg moeten blijven, maar die het leven van een ontwikkelingsteam veel gemakkelijker kan maken – mits hij met zorg wordt gebruikt. In dit artikel vergelijken we git rebase
met de gerelateerde opdracht git merge
en identificeren we alle mogelijkheden om rebasing op te nemen in een typische Git-workflow.
Conceptual overview
Het eerste dat je over git rebase
moet begrijpen, is dat de opdracht hetzelfde probleem oplost als git merge
. Beide opdrachten zijn ontworpen om wijzigingen van de ene branch in een andere branch te integreren. Ze doen het gewoon op heel verschillende manieren.
Bedenk wat er gebeurt als je begint te werken aan een nieuwe functie in een speciale branch, waarna een ander teamlid de main
-branch bijwerkt met nieuwe commits. Dit resulteert in een gevorkte geschiedenis. Die zou bekend moeten zijn bij iedereen die Git heeft gebruikt als hulpprogramma voor samenwerking.
Stel nu dat de nieuwe commits in main
relevant zijn voor de functie waaraan je werkt. Je hebt twee opties om de nieuwe commits in je functie
-branch op te nemen: samenvoegen of rebasen.
gerelateerd materiaal
Een volledige Git-repository verplaatsen
Oplossing bekijken
Git leren met Bitbucket Cloud
The merge option
De eenvoudigste optie is om de main
-branch samen te voegen in de functie-branch met zoiets als het volgende:
git checkout feature
git merge main
Je kunt dit ook samenvatten als een oneliner:
git merge feature main
Dit creëert een nieuwe 'merge commit' in de functie
-branch die de geschiedenissen van beide branches met elkaar verbindt, waardoor je een branch-structuur krijgt die er zo uitziet:
Samenvoegen is leuk omdat het een niet-destructieve bewerking is. De bestaande branches worden op geen enkele manier gewijzigd. Dit vermijdt alle mogelijke valkuilen van rebasing (dat hieronder wordt besproken).
Aan de andere kant betekent dit ook dat de functie
-branch een externe merge-commit zal hebben zodra je upstream wijzigingen moet opnemen. Als main
erg actief is, kan dit de geschiedenis van je functie-branch behoorlijk vervuilen. Hoewel het mogelijk is om dit issue te verhelpen met geavanceerde git log
-opties, kan dit het voor andere ontwikkelaars moeilijk maken om de geschiedenis van het project te begrijpen.
The rebase option
Als alternatief voor samenvoegen kun je een rebase uitvoeren voor de functie
-branch op de main
-branch met behulp van de volgende opdrachten:
git checkout feature
git rebase main
Dit verplaatst de hele functie
-branch om te beginnen naar de tip van de main
-branch, waarbij alle nieuwe commits in main
worden opgenomen. Maar in plaats van een merge-commit te gebruiken, herschrijft een rebase de projectgeschiedenis door gloednieuwe commits te maken voor elke commit in de oorspronkelijke branch.
Het grote voordeel van rebasing is dat je een veel schonere projectgeschiedenis krijgt. Ten eerste elimineert het proces de onnodige merge-commits die door git merge
worden vereist. Ten tweede resulteert een rebase, zoals je in het bovenstaande diagram kunt zien, in een perfect lineaire projectgeschiedenis: je kunt de tip van functie
helemaal tot aan het begin van het project volgen zonder enige vertakkingen. Dit maakt het gemakkelijker om door je project te navigeren met opdrachten zoals git log
, git bisect
en gitk
.
Maar er zijn twee afwegingen voor deze ongerepte commit-geschiedenis: veiligheid en traceerbaarheid. Als je de Gouden regel van rebasing niet volgt, kan het herschrijven van projectgeschiedenis mogelijk catastrofaal zijn voor je samenwerkingsworkflow. En, hoewel minder belangrijk: rebasen verliest de context die wordt geboden door een merge-commit. Je kunt niet zien wanneer upstream wijzigingen in de functie zijn opgenomen.
Interactive rebasing
Interactief rebasen geeft je de mogelijkheid om commits te wijzigen wanneer ze naar de nieuwe branch worden verplaatst. Dit is nog krachtiger dan een geautomatiseerde rebase, omdat het proces volledige controle biedt over de commit-geschiedenis van de branch. Meestal wordt deze optie gebruikt om een rommelige geschiedenis op te schonen voordat een functie-branch in main
wordt samengevoegd.
Om een interactieve rebasing-sessie te starten, geef je de optie i
door aan de git rebase
-opdracht:
git checkout feature
git rebase -i main
Hiermee wordt een teksteditor geopend met alle commits die op het punt staan te worden verplaatst:
pick 33d5b7a Message for commit #1
pick 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
Deze lijst bepaalt precies hoe de branch eruit zal zien nadat de rebase is uitgevoerd. Door de opdracht pick
te wijzigen en/of de items opnieuw te ordenen, kun je de geschiedenis van de branch laten lijken op wat u maar wilt. Als de 2e commit bijvoorbeeld een klein probleem oplost in de 1e commit, kun je ze samenvoegen tot een enkele commit met de opdracht fixup
:
pick 33d5b7a Message for commit #1
fixup 9480b3d Message for commit #2
pick 5c67e61 Message for commit #3
Wanneer je het bestand opslaat en sluit, voert Git de rebase uit volgens je instructies. Dat resulteert in een projectgeschiedenis die er als volgt uitziet:
Het elimineren van onbeduidende commits zoals deze maakt de geschiedenis van je functie veel begrijpelijker. Dit is iets dat git merge
gewoon niet kan doen.
The golden rule of rebasing
Als je eenmaal begrijpt wat rebasing is, is het vooral belangrijk om te leren wanneer je de functie niet moet gebruiken. De gouden regel van git rebase
is om deze nooit te gebruiken voor openbare branches.
Denk bijvoorbeeld na over wat er zou gebeuren als je een rebase voor main
zou uitvoeren op je functie
-branch:
De rebase verplaatst alle commits in main
naar de tip van de functie
. Het probleem is dat dit alleen in jouw repository gebeurde. Alle andere ontwikkelaars werken nog steeds met de originele main
. Omdat de rebase in gloednieuwe commits resulteert, zal Git denken dat de geschiedenis van je main
-branch is afgeleid van die van iedereen.
De enige manier om de twee main
-branches te synchroniseren, is door ze weer samen te voegen. Dat resulteert in een extra merge-commit en twee reeksen commits met dezelfde wijzigingen (de originele en die van je branch waarvoor de rebase werd uitgevoerd). Het is onnodig te zeggen dat dit een zeer verwarrende situatie is.
Stel daarom jezelf altijd de volgende vraag voordat je git rebase
uitvoert: „Kijkt iemand anders naar deze branch?” Als het antwoord 'ja' is, kun je maar beter je handen van het toetsenbord halen en nadenken over een niet-destructieve manier om je wijzigingen aan te brengen (bijv. de opdracht git revert
). Anders ben je veilig om de geschiedenis zoveel te herschrijven als je wilt.
Force-pushing
Als je probeert de rebased main
-branch terug te pushen naar een externe repository, zal Git dit voorkomen vanwege een conflict met de externe main
-branch. Je kunt de push echter afdwingen door markering --force
door te geven, zoals hieronder:
# Be very careful with this command! git push --force
Dit overschrijft de externe main
-branch, zodat deze overeenkomt met de rebased branch uit je repository. Dat werkt nogal verwarrend voor de rest van je team. Wees dus heel voorzichtig en gebruik deze opdracht alleen als je precies weet wat je doet.
Een van de enige keren waarop je geforceerd pushen moet gebruiken, is wanneer je een lokale opschoning hebt uitgevoerd nadat je een private functie-branch naar een externe repository hebt gepusht (bijvoorbeeld omwille van een back-up). Vergelijk het met een situatie waarin je zegt: „Oeps, ik wilde die originele versie van de functie-branch niet echt pushen. Neem in plaats daarvan de huidige.” Nogmaals: het is belangrijk dat niemand aan de commits van de originele versie van de functie-branch werkt.
Workflow walkthrough
Rebasing kan net zo veel of weinig in je bestaande Git-workflow worden opgenomen als je team prettig vindt. In deze sectie bekijken we de voordelen die rebasing kan bieden in de verschillende stadia van de ontwikkeling van een functie.
De eerste stap in elke workflow die gebruikmaakt van git rebase
bestaat uit het maken van een speciale branch voor elke functie. Dit geeft je de nodige branch-structuur om veilig gebruik te maken van rebasing:
Local cleanup
Een van de beste manieren om rebasing in je workflow op te nemen, is door lokale, openstaande functies op te schonen. Door periodiek een interactieve rebase uit te voeren, kun je ervoor zorgen dat elke commit in je functie doelgericht en zinvol is. Hiermee kun je je code schrijven zonder dat je je zorgen hoeft te maken over het opsplitsen in op zichzelf staande commits. Je kunt ze namelijk daarna herstellen.
Wanneer je git rebase
aanroept, heb je twee opties voor de nieuwe basis: de bovenliggende branch van het object (bijv. main
) of een eerdere commit in je object. We zagen in het gedeelte Interactieve rebase een voorbeeld van de eerste optie. De laatste optie is prettig als je alleen de laatste paar commits hoeft te repareren. De volgende opdracht begint bijvoorbeeld een interactieve rebase van alleen de laatste 3 commits.
git checkout feature git rebase -i HEAD~3
Door HEAD~3
als de nieuwe basis op te geven, verplaats je de branch niet echt. Je herschrijft gewoon interactief de 3 commits die daarop volgen. Merk op dat dit geen upstreamwijzigingen in de functie
-branch zal bevatten.
Als je het hele object met deze methode opnieuw wilt schrijven, kan de opdracht git merge-base
handig zijn om de oorspronkelijke basis van de functie
-branch te vinden. Het volgende retourneert de commit-ID van de originele basis, die je vervolgens kunt doorgeven aan git rebase
:
git merge-base feature main
Dit gebruik van interactieve rebasing is een geweldige manier om git rebase
in je workflow te introduceren, omdat het proces alleen lokale branches beïnvloedt. Het enige dat andere ontwikkelaars zullen zien, is je eindproduct – een schone, gemakkelijk te volgen functie-branch.
Maar nogmaals, dit werkt alleen voor private functie-branches. Als je met andere ontwikkelaars samenwerkt via dezelfde functie-branch, is die branch openbaar en mag je de geschiedenis ervan niet opnieuw schrijven.
Er is geen git merge
-alternatief voor het opschonen van lokale commits met een interactieve rebase.
Incorporating upstream changes into a feature
In de sectie Conceptueel overzicht zagen we hoe een functie-branch upstream wijzigingen van main
kan opnemen met behulp van git merge
of git rebase
. Samenvoegen is een veilige optie die de hele geschiedenis van je repository behoudt, maar rebase zorgt voor een lineaire geschiedenis door je functie-branch naar de tip van main
te verplaatsen.
Dit gebruik van git rebase
is vergelijkbaar met een lokale opschoning (en kan tegelijkertijd worden uitgevoerd), maar het proces bevat de upstream commits van main
.
Houd er rekening mee dat het volkomen legaal is om een rebase uit te voeren op een externe branch in plaats van main
. Dit kan gebeuren wanneer je aan dezelfde functie samenwerkt met een andere ontwikkelaar en je diens wijzigingen in je repository moet opnemen.
Stel dat jij en een andere ontwikkelaar, John, commits hebben toegevoegd aan de functie
-branch. Je repository kan er dan als volgt uitzien nadat je de externe functie
-branch uit Johns repository hebt opgehaald:
Je kunt deze vertakking op exact dezelfde manier oplossen als waarop je upstream wijzigingen van main
integreert: voeg je lokale functie
samen met john/feature
of zet je lokale functie
opnieuw op de tip van john/feature
.
Merk op dat deze rebase niet in strijd is met de Gouden regel van rebasing omdat alleen je lokale functie
-commits worden verplaatst – alles ervoor is onaangeroerd. Dit is hetzelfde als zeggen: „Voeg mijn wijzigingen toe aan wat John al heeft gedaan.” In de meeste gevallen is dit intuïtiever dan synchroniseren met de externe branch via een merge-commit.
Standaard voert de opdracht git pull
een merge uit, maar je kunt afdwingen dat de externe branch wordt geïntegreerd met een rebase door de optie --rebase
door te geven.
Reviewing a feature with a pull request
Als je pull requests gebruikt als onderdeel van je codebeoordelingsproces, moet je het gebruik van git rebase
vermijden nadat je het pull request hebt gemaakt. Zodra je het pull request maakt, kijken andere ontwikkelaars naar je commits. Dat betekent dat het een openbare branch is. Als je de geschiedenis opnieuw schrijft, wordt het voor Git en je teamgenoten onmogelijk om vervolgcommits bij te houden die aan de functie zijn toegevoegd.
Alle wijzigingen van andere ontwikkelaars moeten worden opgenomen met git merge
in plaats van git rebase
.
Daarom is het meestal een goed idee om je code op te schonen met een interactieve rebase voordat je je pull-verzoek indient.
Integrating an approved feature
Nadat een functie door je team is goedgekeurd, heb je de mogelijkheid om een rebase van de functie uit te voeren op de tip van de main
-branch voordat je git merge
gebruikt om de functie in de hoofdcodebasis te integreren.
Dit is een vergelijkbare situatie als het opnemen van upstream wijzigingen in een functie-branch, maar omdat je commits niet opnieuw mag schrijven in de main
-branch, moet je uiteindelijk git merge
gebruiken om de functie te integreren. Door echter vóór de samenvoeging een rebase uit te voeren, ben je ervan verzekerd dat de samenvoeging snel wordt doorgestuurd. Dat resulteert in een perfect lineaire geschiedenis. Dit geeft je ook de kans om eventuele vervolgcommits te squashen die tijdens een pull-verzoek zijn toegevoegd.
Als je je niet helemaal op je gemak voelt met git rebase
, kun je de rebase altijd uitvoeren in een tijdelijke branch. Op die manier kun je de originele branch bekijk en het opnieuw proberen als je per ongeluk de geschiedenis van je functie verknoeit. Bijvoorbeeld:
git checkout feature
git checkout -b temporary-branch
git rebase -i main
# [Clean up the history]
git checkout main
git merge temporary-branch
Samenvatting
Dat is alles wat je echt moet weten om te beginnen met het rebasen van je branches. Als je liever een schone, lineaire geschiedenis zonder onnodige merge-commits wilt, moet je git rebase
gebruiken in plaats van git merge
wanneer je wijzigingen van een andere branch integreert.
Maar als je de volledige geschiedenis van je project wilt behouden en het risico van het herschrijven van openbare commits wilt vermijden, kun je beter voor git merge
blijven kiezen. Beide opties zijn prima, maar nu heb je tenminste de mogelijkheid om de voordelen van git rebase
te benutten.
Deel dit artikel
Volgend onderwerp
Aanbevolen artikelen
Bookmark deze resources voor meer informatie over soorten DevOps-teams of voor voortdurende updates over DevOps bij Atlassian.