Git
Chapters ▾ 2nd Edition

3.1 Branchen in Git - Branches in vogelvlucht

Bijna elk VCS ondersteunt een bepaalde vorm van branchen. Branchen komt erop neer dat je een tak afsplitst van de hoofd-ontwikkellijn en daar verder mee gaat werken zonder aan de hoofdlijn te komen. Bij veel VCS’en is dat nogal een duur proces, vaak wordt er een nieuwe kopie gemaakt van de directory waar je broncode in staat, wat lang kan duren voor grote projecten.

Sommige mensen verwijzen naar het branch model in Git als de "killer eigenschap", en het onderscheidt Git zeker in de VCS-gemeenschap. Waarom is het zo bijzonder? De manier waarop Git brancht is ongelooflijk lichtgewicht, waardoor branch operaties vrijwel instant zijn en het wisselen tussen de branches over het algemeen net zo snel. In tegenstelling tot vele andere VCS’s, moedigt Git juist een workflow aan waarbij vaak gebrancht en gemerged wordt, zelfs meerdere keren per dag. Deze eigenschap begrijpen en de techniek beheersen geeft je een krachtig en uniek gereedschap en kan letterlijk de manier waarop je ontwikkelt veranderen.

Branches in vogelvlucht

Om de manier waarop Git brancht echt te begrijpen, moeten we een stap terug doen en onderzoeken hoe Git zijn gegevens opslaat. Zoals je je misschien herinnert van Aan de slag, slaat Git zijn gegevens niet op als een reeks van wijzigingen of delta’s, maar in plaats daarvan als een serie snapshots.

Als je in Git commit, dan slaat Git een commit object op dat een verwijzing bevat naar het snapshot van de inhoud die je gestaged heb Dit object bevat ook de auteur- en bericht-metagegevens, en nul of meer verwijzingen naar de commit of commits die de directe ouders van deze commit waren: nul ouders voor de eerste commit, één ouder voor een normale commit, en meerdere ouders voor een commit die het resultaat is van een merge van twee of meer branches.

Om dit te visualiseren, gaan we aannemen dat je een directory hebt met drie bestanden, en je staget en commit ze allemaal. Door de bestanden te stagen krijgen ze allemaal een checksum (de SHA-1 hash waar we het in Aan de slag over hadden), worden die versies de bestanden in het Git repository (Git noemt ze blobs) opgeslagen, en worden die checksums aan de staging area toegevoegd:

$ git add README test.rb LICENSE
$ git commit -m 'initial commit of my project'

Als je de commit aanmaakt door git commit uit te voeren zal Git iedere directory in het project (in dit geval alleen de root) van een checksum voorzien en slaat ze als boomstructuur (tree) objecten in de Git repository op. Daarna creëert Git een commit object dat de metagegevens bevat en een verwijzing naar de hoofd-tree-object van het project zodat Git deze snapshot opnieuw kan oproepen als dat nodig is.

Je Git repository bevat nu vijf objecten: een blob voor de inhoud van ieder van de drie bestanden, een tree die de inhoud van de directory weergeeft en specificeert welke bestandsnamen opgeslagen zijn als welke blob, en een commit met de verwijzing naar die hoofd-tree en alle commit-metagegevens.

Een commit en zijn tree.
Figure 9. Een commit en zijn tree

Als je wat wijzigingen maakt en nogmaals commit, dan slaat de volgende commit een verwijzing op naar de commit die er direct aan vooraf ging.

Commits en hun ouders.
Figure 10. Commits en hun ouders

Een branch in Git is simpelweg een lichtgewicht verplaatsbare verwijzing naar een van deze commits. De standaard branch-naam in Git is master. Als je commits begint te maken, krijg je een master-branch die wijst naar de laatste commit die je gemaakt hebt. Iedere keer als je commit, beweegt het automatisch vooruit.

Note

De “master” branch in Git is geen speciale branch. Het is gelijk aan eender welke branch. De enige reden waarom vrijwel elke repository er een heeft is dat de git init commando er standaard een maakt en de meeste mensen geen moeite doen om deze te wijzigen.

Een branch en zijn commit-historie.
Figure 11. Een branch en zijn commit-historie

Een nieuwe branch maken

Wat gebeurt er als je een nieuwe branch maakt? Het aanmaken zorgt ervoor dat er een nieuwe verwijzing (pointer) voor je wordt gemaakt die je heen en weer kan bewegen. Stel dat je een nieuwe branch maakt en die testing noemt. Je doet dit met het git branch commando:

$ git branch testing

Dit maakt een nieuwe pointer op dezelfde commit als waar je nu op staat.

Twee branches die naar de zelfde reeks van commits verwijzen.
Figure 12. Twee branches die naar de zelfde reeks van commits verwijzen

