Git --everything-is-local
Chapters ▾ 2nd Edition

3.6 Branchen in Git - Rebasen

Rebasen

In Git zijn er twee hoofdmanieren om wijzigingen te integreren van de ene branch in een andere: de merge en de rebase. In deze paragraaf ga je leren wat rebasen is, hoe je dat moet doen, waarom het een zeer bijzonder stukje gereedschap is en in welke gevallen je het niet wilt gebruiken.

De simpele rebase

Als je het eerdere voorbeeld van “Eenvoudig samenvoegen (mergen)” erop terugslaat, dan zul je zien dat je werk is uiteengelopen en dat je commits hebt gedaan op de twee verschillende branches.

Eenvoudige uiteengelopen historie.
Eenvoudige uiteengelopen historie

De simpelste manier om de branches te integreren, zoals we al hebben besproken, is het merge commando. Het voert een drieweg-merge uit tussen de twee laatste snapshots van de branches (C3 en C4), en de meest recente gezamenlijke voorouder van die twee (C2), en maakt een nieuw snapshot (en commit).

Mergen om uiteengelopen werk historie te integreren.
Mergen om uiteengelopen werk historie te integreren

Maar, er is nog een manier: je kunt de patch van de wijziging die werd geïntroduceerd in C4 pakken en die opnieuw toepassen op C3. In Git, wordt dit rebasen genoemd. Met het rebase commando kan je alle wijzigingen pakken die zijn gecommit op de ene branch, en ze opnieuw afspelen op een andere.

In dit voorbeeld zou je het volgende uitvoeren:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

Het gebeurt door naar de gezamenlijke voorouder van de twee branches te gaan (degene waar je op zit en degene waar je op rebaset), de diff te nemen die geïntroduceerd is voor elke losse commit op de branch waar je op zit, die diffs in tijdelijke bestanden te bewaren, de huidige branch terug te zetten naar dezelfde commit als de branch waar je op rebaset, en uiteindelijk elke diff om de beurt te applyen.

De wijziging gemaakt in `C4` rebasen naar `C3`.
De wijziging gemaakt in C4 rebasen naar C3

En nu kan je teruggaan naar de master branch en een fast-forward merge uitvoeren.

$ git checkout master
$ git merge experiment
De master branch fast-forwarden.
De master branch fast-forwarden

Nu is het snapshot waar C4' naar wijst precies dezelfde als degene waar C5 naar wees in het merge voorbeeld. Er zit geen verschil in het eindresultaat van de integratie, maar rebasen zorgt voor een duidelijkere historie. Als je de log van een branch die gerebased is bekijkt, ziet het eruit als een lineaire historie: het lijkt alsof al het werk in serie heeft plaatsgevonden, zelfs wanneer het in werkelijkheid parallel eraan gedaan is.

Vaak zal je dit doen om er zeker van te zijn dat je commits netjes toegepast kunnen worden op een remote branch - misschien in een project waar je aan probeert bij te dragen, maar welke je niet beheert. In dit geval zou je het werk in een branch uitvoeren en dan je werk rebasen op origin/master als je klaar ben om je patches in te sturen naar het hoofdproject. Op die manier hoeft de beheerder geen integratiewerk te doen - gewoon een fast-forward of een schone apply.

Merk op dat de snapshot waar de laatste commit op het eind naar wijst, of het de laatste van de gerebasede commits voor een rebase is of de laatste merge commit na een merge, detzelfde snapshot is - alleen de historie is verschillend. Rebasen speelt veranderingen van een werklijn opnieuw af op een andere, in de volgorde waarin ze gemaakt zijn, terwijl mergen de eindresultaten pakt en die samenvoegt.

Interessantere rebases

Je kunt je rebase ook opnieuw laten afspelen op iets anders dan de rebase doel branch. Pak een historie zoals in Figure 3-31, bijvoorbeeld. Je hebt een topic branch afgesplitst (server) om wat server-kant functionaliteit toe te voegen aan je project en toen een keer gecommit. Daarna heb je daar vanaf gebranched om de client-kant wijzigingen te doen (client) en een paar keer gecommit. Als laatste, ben je teruggegaan naar je server branch en hebt nog een paar commits gedaan.

Een historie met een topic branch vanaf een andere topic branch.
Een historie met een topic branch vanaf een andere topic branch

Stel nu, je besluit dat je de client-kant wijzigingen wilt mergen in je hoofdlijn voor een release, maar je wilt de server-kant wijzigingen nog vasthouden totdat het verder getest is. Je kunt de wijzigingen van client pakken, die nog niet op server zitten (C8 en C9) en die opnieuw afspelen op je master-branch door de --onto optie te gebruiken van git rebase:

$ git rebase --onto master server client

Dit zegt in feite, “Check de client-branch uit, verzamel de patches van de gezamenlijke voorouder van de client en de server branches, en speel die opnieuw af op master.” Het is een beetje ingewikkeld, maar het resultaat, getoond in Figuur 3-32, is erg prettig.

Een topic branch rebasen vanaf een andere topic branch.
Een topic branch rebasen vanaf een andere topic branch

Nu kun je een fast-forward doen van je master branch (zie Figure 3-33):

$ git checkout master
$ git merge client
Je master branch fast-forwarden om de client branch wijzigingen mee te nemen.
Je master branch fast-forwarden om de client branch wijzigingen mee te nemen

Stel dat je besluit om de server branch ook te pullen. Je kunt de server branch rebasen op de master branch zonder het eerst te hoeven uitchecken door git rebase [basisbranch] [topicbranch] uit te voeren - wat de topic branch voor je uitcheckt (in dit geval, server) en het opnieuw afspeelt op de basis branch (master):

$ git rebase master server

Dit speelt het server werk opnieuw af op het master werk, zoals getoond in Figure 3-34.

Je server branch op je master branch rebasen.
Je server branch op je master branch rebasen

Daarna kan je de basis branch (master) fast-forwarden:

$ git checkout master
$ git merge server

Je kunt de client en server branches verwijderen, omdat al het werk geïntegreerd is en je ze niet meer nodig hebt, en de historie voor het hele proces ziet eruit zoals in Figure 3-35:

$ git branch -d client
$ git branch -d server
Uiteindelijke commit historie.
Uiteindelijke commit historie

De gevaren van rebasen

Ahh, maar de zegeningen van rebasen zijn niet geheel zonder nadelen, samengevat in één enkele regel:

Rebase geen commits die buiten je lokale repository bekend zijn.

Als je die richtlijn volgt, kan je weinig gebeuren. Als je dat niet doet, zullen mensen je gaan haten en je zult door vrienden en familie uitgehoond worden.

Als je spullen rebaset, zet je bestaande commits buitenspel en maak je nieuwe aan die vergelijkbaar zijn maar anders. Wanneer je commits ergens pusht en andere pullen deze en baseren daar werk op, en vervolgens herschrijf je die commits met git rebase en pusht deze weer, dan zullen je medewerkers hun werk opnieuw moeten mergen en zal het allemaal erg vervelend worden als je hun werk probeert te pullen in het jouwe.

Laten we eens kijken naar een voorbeeld hoe werk rebasen dat je publiek gemaakt hebt problemen kan veroorzaken. Stel dat je van een centrale server clonet en dan daar wat werk aan doet. Je commit-historie ziet eruit als volgt:

Cloon een repository, en baseer er wat werk op.
Cloon een repository, en baseer er wat werk op

Nu doet iemand anders wat meer werk wat een merge bevat, en pusht dat werk naar de centrale server. Je fetcht dat en merget de nieuwe remote branch in jouw werk, zodat je historie eruitziet zoals dit:

Fetch meer commits, en merge ze in jouw werk.
Fetch meer commits, en merge ze in jouw werk

Daarna, beslist de persoon die het werk gepusht heeft om erop terug te komen en in plaats daarvan zijn werk te gaan rebasen; hij voert een git push --force uit om de historie op de server te herschrijven. Je pullt daarna van die server, waarbij je de nieuwe commits binnen krijgt.

Iemand pusht gerebasede commits, daarbij commits buitenspel zettend waar jij werk op gebaseerd hebt.
Iemand pusht gerebasede commits, daarbij commits buitenspel zettend waar jij werk op gebaseerd hebt

Nu zitten jullie beiden met een probleem. Als jij een git pull doet, ga je een commit merge maken waar beide tijdslijnen in zitten, en je repository zal er zo uit zien:

Je merget hetzelfde werk opnieuw in een nieuwe merge commit.
Je merget hetzelfde werk opnieuw in een nieuwe merge commit

Als je een git log uitvoert als je historie er zo uitziet, zie je twee commits die dezelfde auteur, datum en bericht hebben, wat verwarrend is. Daarnaast, als je deze historie naar de server terug pusht, zal je al deze gerebasede commits opnieuw herintroduceren op centrale server, wat weer andere mensen zou kunnen verwarren. Het is redelijk veilig om aan te nemen dat de andere ontwikkelaar C4 en C6 niet in de historie wil, dat is juist de reden waarom ze heeft gerebased.

Rebaset spullen rebasen

