This is an in-progress translation.
To help translate the book, please fork the book at GitHub and push your contributions.

Geschiedenis herschrijven

Vaak zul je, als je met Git werkt, je commit geschiedenis om een of andere reden willen aanpassen. Eén van de mooie dingen van Git is dat het je toelaat om beslissingen op het laatste moment te kunnen maken. Je kunt bepalen welke bestanden in welke commits gaan vlak voordat je commit op het staging area, je kunt beslissen dat je niet aan iets bedoelde te werken met het stash commando en je kunt commits herschrijven ook al zijn ze reeds gebeurd, zodat het lijkt alsof ze op een andere manier gebeurd zijn. Dit kan bijvoorbeeld de volgorde van de commits zijn, berichten of bestanden wijzigen, commits samenvoegen of opsplitsen, of complete commits weghalen – dat alles voordat je je werk met anderen deelt.

In deze sectie zul je leren hoe je deze handige taken doet, zodat je je commit geschiedenis er uit kunt laten zien zoals jij dat wilt, voordat je hem met anderen deelt.

De laatste commit veranderen

De laatste commit veranderen is waarschijnlijk de meest voorkomende geschiedenis wijziging die je wilt doen. Vaak wil je twee basale dingen aan je laatste commit wijzigen: het commit bericht, of het snapshot dat je zojuist opgeslagen hebt wijzigen door het toevoegen, wijzigen of weghalen van bestanden.

Als je alleen je laatste commit bericht wilt wijzigen, dan is dat heel eenvoudig:

$ git commit --amend

Dat plaatst je in je teksteditor, met je laatste commit bericht erin, klaar voor jou om je bericht te wijzigen. Als je de editor opslaat en sluit, dan schrijft de editor een nieuwe commit met dat bericht en maakt dat je laatste commit.

Als je hebt gecommit en je wilt het snapshot dat je gecommit hebt wijzigen, door het toevoegen of wijzigen van bestanden, misschien omdat je vergeten was een nieuw bestand toe te voegen toen je committe, werkt het proces ongeveer op dezelfde manier. Je staged de wijzigingen die je wilt door een bestand te wijzigen en git add er op uit te voeren, of git rm op een gevolgd bestand, en de daaropvolgende git commit --amend pakt je huidige staging area en maakt dat het snapshot voor de nieuwe commit.

Je moet oppassen met deze techniek, omdat het amenden de SHA-1 van de commit wijzigt. Het is vergelijkbaar met een kleine rebase – niet je laatste commit wijzigen als je die al gepushed hebt.

Meerdere commit berichten wijzigen

Om een commit te wijzigen die verder terug in je geschiedenis zit, moet je naar complexere tools gaan. Git heeft geen geschiedenis-wijzig tool, maar je kunt het rebase tool gebruiken om een serie commits op de HEAD waarvan ze origineel gebaseerd waren te rebasen, in plaats van ze naar een andere te verhuizen. Met het interactieve rebase tool, kun je dan na iedere commit die je wilt wijzigen stoppen en het bericht wijzigen, bestanden toevoegen, of doen wat je ook maar wilt. Je kunt rebase interactief uitvoeren door de -i optie aan git rebase toe te voegen. Je moet aangeven hoe ver terug je commits wilt herschrijven door het commando te vertellen op welke commit het moet rebasen.

Bijvoorbeeld, als je de laatste drie commit berichten wilt veranderen, of een van de commit berichten in die groep, dan geef je de ouder van de laatste commit die je wilt wijzigen mee als argument aan git rebase -i, wat HEAD~2^ of HEAD~3 is. Het kan makkelijker zijn om de ~3 te onthouden, omdat je de laatste drie commits probeert te wijzigen; maar houd in gedachten dat je eigenlijk vier commits terug aangeeft; de ouder van de laatste commit die je wilt veranderen:

$ git rebase -i HEAD~3

Onthoud ook dat dit een rebase commando is – iedere commit in de serie HEAD~3..HEAD zal worden herschreven, of je het bericht wijzigt of niet. Voeg geen commit toe die je al naar een centrale server gepushed hebt – als je dit doet breng je andere gebruikers in de war door ze een alternatieve versie van dezelfde wijziging te geven.

Dit commando uitvoeren geeft je een lijst met commits in je tekst editor die er ongeveer zo uit ziet:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

Het is belangrijk om te zien dat deze commits in de omgekeerde volgorde getoond worden dan wanneer je ze normaliter ziet als je het log commando gebruikt. Als je een log uitvoert, zie je zoiets als dit:

$ git log --pretty=format:"%h %s" HEAD~3..HEAD
a5f4a0d added cat-file
310154e updated README formatting and added blame
f7f3f6d changed my name a bit

Zie de omgekeerde volgorde. De interactieve rebase geeft je een script dat het gaat uitvoeren. Het zal beginnen met de commit die je specificeert op de commando regel (HEAD~3), en de wijzigingen in ieder van deze commits opnieuw afspelen van boven naar beneden. Het toont de oudste aan de bovenkant, in plaats van de nieuwste, omdat dat de eerste is die het zal afspelen.

Je moet het script wijzigen zodat het stopt bij de commit die je wilt wijzigen. Om dat te doen moet je het woord pick veranderen in het woord edit voor ieder van de commits waarbij je het script wilt laten stoppen. Bijvoorbeeld, om alleen het derde commit bericht te wijzigen, verander je het bestand zodat het er zo uitziet:

edit f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

Als je de editor opslaat en sluit, plaatst Git je terug in de laatste commit van die lijst en zet je op de commando regel met de volgende boodschap:

$ git rebase -i HEAD~3
Stopped at 7482e0d... updated the gemspec to hopefully work better
You can amend the commit now, with

       git commit --amend

Once you’re satisfied with your changes, run

       git rebase --continue

Deze instructies vertellen je precies wat je moet doen. Type

$ git commit --amend

Wijzig het commit bericht, en verlaat de editor. Voer daarna dit uit

$ git rebase --continue

Dit commando zal de andere twee commits automatisch toepassen, en dan ben je klaar. Als je pick in edit veranderd op meerdere regels, dan kun je deze stappen herhalen voor iedere commit die je in edit veranderd hebt. Iedere keer zal Git stoppen, je de commit laten amenden, en verder gaan waar je gewijzigd bent.

Commits rangschikken

Je kunt een interactieve rebase ook gebruiken om commits te rangschikken of volledig te verwijderen. Als je de “added cat-file” commit wilt verwijderen en de volgorde waarin de andere twee commits zijn geïntroduceerd wilt veranderen, dan kun je het rebase script van dit

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

veranderen in dit:

pick 310154e updated README formatting and added blame
pick f7f3f6d changed my name a bit

Als je de editor opslaat en sluit, zal Git je branch terugzetten naar de ouder van deze commits, eerst 310154e en dan f7f3f6d toepassen, en dan stoppen. Effectief verander je de volgorde van die commits en verwijder je de “added cat-file” commit in zijn geheel.

Een commit squashen

Het is ook mogelijk een serie commits te pakken en ze in één enkele commit te squashen met het interactieve rebase tool. Het script stopt behulpzame instructies in het rebase bericht:

#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

Als je, in plaats van “pick” of “edit”, “squash” specificeert zal Git zowel die verandering als de verandering die er direct aan vooraf gaat toepassen, en je dwingen om de merge berichten samen te voegen. Dus als je een enkele commit van deze drie commits wil maken, laat je het script er zo uit zien:

pick f7f3f6d changed my name a bit
squash 310154e updated README formatting and added blame
squash a5f4a0d added cat-file

Als je de editor opslaat en sluit, zal Git alledrie de veranderingen toepassen en je terug in de editor brengen om de drie commit berichten samen te voegen:

# This is a combination of 3 commits.
# The first commit's message is:
changed my name a bit

# This is the 2nd commit message:

updated README formatting and added blame

# This is the 3rd commit message:

added cat-file

Als je dat opslaat, heb je een enkele commit die de veranderingen van alledrie de vorige commits introduceert.

Een commit splitsen

Een commit opsplitsen zal een commit ongedaan maken, en dan zo vaak als het aantal commits waar je mee wilt eindigen gedeeltelijk stagen en committen. Bijvoorbeeld, stel dat je de middelste van je drie commits wilt splitsen. In plaats van “updated README formatting and added blame”, wil je het splitsen in twee commits: “updated README formatting” als eerste, en “added blame” als tweede. Je kunt dat doen in het rebase -i script door de instructie van de commit die je wilt splitsen te veranderen in “edit”:

pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

Als het script je dan op de commando regel zet, reset je die commit, neemt de wijzigingen die zijn gereset, en maakt daar meerdere commits van.Als je de editor opslaat en sluit, zal Git teruggaan naar de ouder van de eerste commit in je lijst, de eerste commit toepassen (f7f3f6d), de tweede commit toepassen (310154e), en je op de console plaatsen. Dan kun je een gemengde reset van die commit doen met git reset HEAD^, wat effectief die commit ongedaan maakt en de gewijzigde bestanden unstaged laat. Nu kun je bestanden stagen en committen totdat je meerdere commits hebt, en git rebase --continue uitvoeren zodra je klaar bent:

