Git
Chapters ▾ 2nd Edition

7.13 Git Tools - Vervangen

Vervangen

Git objecten kunnen niet worden gewijzigd, maar Git heeft een interessante manier om te doen alsof objecten in haar database worden vervangen met andere objecten.

Het replace commando laat je een object in Git opgeven en haar vertellen dat "elke keer als je dit ziet, doe alsof het dit andere ding is". Dit is vaak het nuttigst voor het vervangen van een commit in je historie met een andere.

Bijvoorbeeld, laten we zeggen dat je een enorme code historie hebt en je wilt je repository opsplitsen in een korte historie voor nieuwe ontwikkelaars en een veel langere en grotere historie voor mensen die geïnteresseerd zijn in het graven in gegevens (data mining). Je kunt de ene historie op de andere "enten" door de vroegste commit in de nieuwe lijn met de laatste commit van de oude lijn te `replace`n. Dit is prettig omdat het betekent dat je niet echt alle commits in de nieuwe historie hoeft te herschrijven, wat je normaalgesproken wel zou moeten doen om ze samen te voegen (omdat de voorouderschap de SHA-1’s beïnvloedt).

Laten we dat eens uitproberen. Laten we een bestaande repository nemen, en deze in twee repositories splitsen, een recente en een historische, en laten we dan kijken hoe we ze kunnen herschikken zonder de SHA-1 waarden van de recente repository te wijzigen met behulp van replace.

We zullen een eenvoudige repository met vijf simpele commits gebruiken:

$ git log --oneline
ef989d8 fifth commit
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

We willen deze opdelen in twee lijnen met historie. Een lijn gaat van commit een tot commit vier (first t.m. fourth) - dat zal de historische worden. De tweede lijn zal alleen commits vier en vijf (fourth en fifth) zijn - dat is dan de recente historie.

replace1

Nu, de historische historie maken is eenvoudig, we kunnen gewoon een branch in de geschiedenis zetten en dan die branch naar de master branch pushen van een nieuwe remote repository.

$ git branch history c6e1e95
$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit
replace2

Nu kunnen we de nieuwe history branch naar de master branch van onze nieuwe repository pushen:

$ git remote add project-history https://github.com/schacon/project-history
$ git push project-history history:master
Counting objects: 12, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (12/12), 907 bytes, done.
Total 12 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (12/12), done.
To git@github.com:schacon/project-history.git
 * [new branch]      history -> master

Goed, onze historie is nu openbaar. Nu is het moeilijkere gedeelte het terugsnoeien van onze recente historie zodat het kleiner wordt. We moeten een overlap maken zodat we een commit kunnen vervangen in een repository die een gelijke commit heeft, dus we gaan deze terugsnoeien tot alleen commits vier en vijf (dus de vierde commit overlapt).

$ git log --oneline --decorate
ef989d8 (HEAD, master) fifth commit
c6e1e95 (history) fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Het is in dit geval handig om een basis commit te maken die instructies bevat hoe de historie uit te breiden, zodat andere ontwikkelaars weten wat te doen als ze de eerste commit in de afgekapte historie tegenkomen en meer nodig hebben. Dus wat we hier gaan doen is een initieel commit object maken als onze basis en daar instructies in zetten, dan rebasen we de overige commits (vier en vijf) daar bovenop.

Om dat te doen, moeten we een punt kiezen om af te splitsen, wat voor ons de derde commit is, welke 9c68fdc in SHA-spraak is. Dus onze basis commit zal van die tree af worden getakt. We kunnen onze basis commit maken met het commit-tree commando, wat gewoon een tree neemt en ons een SHA-1 teuggeeft van een gloednieuw, ouderloos commit object.

$ echo 'get history from blah blah blah' | git commit-tree 9c68fdc^{tree}
622e88e9cbfbacfb75b5279245b9fb38dfea10cf
Note

Het commit-tree commando is een uit de reeks van commando’s die gewoonlijk binnenwerk (plumbing) commando’s worden genoemd. Dit zijn commando’s die niet direct voor normaal gebruik bedoeld zijn, maar die in plaats daarvan door andere Git commando’s worden gebruikt om kleinere taken uit te voeren. Bij tijd en wijle, als we wat vreemdere zaken als dit uitvoeren, stellen ze ons in staat om echt lage dingen uit te voeren maar ze zijn niet bedoeld voor dagelijks gebruik. Je kunt meer over deze plumbing commando’s lezen in Binnenwerk en koetswerk (plumbing and porcelain).