Hoe weet Git op welke branch je nu zit? Het houdt een speciale verwijzing bij genaamd HEAD. Let op dat dit heel anders is dan het concept van HEAD in andere VCS’s waar je misschien gewend aan bent, zoals Subversion of CVS. In Git is dit een verwijzing naar de lokale branch waar je nu op zit. In dit geval zit je nog steeds op master. Het git branch-commando heeft alleen een nieuwe branch aangemaakt - we zijn nog niet overgeschakeld naar die branch.

HEAD verwijzend naar een branch.
Figure 13. HEAD verwijzend naar een branch

Je kunt dit simpelweg zien door een eenvoudige git log commando uit te voeren wat je laat zien waar de branch pointers naar verwijzen. Deze optie heet --decorate.

$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit of my project

Je kunt de “master” en “testing” branches zien die direct naast de f30ab commit staan.

Tussen branches schakelen (switching)

Om te schakelen (switch) naar een bestaande branch, kan je het git checkout commando gebruiken. Laten we eens switchen naar de nieuwe testing branch:

$ git checkout testing

Dit verplaatst HEAD om te verwijzen naar de testing branch.

HEAD verwijst naar de huidige branch.
Figure 14. HEAD verwijst naar de huidige branch

Wat is daar nu belangrijk aan? Nou, laten we nog eens een commit doen:

$ vim test.rb
$ git commit -a -m 'made a change'
De HEAD branch beweegt voorwaarts als een commit wordt gedaan.
Figure 15. De HEAD branch beweegt voorwaarts als een commit wordt gedaan

Dat is interessant: omdat je testing branch nu naar voren is bewogen, maar je master branch nog steeds op het punt staat waar je was toen je git checkout uitvoerde om van branch te switchen. Laten we eens terug switchen naar de master branch:

$ git checkout master
HEAD verplaatst als je checkout uitvoert.
Figure 16. HEAD verplaatst als je checkout uitvoert

Dat commando deed twee dingen. Het verplaatste de HEAD pointer terug om te verwijzen naar de master branch, en het draaide de bestanden terug in je werk directory naar de stand van de snapshot waar de master naar verwijst. Dit betekent ook dat de wijzigingen die je vanaf nu maakt zullen afwijken van een oudere versie van het project. Essentieel draait dit het werk terug dat je in je testing branch gedaan hebt zodat je in een andere richting kunt bewegen.

Note
Branches switchen wijzigt bestanden in je directory

Het is belangrijk op te merken dat wanneer je tussen branches switcht in Git, bestanden in je werk directory zullen wijzigen. Als je naar een oudere branch switcht, zal je werk directory teruggedraaid worden zodat de inhoud gelijk is aan hoe het eruit zag toen je voor het laatst committe op die branch. Als Git dat niet op een nette manier kan doen, zal het je niet laten switchen.

Laten we een paar wijzigingen aanbrengen en opnieuw committen:

$ vim test.rb
$ git commit -a -m 'made other changes'

Nu is je project historie uiteengelopen (zie Uiteengelopen histories). Je hebt een branch gemaakt en bent er naartoe overgeschakeld, hebt er wat werk op gedaan, en bent toen teruggeschakeld naar je hoofd-branch en hebt nog wat ander werk gedaan. Al die veranderingen zijn geïsoleerd van elkaar in aparte branches: je kunt heen en weer schakelen tussen de branches en ze mergen als je klaar bent. En je hebt dat alles gedaan met eenvoudige branch, checkout en commit commando’s.

Uiteengelopen histories.
Figure 17. Uiteengelopen histories

Je kunt dit ook eenvoudig zien met het git log commando. Als je git log --oneline --decorate --graph --all uitvoert zal het de historie van jouw commits afdrukken, laten zien waar jouw branch pointers zijn en hoe je historie uiteengelopen is.

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project

Omdat een branch in Git in werkelijkheid een eenvoudig bestand is dat de SHA-1 checksum van 40 karakters van de commit waar het naar verwijst bevat, zijn branches goedkoop om te maken en te verwijderen. Een nieuwe branch maken is net zo snel en simpel te maken als 41 bytes naar een bestand te schrijven (40 karakters en een newline).

Dit is in schril contrast met de manier waarop de meeste ousere VCS applicaties branchen, wat het kopiëren van alle bestanden van het project in een tweede directorie inhoudt. Dit kan een aantal seconden of zelfs minuten duren, afhankelijk van de grootte van het project. Dit terwijl bij Git het proces altijd onmiddelijk gereed is. Daarbij komt dat, omdat we de ouders bijhouden als we een commit maken, het vinden van een goede merge basis voor het merge proces automatisch voor ons gedaan is en dit doorgaans eenvoudig te doen is. Deze kenmerken helpen ontwikkelaars aan te moedigen om vaak en veel branches aan te maken.

Laten we eens kijken waarom je dat zou moeten doen.