$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue

Git zal de laatste commit (a5f4a0d) in het script toepassen, en je geschiedenis zal er zo uitzien:

$ git log -4 --pretty=format:"%h %s"
1c002dd added cat-file
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit

Nogmaals, dit veranderd alle SHA’s van alle commits in je lijst, dus zorg er voor dat er geen commit in die lijst zit die je al naar een gedeeld repository gepushed hebt.

De nucleaire optie: filter-branch

Er is nog een geschiedenis-herschrijvende optie, die je kunt gebruiken als je een groter aantal commits moet herschrijven in een scriptbare manier – bijvoorbeeld, het globaal veranderen van je e-mail adres of een bestand uit iedere commit verwijderen. Het commando heet filter-branch, en het kan grote gedeelten van je geschiedenis herschrijven, dus je moet het niet gebruiken tenzij je project nog niet publiekelijk is gemaakt en andere mensen nog geen werk hebben gebaseerd op jouw commits, die je op het punt staat te herschrijven. Maar het kan heel handig zijn. Je zult een paar gebruikelijke toepassingen leren zodat je een idee kunt krijgen van de mogelijkheden.

Een bestand uit iedere commit verwijderen

Dit gebeurd vrij vaak. Iemand voegt per ongeluk een enorm binair bestand toe met een gedachteloze git add ., en je wilt het overal weghalen. Misschien heb je per ongeluk een bestand toegevoegd dat een wachtwoord bevat, en wil je je project open source maken. filter-branch is het tool dat je waarschijnlijk wil gebruiken om je hele geschiedenis schoon te vegen. Om een bestand genaamd passwords.txt uit je hele geschiedenis weg te halen, kun je de --tree-filter optie toevoegen aan filter-branch:

$ git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
Rewrite 6b9b3cf04e7c5686a9cb838c3f36a8cb6a0fc2bd (21/21)
Ref 'refs/heads/master' was rewritten

De --tree-filter optie voert het gegeven commando uit na iedere checkout van het project, en commit de resultaten weer. In dit geval, verwijder je een bestand genaamd passwords.txt van iedere snapshot, of het bestaat of niet. Als je alle per ongeluk toegevoegde editor backup bestanden wilt verwijderen, kun je zoiets als dit uitvoeren git filter-branch --tree-filter 'rm -f *~' HEAD.

Je kunt Git bomen en commits zien herschrijven en de branch wijzer aan het einde zien verplaatsen. Het is over het algemeen een goed idee om dit in een test branch te doen, en dan je master branch te hard-resetten nadat je gecontroleerd hebt dat de uitkomst echt zo is als je wil. Om filter-branch op al je branches uit te voeren, kun je --all aan het commando meegeven.

Een subdirectory het nieuwe beginpunt maken

Stel dat je een import vanuit een ander versiebeheersysteem hebt gedaan, en subdirectories hebt die geen zin maken (trunk, tags enzovoort). Als je de trunk subdirectory het nieuwe beginpunt van het project wilt maken voor iedere commit, dan kan filter-branch je daar ook mee helpen:

$ git filter-branch --subdirectory-filter trunk HEAD
Rewrite 856f0bf61e41a27326cdae8f09fe708d679f596f (12/12)
Ref 'refs/heads/master' was rewritten

Nu is je nieuwe project wat het was in de trunk subdirectory. Git zal ook automatisch commits verwijderen, die geen effect hadden op de subdirectory.

E-mail adressen globaal veranderen

Een ander veel voorkomend geval is dat je vergeten bent om git config uit te voeren om je naam en e-mail adres in te stellen voordat je bent begonnen met werken, of misschien wil je een project op het werk open source maken en al je werk e-mail adressen veranderen naar je privé adres. In ieder geval kun je e-mail adressen in meerdere commits ook ineens veranderen met filter-branch. Je moet wel oppassen om alleen de e-mail adressen aan te passen, die van jou zijn, dus gebruik je --commit-filter:

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
        then
                GIT_AUTHOR_NAME="Scott Chacon";
                GIT_AUTHOR_EMAIL="schacon@example.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

Dit gaat door en herschrijft iedere commit zodat het jouw nieuwe adres bevat. Om dat commits de SHA-1 waarde van hun ouders bevatten, zal dit commando iedere commit SHA in jouw geschiedenis veranderen, niet alleen degenen die het passende e-mailadres bevatten.