replace3

Goed, nu we dus een basis commit hebben, kunnen we de rest van onze historie hier boven op rebasen met git rebase --onto. Het --onto argument zal de SHA-1 zijn die we zojuist terugkregen van commit-tree en het rebase punt zal de derde commit zijn (de ouder van de eerste commit die we willen bewaren: 9c68fdc):

$ git rebase --onto 622e88 9c68fdc
First, rewinding head to replay your work on top of it...
Applying: fourth commit
Applying: fifth commit
replace4

Mooi, dus we hebben onze recente historie herschreven bovenop een weggooi basis commit die nu onze instructies bevat hoe de gehele historie weer te herbouwen als we dat zouden willen. We kunnen die nieuwe historie op een nieuw project pushen en nu, als mensen die repository klonen, zullen ze alleen de meest recente twee commits zien en dan een basis commit met instructies.

Laten we de rollen nu omdraaien naar iemand die het project voor het eerst kloont en die de hele historie wil hebben. Om de historische gegevens na het klonen van deze gesnoeide repository te krijgen, moet je een tweede remote toevoegen voor de historische repository en deze dan fetchen:

$ git clone https://github.com/schacon/project
$ cd project

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git remote add project-history https://github.com/schacon/project-history
$ git fetch project-history
From https://github.com/schacon/project-history
 * [new branch]      master     -> project-history/master

Nu zal de medewerker hun recente commits in de master branch hebben en de historische commits in de project-history/master branch.

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
622e88e get history from blah blah blah

$ git log --oneline project-history/master
c6e1e95 fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Om deze te combineren, kan je simpelweg git replace aanroepen met de commit die je wilt vervangen en dan de commit waarmee je het wilt vervangen. Dus we willen de "fourth" commit in de master branch met de "fourth" commit in de project-history/master branch vervangen:

$ git replace 81a708d c6e1e95

Als je nu naar de historie van de master branch kijkt, lijkt het er zo uit te zien:

$ git log --oneline master
e146b5f fifth commit
81a708d fourth commit
9c68fdc third commit
945704c second commit
c1822cf first commit

Gaaf, toch? Zonder alle SHA-1 stroomopwaarts te hoeven vervangen, waren we toch in staat om een commit in onze history te vervangen met een compleet andere commit en alle normale instrumenten (bisect, blame, etc.) blijven werken zoals we van ze verwachten.

replace5

Interessant genoeg, blijf het nog steeds 81a708d als de SHA-1 laten zien, zelfs als het in werkelijkheid de gegevens van de c6e1e95 commit gebruikt waar we het mee hebben vervangen. Zelfs als je een commando als cat-file aanroept, zal het je de vervangen gegevens tonen:

$ git cat-file -p 81a708d
tree 7bc544cf438903b65ca9104a1e30345eee6c083d
parent 9c68fdceee073230f19ebb8b5e7fc71b479c0252
author Scott Chacon <schacon@gmail.com> 1268712581 -0700
committer Scott Chacon <schacon@gmail.com> 1268712581 -0700

fourth commit

Onthoud dat de echte ouder van 81a708d onze plaatsvervangende commit was (622e88e), niet 9c68fdce zoals hier vermeld staat.

Het andere interessante is dat deze gegevens in onze referenties opgeslagen zijn:

$ git for-each-ref
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/heads/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/remotes/history/master
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/HEAD
e146b5f14e79d4935160c0e83fb9ebe526b8da0d commit	refs/remotes/origin/master
c6e1e95051d41771a649f3145423f8809d1a74d4 commit	refs/replace/81a708dd0e167a3f691541c7a6463343bc457040

Dit houdt in dat het eenvoudig is om onze vervanging met anderen te delen, omdat we deze naar onze server kunnen pushen en andere mensen het eenvoudig kunnen downloaden. Dit is niet zo nuttig in het scenario van historie-enten welke we hier nu behandeld hebben (omdat iedereen beide histories nu zou gaan downloaden, waarom zouden we ze dan gaan splitsen) maar het kan handig zijn in andere omstandigheden.