-
1. Aan de slag
- 1.1 Over versiebeheer
- 1.2 Een kort historisch overzicht van Git
- 1.3 Wat is Git?
- 1.4 De commando-regel
- 1.5 Git installeren
- 1.6 Git klaarmaken voor eerste gebruik
- 1.7 Hulp krijgen
- 1.8 Samenvatting
-
2. Git Basics
-
3. Branchen in Git
- 3.1 Branches in vogelvlucht
- 3.2 Eenvoudig branchen en mergen
- 3.3 Branch-beheer
- 3.4 Branch workflows
- 3.5 Branches op afstand (Remote branches)
- 3.6 Rebasen
- 3.7 Samenvatting
-
4. Git op de server
- 4.1 De protocollen
- 4.2 Git op een server krijgen
- 4.3 Je publieke SSH sleutel genereren
- 4.4 De server opzetten
- 4.5 Git Daemon
- 4.6 Slimme HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Hosting oplossingen van derden
- 4.10 Samenvatting
-
5. Gedistribueerd Git
-
6. GitHub
-
7. Git Tools
- 7.1 Revisie Selectie
- 7.2 Interactief stagen
- 7.3 Stashen en opschonen
- 7.4 Je werk tekenen
- 7.5 Zoeken
- 7.6 Geschiedenis herschrijven
- 7.7 Reset ontrafeld
- 7.8 Mergen voor gevorderden
- 7.9 Rerere
- 7.10 Debuggen met Git
- 7.11 Submodules
- 7.12 Bundelen
- 7.13 Vervangen
- 7.14 Het opslaan van inloggegevens
- 7.15 Samenvatting
-
8. Git aanpassen
- 8.1 Git configuratie
- 8.2 Git attributen
- 8.3 Git Hooks
- 8.4 Een voorbeeld van Git-afgedwongen beleid
- 8.5 Samenvatting
-
9. Git en andere systemen
- 9.1 Git als een client
- 9.2 Migreren naar Git
- 9.3 Samenvatting
-
10. Git Binnenwerk
- 10.1 Binnenwerk en koetswerk (plumbing and porcelain)
- 10.2 Git objecten
- 10.3 Git Referenties
- 10.4 Packfiles
- 10.5 De Refspec
- 10.6 Uitwisseling protocollen
- 10.7 Onderhoud en gegevensherstel
- 10.8 Omgevingsvariabelen
- 10.9 Samenvatting
-
A1. Bijlage A: Git in andere omgevingen
- A1.1 Grafische interfaces
- A1.2 Git in Visual Studio
- A1.3 Git in Visual Studio Code
- A1.4 Git in Eclipse
- A1.5 Git in Sublime Text
- A1.6 Git in Bash
- A1.7 Git in Zsh
- A1.8 Git in PowerShell
- A1.9 Samenvatting
-
A2. Bijlage B: Git in je applicaties inbouwen
- A2.1 Commando-regel Git
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. Bijlage C: Git Commando’s
- A3.1 Setup en configuratie
- A3.2 Projecten ophalen en maken
- A3.3 Basic Snapshotten
- A3.4 Branchen en mergen
- A3.5 Projecten delen en bijwerken
- A3.6 Inspectie en vergelijking
- A3.7 Debuggen
- A3.8 Patchen
- A3.9 Email
- A3.10 Externe systemen
- A3.11 Beheer
- A3.12 Binnenwerk commando’s (plumbing commando’s)
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 mergen (samenvoegen) erop terugslaat, dan zul je zien dat je werk is uiteengelopen en dat je commits hebt gedaan op de twee verschillende branches.
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).
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 de branch experiment
uitchecken, en dan op de master
branch rebasen op de volgende wijze:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
Deze operatie 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 door 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 een voor een te applyen.
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
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 volgorderlijk is gebeurd, 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 Een historie met een topic branch vanaf een andere topic branch, 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.
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 in de client
-branch alsof deze direct afgeleid was van de master
-branch.'
Het is een beetje ingewikkeld, maar het resultaat is best wel gaaf.
Nu kun je een fast-forward doen van je master
-branch (zie Je master branch fast-forwarden om de client branch wijzigingen mee te nemen):
$ git checkout master
$ git merge client
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 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 Uiteindelijke commit historie:
$ git branch -d client
$ git branch -d server
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 repository bekend zijn, en waar anderen werk op gebaseerd hebben.
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:
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:
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.
Nu zitten jullie beiden in de penarie.
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:
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 geforceerd 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 Iemand pusht gerebasede commits, daarbij commits buitenspel zettend waar jij werk op gebaseerd hebt 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 Je merget hetzelfde werk opnieuw in een nieuwe merge commit, zouden we eindigen met iets wat meer lijkt op 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 wijzigingen ongeveer hetzelfde daar al 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 alleen maar commits rebaset die nooit buiten jouw computer bekend zijn, zou er geen vuiltje aan de lucht moeten zijn. Als je commits rebaset die zijn gepusht, maar niemand nog werk daarop heeft gebaseerd, is er ook geen probleem. Als je commits rebaset die al publiekelijk gepusht 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 gebeurd 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 historie 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.