Mocht je in zo’n situatie belanden, heeft Git nog wat tovertrucs in petto die je kunnen helpen. Als iemand of een aantal mensen in jouw team met pushes wijzigingen hebben geforceert die werk overschrijven waar jij je werk op gebaseerd hebt, is het jouw uitdaging om uit te vinden wat jouw werk is en wat zij herschreven hebben.

Het komt zo uit dat naast de SHA-1 checksum van de commit, Git ook een checksum berekent die enkel is gebaseerd op de patch die is geïntroduceerd met de commit. Dit heet een ``patch-id"`.

Als je werk pullt die was herschreven en deze rebased op de nieuwe commits van je partner, kan Git vaak succesvol uitvinden wat specifiek van jou is en deze opnieuw afspelen op de nieuwe branch.

Bijvoorbeeld in het vorige scenario, als in plaats van een merge te doen we in een situatie zijn die beschreven is in Figure 3-38 en we git rebase teamone/master aanroepen, zal Git:

  • Bepalen welk werk uniek is in onze branch (C2, C3, C4, C6, C7)

  • Bepalen welke geen merge commits zijn (C2, C3, C4)

  • Bepalen welke nog niet herschreven zijn in de doel-branch (alleen C2 en C3, omdat C4 dezelfde patch is als C4')

  • Deze commits op teamone/master afspelen

Dus in plaats van het resultaat dat we zien in Figure 3-39, zouden we eindigen met iets wat meer lijkt op Figure 3-40.

Rebase op een force-pushed rebase werk.
Rebase op een force-pushed rebase werk.

Dit werkt alleen als de door je partner gemaakte C4 en C4’ vrijwel dezelfde patch zijn. Anders kan de rebase niet achterhalen dat het een duplicaat is en zal dan een andere C4-achtige patch toevoegen (die waarschijnlijk niet schoon kan worden toegepast, omdat ongeveer dezelfde wijzigingen al daar staan).

Je kunt dit versimpelen door een git pull --rebase in plaats van een gewone git pull te draaien. Of in dit geval kan je handmatig een git fetch gevolgd door een git rebase teamone/master uitvoeren.

Als je git pull gebruikt en --rebase de standaard maken, kan je de pull.rebase configuratie waarde zetten op git config --global pull.rebase true.

Als je rebasen behandelt als een manier om op te schonen en met de commits te werken voordat je ze pusht, en als je alleen commits rebased die nooit eerder publiekelijk beschikbaar waren, is er geen probleem. Als je commits rebased dat al publiekelijk gepushed zijn, en mensen kunnen hun werk gebaseerd hebben op die commits, dan heb je de basis gelegd voor wat frustrerende problemen, en de hoon van je teamgenoten.

Als jij of een partner het nodig vinden op een gegeven moment, verzeker je ervan dat iedereen weet dat ze een git pull --rebase moeten draaien om de pijn te verzachten nadat dit gebeurt is.

Rebase vs. Merge

Nu we rebasen en mergen in actie hebben laten zien, kan je je afvragen welk van de twee beter is. Voordat we die vraag kunnen beantwoorden, laten we eerst een stapje terug nemen en bespreken wat historie eigenlijk inhoudt.

Een standpunt is dat de commit historie van jouw repository een vastlegging is van wat daadwerkelijk gebeurd is. Het is een historisch document, op zichzelf waardevol, waarmee niet mag worden gerommeld. Vanuit dit gezichtspunt, is het wijzigen van de commit historie bijna vloeken in de kerk; je bent aan het liegen over wat er werkelijk gebeurd is. Wat hindert het dat er een slorige reeks merge commits waren? Dat is hoe het gebeurd is, en de repository moet dat bewaren voor het nageslacht.

Een ander standpunt is dat de commit historiie het verhaal is hoe jouw project tot stand is gekomen. Je puliceert ook niet het eerste manuscript van een boek, en de handleiding hoe je software te onderhouden verdient zorgvuldig samenstellen. Dit is het kamp dat gereedschappen als rebase en filter-branch gebruikt om het verhaal te vertellen dat het beste is voor toekomstige lezers.

Nu, terug naar de vraag of mergen of rebasen beter is: hopelijk snap je nu dat het niet zo eenvoudig ligt. Git is een krachtig instrument, en stelt je in staat om veel dingen te doen met en middels je historie, maar elk team en elk project is anders. Nu je weet hoe beide werken, is het aan jou om te besluiten welke het beste is voor jouw specifieke situatie.

Om het beste van beide aanpakken te krijgen is het over het algemeen het beste om lokale wijzigingen die je nog niet gedeeld hebt te rebasen voordat je ze pusht zodat je verhaal het schoonste blijft, maar nooit iets te rebasen wat je elders gepusht hebt.