Git

7.9 Git Tools - Rerere

Rerere

De functionaliteit van git rerere is een beetje onbekend. De naam staat voor “reuse recorded resolution” (hergebruik opgenomen resoluties/oplossingen) en zoals de naam al aangeeft, stelt het je in staat om Git te vragen te onthouden hoe je een bepaald deel van een conflict hebt opgelost zodat Git, als het de volgende keer een vergelijkbaar conflict ziet, deze automatisch voor je kan oplossen.

Er zijn een aantal scenarios waarin deze functionaliteit erg handig zou kunnen zijn. Een van de voorbeelden dat in de documentatie wordt genoemd is dat je ervoor wilt zorgen dat een langlevende topic branch netjes zal mergen maar dat je niet een berg tussenliggende merge commits hoeft te maken die je historie vervuilen. Met rerere ingeschakeld kan je af en toe mergen, de conflicten oplossen en dan de merge terugdraaien. Als je dit doorlopend doet, zou de laatste merge eenvoudig moeten zijn omdat rerere alles gewoon automatisch voor je kan doen.

Deze zelfde taktiek kan gebruikt worden als je een branch rebased wilt houden zodat je niet elke keer met dezelfde rebasing conflicten te maken krijgt elke keer als je dit doet. Of als je een branch hebt die je hebt gemerged en daar een bergje conflicten hebt opgelost en dan besluit om deze toch maar te rebasen — je zult waarschijnlijk niet dezelfde conflicten willen doorlopen.

Een andere toepassing van rerere is er een waar je een af en toe aantal in ontwikkeling zijnde topic branches wilt mergen in een testbare head, zoals in het Git project zelf ook vaark gebeurt. Als de tests falen, kan je de merges terugdraaien en ze weer doen zonder de topic branch die de tests liet falen zonder de conflicten opnieuw te moeten oplossen.

Om de rerere functionaliteit in te schakelen, kan je eenvoudigweg de volgende configuratie setting aanroepen:

$ git config --global rerere.enabled true

Je kunt het ook inschakelen door de .git/rr-cache directory in een specifieke repository aan te maken, maar de configuratie setting is duidelijker en het kan globaal gedaan worden.

Laten we nu eens een eenvoudig voorbeeld bekijken, vergelijkbaar met de vorige. Laten we zeggen dat we een bestand hebben dat er als volgt uitziet:

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

In de ene branch hebben we het woord “hello” in “hola” gewijzigd, en daarna in de andere branch veranderen we “world” in “mundo”, net zoals eerder.

rerere1

Als we de twee branches mergen, zullen we een merge conflict krijgen:

$ git merge i18n-world
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Recorded preimage for 'hello.rb'
Automatic merge failed; fix conflicts and then commit the result.

Je zult de nieuwe regel Recorded preimage for FILE hier opmerken. Verder zou het er precies als een normale merge conflict uit moeten zien. Op dit moment kan rerere ons een aantal dingen vertellen. Normaalgesproken zou je een git status kunnen aanroepen om te zien waar de conflicten zitten:

$ git status
# On branch master
# Unmerged paths:
#   (use "git reset HEAD <file>..." to unstage)
#   (use "git add <file>..." to mark resolution)
#
#	both modified:      hello.rb
#

Echter, git rerere zal je ook vertellen waar het de pre-merge status voor heeft opgenomen met git rerere status:

$ git rerere status
hello.rb

En git rerere diff zal ons de huidige staat van de resolutie laten zien — waar je mee begonnen bent met oplossen en waar je het in hebt opgelost.

$ git rerere diff
--- a/hello.rb
+++ b/hello.rb
@@ -1,11 +1,11 @@
 #! /usr/bin/env ruby

 def hello
-<<<<<<<
-  puts 'hello mundo'
-=======
+<<<<<<< HEAD
   puts 'hola world'
->>>>>>>
+=======
+  puts 'hello mundo'
+>>>>>>> i18n-world
 end

Daarnaast (en dit is eigenlijk niet gerelateerd aan rerere), kan je ls-files -u gebruiken om de conflicterende bestanden en de voor, links en rechts versies te zien:

$ git ls-files -u
100644 39804c942a9c1f2c03dc7c5ebcd7f3e3a6b97519 1	hello.rb
100644 a440db6e8d1fd76ad438a49025a9ad9ce746f581 2	hello.rb
100644 54336ba847c3758ab604876419607e9443848474 3	hello.rb

Je kunt het oplossen zodat het alleen puts 'hola mundo' wordt en je kunt het rerere diff commando nog een keer aanroepen om te zien wat rerere zal onthouden:

$ git rerere diff
--- a/hello.rb
+++ b/hello.rb
@@ -1,11 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-<<<<<<<
-  puts 'hello mundo'
-=======
-  puts 'hola world'
->>>>>>>
+  puts 'hola mundo'
 end

Dit zegt eigenlijk dat, wanneer Git een conflict in een deel van een hello.rb bestand ziet waar “hello mundo” aan de ene en “hola world” aan de andere kant staat, het zal oplossen naar “hola mundo”.

Nu kunnen we het als opgelost markeren en committen:

$ git add hello.rb
$ git commit
Recorded resolution for 'hello.rb'.
[master 68e16e5] Merge branch 'i18n'

Je kunt zien aan de boodschap "Recorded resolution for FILE" zien dat het de resolutie voor het bestand heeft opgeslagen.

rerere2

Laten we nu die merge eens ongedaan maken, en in plaats daarvan deze op onze master branch gaan rebasen. We kunnen onze branch terugzetten door reset te gebruiken zoals we zagen in Reset ontrafeld.

$ git reset --hard HEAD^
HEAD is now at ad63f15 i18n the hello

Onze merge is ongedaan gemaakt. Laten we de topic branch gaan rebasen.

$ git checkout i18n-world
Switched to branch 'i18n-world'

$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: i18n one word
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Failed to merge in the changes.
Patch failed at 0001 i18n one word

We hebben nu dezelfde merge conflict zoals verwacht, maar kijk eens naar de regel met Resolved FILE using previous resolution. Als we nu het bestand bekijken zullen we zien dat het al is opgelost, er staan geen merge conflict markeringen in.

#! /usr/bin/env ruby

def hello
  puts 'hola mundo'
end
-----

Ook zal `git diff` je laten zien hoe het automatisch opnieuw was opgelost:

[source,console]

$ git diff diff --cc hello.rb index a440db6,54336ba..0000000 --- a/hello.rb + b/hello.rb @@@ -1,7 -1,7 +1,7 @@@ #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end
image::images/rerere3.png[]

Je kunt ook de staat van het conflicterende bestand opnieuw creeëren met het `git checkout` commando:

[source,console]

$ git checkout --conflict=merge hello.rb $ cat hello.rb #! /usr/bin/env ruby

def hello <<<<<<< ours puts hola world

  puts 'hello mundo'
>>>>>>> theirs
end
We zagen hier eerder een voorbeeld van in <<_advanced_merging>>.
Voor nu echter, laten we het opnieuw oplossen door eenvoudigweg weer `rerere` aan te roepen:

[source,console]

$ git rerere Resolved hello.rb using previous resolution. $ cat hello.rb #! /usr/bin/env ruby

def hello puts hola mundo end

We hebben het bestand automatisch her-opgelost door de opgeslagen `rerere` resolutie te gebruiken.
Je kunt het nu toevoegen en de rebase vervolgen om het te voltooien.

[source,console]

$ git add hello.rb $ git rebase --continue Applying: i18n one word

Dus, als je vaak opnieuw merget, of je wilt een topic branch up-to-date houden met je master branch zonder talloze merges, of als je vaak rebaset, kan je `rerere` aanzetten om je leven wat aangenamer te maken.


////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Debuggen met Git

Additioneel aan het feit dat het primair voor versiebeheer is, levert Git ook een aantal instrumenten om je te helpen met het debuggen van problemen in je projecten.
Omdat Git is ontworpen om te werken met bijna alle soorten projecten, zijn deze instrumenten redelijk generiek, maar ze kunnen je vaak helpen om een fout op te sporen of een schuldige aan te wijzen als dingen fout gaan.

[[_file_annotation]]
==== Bestands annotatie

Als je op zoek bent naar een bug in je code en je wilt weten wanneer deze er in is geslopen en waarom, is bestands annotatie vaak het beste gereedschap.
Het laat voor alle regels in alle bestanden zien welke de commit de laatste was die een wijziging aanbracht.
Dus, als je ziet dat een methode in je code labiel is, kan je het bestand annoteren met `git blame` om te zien welke commit verantwoordelijk was voor de introductie van die regel.

Het volgende voorbeeld gebruikt `git blame` om te bepalen welke commit en committer verantwoordelijk zijn voor regels in de top-level Linux kernel `Makefile` en, vervolgens, de `-L` optie om de uitvoer van de geannoteerde regels te beperken tot regels 69 tot en met 82 van dat bestand:

[source,console]

$ git blame -L 69,82 Makefile b8b0618cf6fab (Cheng Renquan 2009-05-26 16:03:07 +0800 69) ifeq ("$(origin V)", "command line") b8b0618cf6fab (Cheng Renquan 2009-05-26 16:03:07 +0800 70) KBUILD_VERBOSE = $(V) ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 71) endif ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 72) ifndef KBUILD_VERBOSE ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 73) KBUILD_VERBOSE = 0 ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 74) endif ^1da177e4c3f4 (Linus Torvalds 2005-04-16 15:20:36 -0700 75) 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 76) ifeq ($(KBUILD_VERBOSE),1) 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 77) quiet = 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 78) Q = 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 79) else 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 80) quiet=quiet_ 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 81) Q = @ 066b7ed955808 (Michal Marek 2014-07-04 14:29:30 +0200 82) endif

Merk op dat het eerste veld een deel is van de SHA-1 van de commit die het laatste die regel wijzigde.
De volgende twee velden zijn waarden die uit die commit zijn gehaald -- de naam van de auteur en de schrijfdatum van die commit -- zodat je eenvoudig kunt zien wie de regel gewijzigd heeft en wanneer.
Daarna volgt het regelnummer en de inhoud van het bestand.
Merk ook de `^1da177e4c3f4` commit regels op, waar de `^` prefix de regels aangeeft dit in de initiele commit van de repository aan dit project zijn toegevoegd, en deze regels zijn sindsdien niet gewijzigd.
Dit is een beetje verwarrend, omdat je nu op z'n minst drie verschillende manieren hebt gezien waarop Git het `^`-teken heeft gebruikt om een SHA-1 van een commit te duiden, maar dat is wat het hier betekent.

Een ander gaaf iets van Git is dat het bestandsnaam wijzigingen niet expliciet bijhoudt.
Het slaat de snapshots op en probeert dan impliciet uit te vinden dat het hernoemd is, nadat het gebeurd is.
Een van de interessante toepassingen hiervan is dat je het ook kunt vragen allerhande code verplaatsingen uit te vinden.
Als je `-C` aan `git blame` meegeeft, zal Git het bestand dat je annoteerd analiseren en probeert het uit te vinden waar delen van de code in dat bestand oorspronkelijk vandaan kwamen als ze van elders waren gekopieerd.
Bijvoorbeeld, stel dat je een bestand genaamd `GITServerHandler.m` aan het herstructureren bent in meerdere bestanden, waarvan er een `GITPackUpload.m` heet.
Door `GITPackUpload.m` te 'blamen' met de `-C` optie, kan je zien waar delen van de code oorspronkelijk vandaan kwamen:

[source,console]

$ git blame -C -L 141,153 GITPackUpload.m f344f58d GITServerHandler.m (Scott 2009-01-04 141) f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC f344f58d GITServerHandler.m (Scott 2009-01-04 143) { 70befddd GITServerHandler.m (Scott 2009-03-22 144) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 145) ad11ac80 GITPackUpload.m (Scott 2009-03-24 146) NSString *parentSha; ad11ac80 GITPackUpload.m (Scott 2009-03-24 147) GITCommit *commit = [g ad11ac80 GITPackUpload.m (Scott 2009-03-24 148) ad11ac80 GITPackUpload.m (Scott 2009-03-24 149) //NSLog(@"GATHER COMMI ad11ac80 GITPackUpload.m (Scott 2009-03-24 150) 56ef2caf GITServerHandler.m (Scott 2009-01-05 151) if(commit) { 56ef2caf GITServerHandler.m (Scott 2009-01-05 152) [refDict setOb 56ef2caf GITServerHandler.m (Scott 2009-01-05 153)

Dit is erg nuttig.
Normaalgesproken krijg je als de oorspronkelijke commit, de commit waar je de code naartoe hebt gekopieerd, omdat dat de eerste keer is dat je deze regels in dit bestand hebt aangeraakt.
Git geeft je de oorspronkelijke commit waarin je deze regels hebt geschreven, zelfs als dat in een ander bestand was.

[[_binary_search]]
==== Binair zoeken

Een bestand annoteren helpt je als je meteen al weet waar het probleem is.
Als je niet weet wat er kapot gaat, en er zijn tientallen of honderden commits geweest sinds de laatste staat waarin je weet dat de code werkte, zal je waarschijnlijk `git bisect` inschakelen voor hulp.
Het `bisect` commando voert een binair zoektocht uit door je commit historie om je te helpen zo snel als mogelijk de commit te vinden die een probleem heeft geïntroduceerd.

Stel dat je zojuist een release van je code hebt ingevoerd in een productie omgeving, je krijgt fout rapporten over iets wat niet in je ontwikkelomgeving optrad, en je kunt je niet indenken waarom de code zich zo gedraagt.
Je duikt in je code en het blijkt dat je het probleem kunt reproduceren, maar je kunt maar niet vinden waar het fout gaat.
Je kunt de code _bisecten_ om dit op te sporen.
Eerst roep je `git bisect start` aan om het proces in gang te zetten, en dan gebruik je `git bisect bad` om het systeem te vertellen dat de huidige commit waar je op staat kapot is.
Daarna moet je bisect vertellen waar de laatst bekende goede staat was, door `git bisect good <goede_commit> te gebruiken:

[source,console]

$ git bisect start $ git bisect bad $ git bisect good v1.0 Bisecting: 6 revisions left to test after this [ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo

Git kon opzoeken dat er ongeveer 12 commit zijn geweest tussen de commit die je als de laatst correcte hebt gemarkeerd (v1.0) en de huidige slechte versie, en dat het de middelste voor je heeft uitgecheckt.
Op dit moment kan je je tests laten lopen om te zien of het probleem op deze commit voorkomt.
Als dit het geval is, dan was het ergens voor deze middelste commit erin geslopen; als dat het niet het geval is, dan is het probleem na deze middelste commit geïntroduceerd.
Het blijkt nu dat er hier geen probleem is, en je zegt Git dit door `git bisect good` in te typen en je zoektocht voort te zetten:

[source,console]

$ git bisect good Bisecting: 3 revisions left to test after this [b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing

Nu zit je op een andere commit, halverwege tussen de ene die je zojuist getest hebt, en je slechte commit.
Je gaat weer testen en ziet nu dat deze commit kapot is, dus je zegt dit met `git bisect bad`:

[source,console]

$ git bisect bad Bisecting: 1 revisions left to test after this [f71ce38690acf49c1f3c9bea38e09d82a5ce6014] drop exceptions table

Deze commit is prima, en nu heeft Git alle informatie die het nodig heeft om te bepalen waar het probleem is begonnen.
Het geeft je de SHA-1 van de eerste slechte commit en laat je wat van de commit informatie zien en welke bestanden gewijzigd waren in die commit zodat je kunt uitzoeken wat er gebeurd is waardoor deze fout optreedt:

[source,console]

$ git bisect good b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04 Author: PJ Hyett <pjhyett@example.com> Date: Tue Jan 27 14:48:32 2009 -0800

secure this thing

:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730 f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M config

Als je klaar bent, moet je `git bisect reset` aanroepen om je HEAD terug te zetten naar waar je was voordat je startte, of je verzandt in een hele vreemde status:

[source,console]

$ git bisect reset

Dit is een krachtig instrument dat je kan helpen met het in enkele minuten doorzoeken van honderden commits voor een opgetreden fout.
Als je een script hebt dat met 0 eindigt als het project correct en niet-0 als het project fout is, kan je het `git bisect` proces zelfs volledig automatiseren.
Allereerst vertel je het de reikwijdte van de bisect door de bekende goede en slechte commits door te geven.
Je kunt dit doen door ze te tonen met de `bisect start` commando als je dit wilt, door de bekende slechte commit als eerste door te geven en de bekende goede commit als tweede:

[source,console]

$ git bisect start HEAD v1.0 $ git bisect run test-error.sh

Op deze manier wordt `test-error.sh` automatisch aanroepen voor elke uitgecheckte commit totdat Git de eerste kapotte commit vindt.
Je kunt ook zoiets als `make` of `make tests` aanroepen of wat je ook maar hebt dat geautomatiseerde tests voor je uitvoert.


[[_git_submodules]]
////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Submodules

Het gebeurt vaak dat terwijl je aan het werk bent op het ene project, je van daar uit een ander project moet gebruiken.
Misschien is het een door een derde partij ontwikkelde library of een die je zelf elders aan het ontwikkelen bent en die je gebruikt in meerdere ouder (parent) projecten.
Er ontstaat een veelvoorkomend probleem in deze scanario's: je wilt de twee projecten als zelfstandig behandelen maar ondertussen wel in staat zijn om de een vanuit de ander te gebruiken.

Hier is een voorbeeld.
Stel dat je een web site aan het ontwikkelen bent en daar Atom feeds maakt.
In plaats van je eigen Atom-genererende code te schrijven, besluit je om een library te gebruiken.
De kans is groot dat je deze code moet insluiten vanuit een gedeelde library zoals een CPAN installatie of een Ruby gem, of de broncode naar je projecttree moet kopieëren.
Het probleem met de library insluiten is dat het lastig is om de library op enige manier aan te passen aan jouw wensen en vaak nog moeilijker om het uit te rollen, omdat je jezelf ervan moet verzekeren dat elke gebruikende applicatie deze library beschikbaar moet hebben.
Het probleem met het inbouwen van de code in je eigen project is dat elke eigen aanpassing het je moeilijk zal maken om te mergen als er stroomopwaarts wijzigingen beschikbaar komen.

Git adresseert dit probleem met behulp van submodules.
Submodules staan je toe om een Git repository als een subdirectory van een andere Git repository op te slaan.
Dit stelt je in staat om een andere repository in je eigen project te klonen en je commits apart te houden.

[[_starting_submodules]]
==== Beginnen met submodules

We zullen een voorbeeld nemen van het ontwikkelen van een eenvoudig project die is opgedeeld in een hoofd project en een aantal sub-projecten.

Laten we beginnen met het toevoegen van een bestaande Git repository als een submodule van de repository waar we op aan het werk zijn.
Om een nieuwe submodule toe te voegen gebruik je het `git submodule add` commando met de absolute of relatieve URL van het project dat je wilt gaan tracken.
In dit voorbeeld voegen we een library genaamd ``DbConnector'' toe.

[source,console]

$ git submodule add https://github.com/chaconinc/DbConnector Cloning into DbConnector…​ remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity…​ done.

Standaard zal submodules het subproject in een directory met dezelfde naam als de repository toevoegen, in dit geval ``DbConnector''.
Je kunt aan het eind van het commando een ander pad toevoegen als je het ergens anders wilt laten landen.

Als je `git status` op dit moment aanroept, zullen je een aantal dingen opvallen.

[source,console]

$ git status On branch master Your branch is up-to-date with origin/master.

Changes to be committed: (use "git reset HEAD <file>…​" to unstage)

new file:   .gitmodules
new file:   DbConnector
Het eerste wat je moet opvallen is het nieuwe `.gitmodules` bestand.
Dit is een configuratie bestand waarin de relatie wordt vastgelegd tussen de URL van het project en de lokale subdirectory waar je het in gepulld hebt:

[source,ini]
path = DbConnector
url = https://github.com/chaconinc/DbConnector
Als je meerdere submodules hebt, zal je meerdere van deze regels in dit bestand hebben.
Het is belangrijk om op te merken dat dit bestand onder versiebeheer staat samen met je andere bestanden, zoals je `.gitignore` bestand.
Het wordt met de rest van je project gepusht en gepulld.
Dit is hoe andere mensen die dit project klonen weten waar ze de submodule projecten vandaan moeten halen.

[NOTE]
=====
Omdat de URL in het .gitmodules bestand degene is waarvan andere mensen als eerste zullen proberen te klonen of fetchen, moet je je ervan verzekeren dat ze er wel bij kunnen.
Bijvoorbeeld, als je een andere URL gebruikt om naar te pushen dan waar anderen van zullen pullen, gebruik dan degene waar anderen toegang toe hebben.
Je kunt deze waarde lokaal overschrijven met `git config submodule.DbConnector.url PRIVATE_URL` voor eigen gebruik.
Waar van toepassing, kan een relatieve URL nuttig zijn.
=====

De andere regel in de `git status` uitvoer is de entry voor de project folder.
Als je `git diff` daarop aanroept, zal je iets opvallends zien:

[source,console]

$ git diff --cached DbConnector diff --git a/DbConnector b/DbConnector new file mode 160000 index 0000000..c3f01dc --- /dev/null + b/DbConnector @@ -0,0 +1 @@ +Subproject commit c3f01dc8862123d317dd46284b05b6892c7b29bc

Alhoewel `DbConnector` een subdirectory is in je werk directory, zie Git het als een submodule en zal de inhoud ervan niet tracken als je niet in die directory staat.
In plaats daarvan ziet Git het als een specifieke commit van die repository.

Als een een iets betere diff uitvoer wilt, kan je de `--submodule` optie meegeven aan `git diff`.

[source,console]

$ git diff --cached --submodule diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..71fc376 --- /dev/null + b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "DbConnector"] + path = DbConnector + url = https://github.com/chaconinc/DbConnector Submodule DbConnector 0000000…​c3f01dc (new submodule)

Als je commit, zal je iets als dit zien:

[source,console]

$ git commit -am added DbConnector module [master fb9093c] added DbConnector module 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 DbConnector

Merk de `160000` mode op voor de `DbConnector` entry.
Dat is een speciale mode in Git wat gewoon betekent dat je een commit opslaat als een directory entry in plaats van een subdirectory of een bestand.

[[_cloning_submodules]]
==== Een project met submodules klonen

Hier zullen we een project met een submodule erin gaan klonen.
Als je zo'n project kloont, krijg je standaard de directories die submodules bevatten, maar nog geen bestanden die daarin staan:

[source,console]

$ git clone https://github.com/chaconinc/MainProject Cloning into MainProject…​ remote: Counting objects: 14, done. remote: Compressing objects: 100% (13/13), done. remote: Total 14 (delta 1), reused 13 (delta 0) Unpacking objects: 100% (14/14), done. Checking connectivity…​ done. $ cd MainProject $ ls -la total 16 drwxr-xr-x 9 schacon staff 306 Sep 17 15:21 . drwxr-xr-x 7 schacon staff 238 Sep 17 15:21 .. drwxr-xr-x 13 schacon staff 442 Sep 17 15:21 .git -rw-r—​r-- 1 schacon staff 92 Sep 17 15:21 .gitmodules drwxr-xr-x 2 schacon staff 68 Sep 17 15:21 DbConnector -rw-r—​r-- 1 schacon staff 756 Sep 17 15:21 Makefile drwxr-xr-x 3 schacon staff 102 Sep 17 15:21 includes drwxr-xr-x 4 schacon staff 136 Sep 17 15:21 scripts drwxr-xr-x 4 schacon staff 136 Sep 17 15:21 src $ cd DbConnector/ $ ls $

De `DbConnector` directory is er wel, maar leeg.
Je moet twee commando's aanroepen: `git submodule init` om het lokale configuratie bestand te initialiseren, en `git submodule update` om alle gegevens van dat project te fetchen en de juiste commit uit te checken die in je superproject staat vermeld:

[source,console]

$ git submodule init Submodule DbConnector (https://github.com/chaconinc/DbConnector) registered for path DbConnector $ git submodule update Cloning into DbConnector…​ remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity…​ done. Submodule path DbConnector: checked out c3f01dc8862123d317dd46284b05b6892c7b29bc

Nu is je `DbConnector` subdirectory in precies dezelfde staat als het was toen je het eerder committe.

Er is echter een andere, iets eenvoudiger, manier om dit te doen.
Als je `--recursive-submodules` doorgeeft aan het `git clone` commando zal het automatisch elke submodule in de repository initialiseren en updaten.

[source,console]

$ git clone --recurse-submodules https://github.com/chaconinc/MainProject Cloning into MainProject…​ remote: Counting objects: 14, done. remote: Compressing objects: 100% (13/13), done. remote: Total 14 (delta 1), reused 13 (delta 0) Unpacking objects: 100% (14/14), done. Checking connectivity…​ done. Submodule DbConnector (https://github.com/chaconinc/DbConnector) registered for path DbConnector Cloning into DbConnector…​ remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity…​ done. Submodule path DbConnector: checked out c3f01dc8862123d317dd46284b05b6892c7b29bc

==== Werken aan een project met submodules

Nu hebben we een kopie van een project met submodules erin en gaan we met onze teamgenoten samenwerken op zowel het hoofdproject als het submodule project.

===== Wijzigingen van stroomopwaarts pullen

De eenvoudigste werkwijze bij het gebruik van submodules in een project zou zijn als je eenvoudigweg een subproject naar binnentrekt en de updates ervan van tijd tot tijd binnen haalt maar waarbij je niet echt iets wijzigt in je checkout.
Laten we een eenvoudig voorbeeld doornemen.

Als je wilt controleren voor nieuw werk in een submodule, kan je in de directory gaan en `git fetch` aanroepen en `git merge` gebruiken om de wijzigingen uit de branch stroomopwaarts in de lokale code in te voegen.

[source,console]

$ git fetch From https://github.com/chaconinc/DbConnector c3f01dc..d0354fc master → origin/master $ git merge origin/master Updating c3f01dc..d0354fc Fast-forward scripts/connect.sh | 1
src/db.c | 1
2 files changed, 2 insertions(+)

Als je nu teruggaat naar het hoofdproject en `git diff --submodule` aanroept kan je zien dat de submodule is bijgewerkt en je krijgt een lijst met commits die eraan is toegevoegd.
Als je niet elke keer `--submodule` wilt intypen voor elke keer dat je `git diff` aanroept, kan je dit als standaard formaat instellen door de `diff.submodule` configuratie waarde op ``log'' te zetten.

[source,console]

$ git config --global diff.submodule log $ git diff Submodule DbConnector c3f01dc..d0354fc: > more efficient db routine > better connection routine

Als je nu gaat committen zal je de nieuwe code in de submodule insluiten als andere mensen updaten.

Er is ook een makkelijker manier om dit te doen, als je er de voorkeur aan geeft om niet handmatig te fetchen en mergen in de subdirectory.
Als je `git submodule update --remote` aanroept, zal Git naar je submodules gaan en voor je fetchen en updaten.

[source,console]

$ git submodule update --remote DbConnector remote: Counting objects: 4, done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 2), reused 4 (delta 2) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector 3f19983..d0354fc master → origin/master Submodule path DbConnector: checked out d0354fc054692d3906c85c3af05ddce39a1c0644

Dit commando zal standaard aannemen dat je de checkout wilt updaten naar de `master`-branch van de submodule repository.
Je kunt echter dit naar iets anders wijzigen als je wilt.
Bijvoorbeeld, als je de DbConnector submodule de ``stable'' branch van die repository wilt laten tracken, kan je dit aangeven in het `.gitmodules` bestand (zodat iedereen deze ook trackt), of alleen in je lokale `.git/config` bestand.
Laten we het aangeven in het `.gitmodules` bestand:

[source,console]

$ git config -f .gitmodules submodule.DbConnector.branch stable

$ git submodule update --remote remote: Counting objects: 4, done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 2), reused 4 (delta 2) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector 27cf5d3..c87d55d stable → origin/stable Submodule path DbConnector: checked out c87d55d4c6d4b05ee34fbc8cb6f7bf4585ae6687

Als je de `-f .gitmodules` weglaat, zal het de wijziging alleen voor jou maken, maar het is waarschijnlijk zinvoller om die informatie bij de repository te tracken zodat iedereen dat ook zal gaan doen.

Als we nu `git status` aanroepen, zal Git ons laten zien dat we ``new commits'' hebben op de submodule.

[source,console]

$ git status On branch master Your branch is up-to-date with origin/master.

Changes not staged for commit: (use "git add <file>…​" to update what will be committed) (use "git checkout — <file>…​" to discard changes in working directory)

modified:   .gitmodules
modified:   DbConnector (new commits)

no changes added to commit (use "git add" and/or "git commit -a")

Als je de configuratie instelling `status.submodulessummary` instelt, zal Git je ook een korte samenvatting van de wijzigingen in je submodule laten zien:

[source,console]

$ git config status.submodulesummary 1

$ git status On branch master Your branch is up-to-date with origin/master.

Changes not staged for commit: (use "git add <file>…​" to update what will be committed) (use "git checkout — <file>…​" to discard changes in working directory)

modified:   .gitmodules
modified:   DbConnector (new commits)

Submodules changed but not updated:

  • DbConnector c3f01dc…​c87d55d (4): > catch non-null terminated lines

Als je op dit moment `git diff` aanroept kunnen we zien dat zowel we onze `.gitmodules` bestand hebben gewijzigd als dat daarbij er een aantal commmits is die we omlaag hebben gepulld en die klaar staan om te worden gecommit naar ons submodule project.

[source,console]

$ git diff diff --git a/.gitmodules b/.gitmodules index 6fc0b3d..fd1cc29 100644 --- a/.gitmodules + b/.gitmodules @@ -1,3 +1,4 @@ [submodule "DbConnector"] path = DbConnector url = https://github.com/chaconinc/DbConnector + branch = stable Submodule DbConnector c3f01dc..c87d55d: > catch non-null terminated lines > more robust error handling > more efficient db routine > better connection routine

Dit is best wel handig omdat we echt de log met commits kunnen zien waarvan we op het punt staan om ze in onze submodule te committen.
Eens gecommit, kan je deze informatie ook achteraf zien als je `git log -p` aanroept.

[source,console]

$ git log -p --submodule commit 0a24cfc121a8a3c118e0105ae4ae4c00281cf7ae Author: Scott Chacon <schacon@gmail.com> Date: Wed Sep 17 16:37:02 2014 +0200

updating DbConnector for bug fixes

diff --git a/.gitmodules b/.gitmodules index 6fc0b3d..fd1cc29 100644 --- a/.gitmodules + b/.gitmodules @@ -1,3 +1,4 @@ [submodule "DbConnector"] path = DbConnector url = https://github.com/chaconinc/DbConnector + branch = stable Submodule DbConnector c3f01dc..c87d55d: > catch non-null terminated lines > more robust error handling > more efficient db routine > better connection routine

Git zal standaard proberen *alle*  submodules te updaten als je `git submodule update --remote` aanroept, dus als je er hier veel van hebt, is het wellicht aan te raden om de naam van alleen die submodule door te geven die je wilt updaten.

===== Werken aan een submodule

Het is zeer waarschijnlijk dat als je submodules gebruikt, je dit zult doen omdat je echt aan de code in die submodule wilt werken tegelijk met het werken aan de code in het hoofdproject (of verspreid over verschillende submodules).
Anders zou je waarschijnlijk een eenvoudiger afhankelijkheids-beheer systeem (dependency management system) hebben gebruikt (zoals Maven of Rubygems).

Dus laten we nu eens een voorbeeld behandelen waarin we gelijkertijd wijzigingen aan de submodule en het hoofdproject maken en deze wijzigingen ook gelijktijdig committen en publiceren.

Tot dusverre, als we het `git submodule update` commando aanriepen om met fetch wijzigen uit de repositories van de submodule te halen, ging Git de wijzigingen ophalen en de files in de subdirectory updaten, maar zou het de subdirectory laten in een staat die bekend staat als ``detached HEAD''.
Dit houdt in dat er geen lokale werk branch is (zoals ``master'', bijvoorbeeld) waar de wijzigingen worden getrackt.
Zonder een werkbranch waarin de wijzigingen worden getrackt, betekent het dat zelfs als je wijzigingen aan de submodule commit, deze wijzigingen waarschijnlijk verloren zullen gaan bij de volgende keer dat je `git submodule update` aanroept.
Je zlut een aantal extra stappen moeten zetten als je wijzigingen in een submodule wilt laten tracken.

Om de submodule in te richten zodate het eenvoudiger is om erin te werken, moet je twee dingen doen.
Je moet in elke submodule gaan en een branch uitchecken om in te werken.
Daarna moet je Git vertellen wat het moet doen als je wijzigingen hebt gemaakt en daarna zal `git submodule update --remote` nieuw werk van stroomopwaarts pullen.
Je hebt nu de keuze om dit in je lokale werk te mergen, of je kunt proberen je nieuwe lokale werk te rebasen bovenop de nieuwe wijzigingen.

Laten we eerst in onze submodule directory gaan en een branch uitchecken.

[source,console]

$ git checkout stable Switched to branch stable

Laten we het eens proberen met de ``merge'' optie.
Om dit handmatig aan te geven kunnen we gewoon de `--merge` optie in onze `update` aanroep toevoegen.
Hier zullen we zien dat er een wijziging op de server was voor deze submodule en deze wordt erin gemerged.

[source,console]

$ git submodule update --remote --merge remote: Counting objects: 4, done. remote: Compressing objects: 100% (2/2), done. remote: Total 4 (delta 2), reused 4 (delta 2) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector c87d55d..92c7337 stable → origin/stable Updating c87d55d..92c7337 Fast-forward src/main.c | 1
1 file changed, 1 insertion(+) Submodule path DbConnector: merged in 92c7337b30ef9e0893e758dac2459d07362ab5ea

Als we in de DbConnector directory gaan, hebben we de nieuwe wijzigingen al in onze lokale `stable`-branch gemerged.
Laten we nu eens kijken wat er gebeurt als we onze lokale wijziging maken aan de library en iemand anders pusht tegelijk nog een wijziging stroomopwaarts.

[source,console]

$ cd DbConnector/ $ vim src/db.c $ git commit -am unicode support [stable f906e16] unicode support 1 file changed, 1 insertion(+)

Als we nu onze submodule updaten kunnen we zien wat er gebeurt als we een lokale wijziging maken en er stroomopwaarts ook nog een wijziging is die we moeten verwerken.

[source,console]

$ git submodule update --remote --rebase First, rewinding head to replay your work on top of it…​ Applying: unicode support Submodule path DbConnector: rebased into 5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94

Als je de `--rebase` of `--merge` bent vergeten, zal Git alleen de submodule updaten naar wat er op de server staat en je lokale project in een detached HEAD status zetten.

[source,console]

$ git submodule update --remote Submodule path DbConnector: checked out 5d60ef9bbebf5a0c1c1050f242ceeb54ad58da94

Maak je geen zorgen als dit gebeurt, je kunt eenvoudigweg teruggaan naar deze directory en weer je branch uitchecken (die je werk nog steeds bevat) en handmatig `origin/stable` mergen of rebasen (of welke remote branch je wilt).

Als je jouw wijzigingen aan je submodule nog niet hebt gecommit en je roept een submodule update aan die problemen zou veroorzaken, zal Git de wijzigingen ophalen (fetchen) maar het nog onbewaarde werk in je submodule directory niet overschrijven.

[source,console]

$ git submodule update --remote remote: Counting objects: 4, done. remote: Compressing objects: 100% (3/3), done. remote: Total 4 (delta 0), reused 4 (delta 0) Unpacking objects: 100% (4/4), done. From https://github.com/chaconinc/DbConnector 5d60ef9..c75e92a stable → origin/stable error: Your local changes to the following files would be overwritten by checkout: scripts/setup.sh Please, commit your changes or stash them before you can switch branches. Aborting Unable to checkout c75e92a2b3855c9e5b66f915308390d9db204aca in submodule path DbConnector

Als je wijzigingen hebt gemaakt die conflicteren met wijzigingen die stroomopwaarts zijn gemaakt, zal Git je dit laten weten als je de update uitvoert.

[source,console]

$ git submodule update --remote --merge Auto-merging scripts/setup.sh CONFLICT (content): Merge conflict in scripts/setup.sh Recorded preimage for scripts/setup.sh Automatic merge failed; fix conflicts and then commit the result. Unable to merge c75e92a2b3855c9e5b66f915308390d9db204aca in submodule path DbConnector

Je kunt in de directory van de submodule gaan en de conflicten oplossen op dezelfde manier zoals je anders ook zou doen.

[[_publishing_submodules]]
===== Submodule wijzigingen publiceren

We hebben nu een aantal wijzigingen in onze submodule directory.
Sommige van deze zijn van stroomopwaarts binnengekomen via onze updates en andere zijn lokaal gemaakt en zijn nog voor niemand anders beschikbaar omdat we ze nog niet hebben gepusht.

[source,console]

$ git diff Submodule DbConnector c87d55d..82d2ad3: > Merge from origin/stable > updated setup script > unicode support > remove unnecessary method > add new option for conn pooling

Als we het hoofdproject committen en deze pushen zonder de submodule wijzigingen ook te pushen, zullen andere mensen die willen zien wat onze wijzigingen inhouden problemen krijgen omdat er geen enkele manier is voor hen om de wijzigingen van de submodule te pakken krijgen waar toch op wordt voortgebouwd.
Deze wijzigingen zullen alleen in onze lokale kopie bestaan.

Om er zeker van te zijn dat dit niet gebeurt, kan je Git vragen om te controleren dat al je submodules juist gepusht zijn voordat het hoofdproject wordt gepusht.
Het `git push` commando leest het `--recurse-submodules` argument die op de waardes ``check'' of ``on-demand'' kan worden gezet.
De ``check'' optie laat een `push` eenvoudigweg falen als een van de gecomitte submodule wijzigingen niet is gepusht.

[source,console]

$ git push --recurse-submodules=check The following submodule paths contain changes that can not be found on any remote: DbConnector

Please try

git push --recurse-submodules=on-demand

or cd to the path and use

git push

to push them to a remote.

Zoals je kunt zien, geeft het ook wat behulpzame adviezen over wat he vervolgens kunnen doen.
De eenvoudige optie is om naar elke submodule te gaan en handmatig naar de remotes te pushen om er zeker van te zijn dat ze extern beschikbaar zijn en dan deze push nogmaals te proberen.

De andere optie is om de ``on-demand'' waarde te gebruiken, wat zal proberen dit voor je te doen.

[source,console]

$ git push --recurse-submodules=on-demand Pushing submodule DbConnector Counting objects: 9, done. Delta compression using up to 8 threads. Compressing objects: 100% (8/8), done. Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done. Total 9 (delta 3), reused 0 (delta 0) To https://github.com/chaconinc/DbConnector c75e92a..82d2ad3 stable → stable Counting objects: 2, done. Delta compression using up to 8 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (2/2), 266 bytes | 0 bytes/s, done. Total 2 (delta 1), reused 0 (delta 0) To https://github.com/chaconinc/MainProject 3d6d338..9a377d1 master → master

Zoals je hier kunt zien, ging Git in de DbConnector module en heeft deze gepusht voordat het hoofdproject werd gepusht.
Als die push van de submodule om wat voor reden ook faalt, zal de push van het hoofdproject ook falen.
Je kunt dit gedrag de standaard maken door `git config push.recurseSubmodules on-demand` te doen.

===== Submodule wijzigingen mergen

Als je een submodule-referentie wijzigt op hetzelfde moment als een ander, kan je in enkele problemen geraken.
In die zin, dat wanneer submodule histories uitelkaar zijn gaan lopen en naar uitelkaar lopende branches in het superproject worden gecommit, zal het wat extra werk van je vergen om dit te repareren.

Als een van de commits een directe voorouder is van de ander (een fast-forward merge), dan zal git eenvoudigweg de laatste voor de merge kiezen, dus dat werkt prima.

Git zal echter niet eens een triviale merge voor je proberen.
Als de submodule commits uiteen zijn gaan lopen en ze moeten worden gemerged, zal je iets krijgen wat hier op lijkt:

[source,console]

$ git pull remote: Counting objects: 2, done. remote: Compressing objects: 100% (1/1), done. remote: Total 2 (delta 1), reused 2 (delta 1) Unpacking objects: 100% (2/2), done. From https://github.com/chaconinc/MainProject 9a377d1..eb974f8 master → origin/master Fetching submodule DbConnector warning: Failed to merge submodule DbConnector (merge following commits not found) Auto-merging DbConnector CONFLICT (submodule): Merge conflict in DbConnector Automatic merge failed; fix conflicts and then commit the result.

Dus wat er hier eigenlijk gebeurd is, is dat Git heeft achterhaald dat de twee branches punten in de historie van de submodule hebben opgeslagen die uiteen zijn gaan lopen en die gemerged moeten worden.
Het legt dit uit als ``merge following commits not found'' (merge volgend op commits niet gevonden), wat verwarrend is, maar we leggen zo uit waarom dit zo is.

Om dit probleem op te lossen, moet je uit zien te vinden in welke staat de submodule in zou moeten zijn.
Vreemdgenoeg geeft Git je niet echt veel informatie om je hiermee te helpen, niet eens de SHA-1 getallen van de commits van beide kanten van de historie.
Gelukkig is het redelijk eenvoudig om uit te vinden.
Als je `git diff` aanroept kan je de SHA-1 getallen van de opgeslagen commits krijgen uit beide branches die je probeerde te mergen.

[source,console]

$ git diff diff --cc DbConnector index eb41d76,c771610..0000000 --- a/DbConnector + b/DbConnector

Dus in dit geval, is `eb41d76` de commit in onze submodule die *wij* hebben en `c771610` is de commit die stroomopwaarts aanwezig is.
Als we naar onze submodule directory gaan, moet het al aanwezig zin op `eb41d76` omdat de merge deze nog niet zal hebben aangeraakt.
Als deze om welke reden dan ook er niet is, kan je eenvoudigweg een branch die hiernaar wijst aanmaken en uit checken.

Wat nu een belangrijke rol gaat spelen is de SHA-1 van de commit van de andere kant.
Dit is wat je in zult moeten mergen en oplossen.
Je kunt ofwel de merge met de SHA-1 gewoon proberen, of je kunt een branch hiervoor maken en dan deze proberen te mergen.
We raden het laatste aan, al was het maar om een mooiere merge commit bericht te krijgen.

Dus, we gaan naar onze submodule directory, maken een branch gebaseerd op die tweede SHA-1 van `git diff` en mergen handmatig.

[source,console]

$ cd DbConnector

$ git rev-parse HEAD eb41d764bccf88be77aced643c13a7fa86714135

$ git branch try-merge c771610 (DbConnector) $ git merge try-merge Auto-merging src/main.c CONFLICT (content): Merge conflict in src/main.c Recorded preimage for src/main.c Automatic merge failed; fix conflicts and then commit the result.

We hebben een echte merge conflict, dus als we deze oplossen en committen, dan kunnen we eenvoudigweg het hoofdproject updaten met het resultaat.

[source,console]

$ vim src/main.c <1> $ git add src/main.c $ git commit -am merged our changes Recorded resolution for src/main.c. [master 9fd905e] merged our changes

$ cd .. <2> $ git diff <3> diff --cc DbConnector index eb41d76,c771610..0000000 --- a/DbConnector + b/DbConnector @@@ -1,1 -1,1 +1,1 @@@ - Subproject commit eb41d764bccf88be77aced643c13a7fa86714135 -Subproject commit c77161012afbbe1f58b5053316ead08f4b7e6d1d Subproject commit 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a $ git add DbConnector <4>

$ git commit -m "Merge Tom’s Changes" <5> [master 10d2c60] Merge Tom’s Changes

<1> Eerst lossen we het conflict op
<2> Dan gaan we terug naar de directory van het hoofdproject
<3> We controleren de SHA-1 getallen nog een keer
<4> Lossen de conflicterende submodule entry op
<5> Committen onze merge.

Dit kan nogal verwarrend overkomen, maar het is niet echt moeilijk.

Interessant genoeg, is er een ander geval die Git aankan.
Als er een merge commit bestaat in de directory van de submodule die *beide* commits in z'n historie bevat, zal Git je deze voorstellen als mogelijke oplossing.
Het ziet dat op een bepaald punt in het submodule project iemand branches heeft gemerged met daarin deze twee commits, dus wellicht wil je die hebben.

Dit is waarom de foutboodschap van eerder ``merge following commits not found'' was, omdat het *dit* niet kon doen.
Het is verwarrend omdat, wie verwacht er nu dat Git dit zou *proberen*?

Als het een enkele acceptabele merge commit vindt, zal je iets als dit zien:

[source,console]

$ git merge origin/master warning: Failed to merge submodule DbConnector (not fast-forward) Found a possible merge resolution for the submodule: 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a: > merged our changes If this is correct simply add it to the index for example by using:

git update-index --cacheinfo 160000 9fd905e5d7f45a0d4cbc43d1ee550f16a30e825a "DbConnector"

which will accept this suggestion. Auto-merging DbConnector CONFLICT (submodule): Merge conflict in DbConnector Automatic merge failed; fix conflicts and then commit the result.

Wat hier gesuggereerd wordt om te doen is om de index te updaten alsof je `git add` zou hebben aangeroepen, wat het conflict opruimt, en dan commit. Echter, je moet dit waarschijnlijk niet doen. Je kunt net zo makkelijk naar de directory van de submodule gaan, kijken wat het verschil is, naar deze commit fast-forwarden, het goed testen en daarna committen.

[source,console]

$ cd DbConnector/ $ git merge 9fd905e Updating eb41d76..9fd905e Fast-forward

$ cd .. $ git add DbConnector $ git commit -am Fast forwarded to a common submodule child

Dit bereikt hetzelfde, maar op deze manier kan je verfiëren dat het werkt en je hebt de code in je submodule als je klaar bent.

==== Submodule Tips

Er zijn een aantal dingen die je kunt doen om het werken met submodules iets eenvoudiger te maken.

===== Submodule Foreach

Er is een `foreach` submodule commando om een willekeurig commando aan te roepen in elke submodule.
Dit kan echt handig zijn als je een aantal submodules in hetzelfde project hebt.

Bijvoorbeeld, stel dat we een nieuwe functie willen beginnen te maken of een bugfix uitvoeren en we hebben werkzaamheden in verscheidene submodules onderhanden.
We kunnen eenvoudig al het werk in al onze submodules stashen.

[source,console]

$ git submodule foreach git stash Entering CryptoLibrary No local changes to save Entering DbConnector Saved working directory and index state WIP on stable: 82d2ad3 Merge from origin/stable HEAD is now at 82d2ad3 Merge from origin/stable

Daarna kunnen we een nieuwe branch maken en ernaar switchen in al onze submodules.

[source,console]

$ git submodule foreach git checkout -b featureA Entering CryptoLibrary Switched to a new branch featureA Entering DbConnector Switched to a new branch featureA

Je ziet waar het naartoe gaat.
Een heel nuttig ding wat je kunt doen is een mooie unified diff maken van wat er gewijzigd is in je hoofdproject alsmede al je subprojecten.

[source,console]

$ git diff; git submodule foreach git diff Submodule DbConnector contains modified content diff --git a/src/main.c b/src/main.c index 210f1ae..1f0acdc 100644 --- a/src/main.c + b/src/main.c @@ -245,6 +245,8 @@ static int handle_alias(int argcp, const char **argv)

commit_pager_choice();

+ url = url_decode(url_orig);

+ /* build alias_argv */ alias_argv = xmalloc(sizeof(*alias_argv) * (argc + 1)); alias_argv[0] = alias_string + 1; Entering DbConnector diff --git a/src/db.c b/src/db.c index 1aaefb6..5297645 100644 --- a/src/db.c + b/src/db.c @@ -93,6 +93,11 @@ char *url_decode_mem(const char *url, int len) return url_decode_internal(&url, len, NULL, &out, 0); }

+char *url_decode(const char *url) +{ + return url_decode_mem(url, strlen(url)); +}

+ char *url_decode_parameter_name(const char **query) { struct strbuf out = STRBUF_INIT;

Hier kunnen we zien dat we een functie aan het definieren zijn in een submodule en dat we het in het hoofdproject aanroepen.
Dit is overduidelijk een versimpeld voorbeeld, maar hopelijk geeft het je een idee van hoe dit handig kan zijn.

===== Bruikbare aliassen

Je wilt misschien een aantal aliassen maken voor een aantal van deze commando's omdat ze redelijk lang kunnen zijn en je geen configuratie opties voor de meeste van deze kunt instellen om ze standaard te maken.
We hebben het opzetten van Git aliassen in <<_git_aliases>> behandeld, maar hier is een voorbeeld van iets wat je misschien zou kunnen opzetten als je van plan bent veel met submodules in Git te werken.

[source,console]

$ git config alias.sdiff !"git diff && git submodule foreach git diff" $ git config alias.spush push --recurse-submodules=on-demand $ git config alias.supdate submodule update --remote --merge

Op deze manier kan je eenvoudig `git supdate` aanroepen als je je submodules wilt updaten, of `git spush` om te pushen met controle op afhankelijkheden op de submodule.

==== Problemen met submodules

Submodules gebruiken is echter niet zonder nukken.

Bijvoorbeeld het switchen van branches met daarin submodulen kan nogal listig zijn.
Als je een nieuwe branch maakt, daar een submodule toevoegt, en dan terug switcht naar een branch zonder die submodule, heb je de submodule directory nog steeds als een untrackt directory.

[source,console]

$ git checkout -b add-crypto Switched to a new branch add-crypto

$ git submodule add https://github.com/chaconinc/CryptoLibrary Cloning into CryptoLibrary…​ …​

$ git commit -am adding crypto library [add-crypto 4445836] adding crypto library 2 files changed, 4 insertions(+) create mode 160000 CryptoLibrary

$ git checkout master warning: unable to rmdir CryptoLibrary: Directory not empty Switched to branch master Your branch is up-to-date with origin/master.

$ git status On branch master Your branch is up-to-date with origin/master.

Untracked files: (use "git add <file>…​" to include in what will be committed)

CryptoLibrary/

nothing added to commit but untracked files present (use "git add" to track)

Die directory weghalen is niet moeilijkm maar het kan nogal verwarrend zijn om hem daar te hebben.
Als je het weghaalt en dan weer terug switcht naar de branch die deze submodule heeft, zal je `submodule update --init` moeten aanroepen om het weer te vullen.

[source,console]

$ git clean -ffdx Removing CryptoLibrary/

$ git checkout add-crypto Switched to branch add-crypto

$ ls CryptoLibrary/

$ git submodule update --init Submodule path CryptoLibrary: checked out b8dda6aa182ea4464f3f3264b11e0268545172af

$ ls CryptoLibrary/ Makefile includes scripts src

Alweer, niet echt moeilijk, maar het kan wat verwarring scheppen.

Het andere grote probleem waar veel mensen tegenaan lopen betreft het omschakelen van subdirectories naar submodules.
Als je files aan het tracken bent in je project en je wilt ze naar een submodule verplaatsen, moet je voorzichtig zijn omdat Git anders erg boos op je gaat worden.
Stel dat je bestanden hebt in een subdirectory van je project, en je wilt er een submodule van maken.
Als je de subdirectory verwijdert en dan `submodule add` aanroept, zal Git tegen je schreeuwen:

[source,console]

$ rm -Rf CryptoLibrary/ $ git submodule add https://github.com/chaconinc/CryptoLibrary CryptoLibrary already exists in the index

Je moet de `CryptoLibrary` directory eerst unstagen.
Daarna kan je de submodule toevoegen:

[source,console]

$ git rm -r CryptoLibrary $ git submodule add https://github.com/chaconinc/CryptoLibrary Cloning into CryptoLibrary…​ remote: Counting objects: 11, done. remote: Compressing objects: 100% (10/10), done. remote: Total 11 (delta 0), reused 11 (delta 0) Unpacking objects: 100% (11/11), done. Checking connectivity…​ done.

Stel je nu voor dat je dit in en branch zou doen.
Als je naar een branch terug zou switchen waar deze bestanden nog steeds in de actuele tree staan in plaats van in een submodule - krijg je deze fout:

[source,console]

$ git checkout master error: The following untracked working tree files would be overwritten by checkout: CryptoLibrary/Makefile CryptoLibrary/includes/crypto.h …​ Please move or remove them before you can switch branches. Aborting

Je kunt forceren om de switch te maken met `checkout -f`, maar wees voorzichtig dat je geen onbewaarde gegevens daar hebt staan omdat deze kunnen worden overschreven met dit commando.

[source,console]

$ git checkout -f master warning: unable to rmdir CryptoLibrary: Directory not empty Switched to branch master

Daarna, als je weer terug switcht, krijg je om de een of andere reden een lege `CryptoLibrary` directory en `git submodule update` zou hier ook geen oplossing voor kunnen bieden.
Je zou misschien naar je submodule directory moeten gaan en een `git checkout .` aanroepen om al je bestanden terug te krijgen.
Je zou dit in een `submodule foreach` script kunnen doen om het voor meerdere submodules uit te voeren.

Het is belangrijk om op te merken dat submodules tegenwoordig al hun Git data in de `.git` directory van het hoogste project opslaan, dus in tegenstelling tot oudere versies van Git, leidt het vernietigen van een submodule directory niet tot verlies van enig commit of branches die je had.

Met al deze gereedschappen, kunnen submodules een redelijk eenvoudig en effectieve manier zijn om een aantal gerelateerde maar toch aparte projecten tegelijk te ontwikkelen.


[[_bundling]]
////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Bundelen

Alhoewel we de reguliere manieren om Git data over een netwerk te transporteren al behandeld hebben (HTTP, SSH, etc), is er eigenlijk nog een weinig gebruikte manier om dit te doen, maar die wel erg nuttig kan zijn.

Git is in staat om zijn gegevens te ``bundelen'' (bundling) in een enkel bestand.
Dit kan handig zijn in verscheidene situaties.
Misschien is je netwerk uit de lucht en je wilt wijzigingen naar je medewerkers sturen.
Misschien werk je ergens buiten de deur en heb je om beveiligingsredenen geen toegang tot het lokale netwerk.
Misschiens is je wireless/ethernet kaart gewoon kapot.
Misschien heb je op dat moment geen toegang tot een gedeelde server, wil je iemand updates mailen en je wilt niet 40 commits via een `format-patch` sturen.

Dit is waar het `git bundle` commando behulpzaam kan zijn.
Het `bundle` commando pakt alles wat normaalgesproken over het netwerk zou worden gepusht met een `git push` commando in een binair bestand die je naar iemand kunt mailen of op een flash drive kunt bewaren, en dan uitpakken in de andere repository.

Laten we een eenvoudig voorbeeld bekijken.
Laten we zeggen dat je een repository met twee commits hebt:

[source,console]

$ git log commit 9a466c572fe88b195efd356c3f2bbeccdb504102 Author: Scott Chacon <schacon@gmail.com> Date: Wed Mar 10 07:34:10 2010 -0800

second commit

commit b1ec3248f39900d2a406049d762aa68e9641be25 Author: Scott Chacon <schacon@gmail.com> Date: Wed Mar 10 07:34:01 2010 -0800

first commit
Als je deze repository naar iemand wilt sturen en je hebt geen toegang tot een repository om naar te pushen, of deze gewoon niet wil inrichten, kan je het bundelen met `git bundle create`.

[source,console]

$ git bundle create repo.bundle HEAD master Counting objects: 6, done. Delta compression using up to 2 threads. Compressing objects: 100% (2/2), done. Writing objects: 100% (6/6), 441 bytes, done. Total 6 (delta 0), reused 0 (delta 0)

Nu heb je een bestand die `repo.bundle` heet die alle gegevens heeft die nodig zijn om de `master`-branch van de repository weer op te bouwen.
Met het `bundle` commando moet je elke referentie of een reeks van commits opgeven die je erin wilt betrekken.
Als de bedoeling is dat deze elders wordt gekloond, moet je ook HEAD als referentie meenemen zoals we hier gedaan hebben.

Je kunt dit `repo.bundle` bestand naar iemand mailen, of op een USB schijf zetten en het even langsbrengen.

Aan de andere kant, stel dat je dit `repo.bundle` bestand gestuurd krijgt en je wilt aan het project werken.
Je kunt dan van dit binaire bestand naar een directory klonen, vergelijkbaar met hoe je dit zou doen vanaf een URL.

[source,console]

$ git clone repo.bundle repo Cloning into repo…​ …​ $ cd repo $ git log --oneline 9a466c5 second commit b1ec324 first commit

Als je de HEAD niet in de referenties meeneemt, moet je ook `-b master` opgeven of welke branch er dan ook in zit, omdat het anders niet duidelijk is welke branch er moet worden uitgechecked.

Laten we nu zeggen dat je drie commits hierop doet en de nieuwe commits terug wilt sturen via een bundel op een USB stick of e-mail.

[source,console]

$ git log --oneline 71b84da last commit - second repo c99cf5b fourth commit - second repo 7011d3d third commit - second repo 9a466c5 second commit b1ec324 first commit

Eerst moeten we de reeks van commits vaststellen die we in de bundel willen stoppen.
In tegenstelling tot de netwerk protocollen die de minimum set van gegevens die verstuurd moeten worden voor ons kunnen bepalen, moeten we het hier handmatig uitvinden.
Je kunt natuurlijk hier hetzelfde doen en de gehele repository bundelen, en dat zou werken, maar het is beter om alleen het verschil te bundelen - alleen de drie commits die we zojuist lokaal gemaakt hebben.

Om dat te doen, moet je het verschil berekenen.
Zoals we hebben beschreven in <<_commit_ranges>>, kan je op verschillende manieren een reeks van commits aangeven.
Om de drie commits te krijgen die we in onze master branch hebben die niet in de originele gekloonde branch zaten, kunnen we zoiets als `origin/master..master` of `master ^origin/master` gebruiken.
Je kunt dat verifiëren met het `log` commando.

[source,console]

$ git log --oneline master ^origin/master 71b84da last commit - second repo c99cf5b fourth commit - second repo 7011d3d third commit - second repo

Dus nu dat we de lijst met commits hebben die we in de bundel willen pakken, laten we ze dan ook gaan bundelen.
We doen dat met het `git bundle create` commando, waaraan we een bestandsnaam meegeven waar we onze bundel in willen pakken en de reeks met commits die we erin willen gaan doen.

[source,console]

$ git bundle create commits.bundle master ^9a466c5 Counting objects: 11, done. Delta compression using up to 2 threads. Compressing objects: 100% (3/3), done. Writing objects: 100% (9/9), 775 bytes, done. Total 9 (delta 0), reused 0 (delta 0)

Nu hebben we een `commits.bundle` bestand in onze directory.
Als we deze naar onze partner sturen, kan zij deze importeren in de orginele repository, zelfs als daar in de tussentijd weer meer werk aan gedaan is.

Als ze de bundel krijgt, kan ze deze inspecteren om te zien wat erin zit voordat ze deze in haar repository importeert.
Het eerste commando is het `bundle verify` commando, dat controleert of het bestand een geldige Git bundel is en dat je alle benodigde voorouders hebt om het op de juiste wijze te importeren.

[source,console]

$ git bundle verify ../commits.bundle The bundle contains 1 ref 71b84daaf49abed142a373b6e5c59a22dc6560dc refs/heads/master The bundle requires these 1 ref 9a466c572fe88b195efd356c3f2bbeccdb504102 second commit ../commits.bundle is okay

Als degene die de bundel heeft aangemaakt van alleen de laatste twee commits die ze hadden gedaan, in plaats van alle drie, zou de originele repository niet in staat zijn geweest om deze te importeren, omdat de benodigde historie ontbreekt.
Het `verify` commando zou dan iets als dit hebben laten zien:

[source,console]

$ git bundle verify ../commits-bad.bundle error: Repository lacks these prerequisite commits: error: 7011d3d8fc200abe0ad561c011c3852a4b7bbe95 third commit - second repo

Echter, onze eerste bundel is geldig, dus we kunnen de commits ervan gaan fetchen.
Als je zou willen zien welke branches er uit de bundel kunnen worden geïmporteerd, is er ook een commando die alleen de heads laat zien:

[source,console]

$ git bundle list-heads ../commits.bundle 71b84daaf49abed142a373b6e5c59a22dc6560dc refs/heads/master

Het `verify` sub-commando laat je ook de heads zien.
Het belangrijkste is om te zien wat er naar binnen gepulld kan worden, zodat je het `fetch` of `pull` commando kunt gebruiken om de commits van deze bundel kunt importeren.
Hier gaan we de 'master' branch van de bundel naar een branch met de naam 'other-master' in onze repository fetchen:

[source,console]

$ git fetch ../commits.bundle master:other-master From ../commits.bundle * [new branch] master → other-master

Nu kunnen we zien dat we de commits op de 'other-master'-branch hebben geïmporteerd zowel als elke andere commit die we in de tussentijd in onze eigen 'master'-branch hebben gedaan.

[source,console]

$ git log --oneline --decorate --graph --all * 8255d41 (HEAD, master) third commit - first repo | * 71b84da (other-master) last commit - second repo | * c99cf5b fourth commit - second repo | * 7011d3d third commit - second repo |/ * 9a466c5 second commit * b1ec324 first commit

Dus `git bundle` kan erg handig zijn voor het delen of netwerk-achtige operaties te doen als je niet de beschikking hebt over een geschikt netwerk of gedeelde repository om te gebruiken.


[[_replace]]
////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Vervangen

Zoals we eerder hebben benadrukt zijn de objecten in de database van Git onwijzigbaar, maar Git heeft een interessante manier om te _doen alsof_ je objecten in de database vervangt met andere objecten.

Het `replace` commando laat je een object in Git opgeven en te zeggen dat "elke keer als je _dit_ object ziet, doe alsof het dit een _ander_ object is".
Dit is het nuttigst voor het vervangen van een commit in je historie met een andere zonder de gehele historie te vervangen met, laten we zeggen, `git filter-branch`.

Bijvoorbeeld, stel 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 "vervangen".
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:

[source,console]

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

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

image::images/replace1.png[]

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.

[source,console]

$ 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

image::images/replace2.png[]

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

[source,console]

$ 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 gepubliceerd.
Nu is het moeilijkere gedeelte het terugsnoeien van onze recente historie zodat deze kleiner wordt.
We moeten een overlapping maken op zo'n manier dat we een commit kunnen vervangen in een repository die een gelijke commit heeft, dus we gaan deze afkappen tot alleen commits vier en vijf (dus de vierde commit overlapt).

[source,console]

$ 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.

[source,console]

$ 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 dan 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 <<_plumbing_porcelain>>.
=====

image::images/replace3.png[]

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`):

[source,console]

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

image::images/replace4.png[]

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 dan fetchen:

[source,console]

$ 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.

[source,console]

$ 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:

[source,console]

$ git replace 81a708d c6e1e95

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

[source,console]

$ 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 mogen verwachten.

image::images/replace5.png[]

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:

[source,console]

$ 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:

[source,console]

$ 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 (als iedereen toch beide histories zou gaan downloaden, waarom zouden we ze dan gaan splitsen) maar het kan handig zijn in andere omstandigheden.


[[_credential_caching]]
////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Het opslaan van inloggegevens

(((credentials)))
(((git commando's, credential)))
Als je het SSH transport gebruikt om verbinding te maken met remotes, is het mogelijk om een sleutel te hebben zonder wachtwoord, wat je in staat stelt veilig gegevens uit te wisselen zonder je gebruikersnaam en wachtwoord in te typen.
Dit is echter niet mogelijk met de HTTP protocollen - elke connectie heeft een gebruikersnaam en wachtwoord nodig.
Het wordt zelfs lastiger voor systemen met twee-factor authenticatie, waar het token dat je gebruikt voor een wachtwoord willekeurig wordt gegenereerd en onuitspreekbaar is.

Gelukkig heeft Git een credentials systeem die ons daarbij kan helpen.
Git heeft standaard een aantal opties in de aanbieding:

* De standaard is om helemaal niets op te slaan.
  Elke verbinding wordt je gevraagd om je gebruikersnaam en wachtwoord.
* De ``cache'' modus houdt deze gegevens voor een bepaalde tijd in het geheugen.
  Geen van de wachtwoorden worden ooit op schijf opgeslagen, en ze worden na 15 minuten uit de cache verwijderd.
* De ``store'' modus bewaart deze gegevens in bestand in een leesbaar tekstformaat, en ze verlopen nooit.
  Dit houdt in dat totdat je je wachtwoord wijzigt op de Git host, je nooit meer je gegevens hoeft in te typen.
  Het nadeel van deze aanpak is dat je wachtwoorden onversleuteld bewaard worden in een gewoon bestand in je home directory.
* Als je een Mac gebruikt, wordt Git geleverd met een ``osxkeychain'' modus, waarbij de gegevens opgeslagen worden in een beveiligde sleutelring die verbonden is aan je systeem account.
  Deze methode bewaart je gegevens op schijf, en ze verlopen nooit, maar ze zijn versleuteld met het zelfde systeem dat de HTTPS certificaten en Safari auto-fills bewaart.
* Als je Windows gebruikt, kan je het hulpprogramma ``Git Credential Manager for Windows'' installeren.
  Dit is vergelijkbaar met het ``osxkeychain'' programma zoals hierboven beschreven, maar het gebruikt de Windows Credential Store om de gevoelige gegevens te beheren.
  Dit kan gevonden worden op https://github.com/Microsoft/Git-Credential-Manager-for-Windows[].

Je kunt een van deze methoden kiezen door een Git configuratie waarde in te stellen:

[source,console]

$ git config --global credential.helper cache

Een aantal van deze hulpprogramma's hebben opties.
De ``store'' helper accepteert een `--file <path>` argument, waarmee je kunt sturen waar het leesbare bestand wordt opgeslagen (standaard is `~/.git-credentials`).
De ``cache'' helper accepteert de `--timeout <seconds>` optie, die de tijdsduur wijzigt gedurende welke de daemon blijft draaien (standaard is dit ``900'', ofwel 15 minuten).
Hier is een voorbeeld van hoe je de ``store'' helper configureert met een eigen bestandsnaam:

[source,console]

$ git config --global credential.helper store --file ~/.my-credentials

Git staat het je zelfs toe om meerdere helpers te configureren.
Als Git op zoek gaat naar inloggegevens voor een specifieke host, zal Git ze in volgorde afvragen, en stoppen als het eerste antwoord wordt gegeven.
Bij het opslaan van de gegevens, zal Git de gebruikersnaam en wachtwoord naar *alle* helpers in de lijst sturen, en zij kunnen besluiten wat met deze gegevens te doen.
Hier is hoe een `.gitconfig` eruit zou kunnen zien als je een credentials bestand op een stickie zou hebben staan, maar de opslag in het geheugen zou willen gebruiken om wat typen te besparen als die stick niet ingeplugd is:

[source,ini]
helper = store --file /mnt/thumbdrive/.git-credentials
helper = cache --timeout 30000
==== Onder de motorkap

Hoe werkt dit nu allemaal?
Het basiscommando van Git voor het credential-helper systeem is `git credential`, wat een commando als argument neemt, en daarna meer invoer vanuit stdin.

Dit is misschien beter te begrijpen met een voorbeeld.
Laten we zeggen dat een credential helper geconfigureerd is, en de helper heeft gegevens bewaard voor `mygithost`.
Hier is een sessie die het ``fill'' commando gebruikt, wat wordt aangeroepen als Git probeert inloggegevens te vinden voor een host:

[source,console]

$ git credential fill <1> protocol=https <2> host=mygithost <3> protocol=https <4> host=mygithost username=bob password=s3cre7 $ git credential fill <5> protocol=https host=unknownhost

Username for https://unknownhost: bob Password for https://bob@unknownhost: protocol=https host=unknownhost username=bob password=s3cre7

<1> Dit is de commando regel die de interactie initieert.
<2> Git-credential gaat dan wachten op invoer van stdin.
    We geven het de dingen die we weten: het protocol en de hostnaam.
<3> Een blanco regel geeft aan dat de invoer compleet is, en het credential systeem moet nu antwoorden met wat het weet.
<4> Git-credential neemt het daarna over, en schrijft de stukken informatie het gevonden heeft naar stdout.
<5> Als er geen inloggegevens gevonden zijn, vraag Git de gebruiker om de gebruikersnaam en wachtwoord en stelt die ter beschikking aan de stdout van de aanroeper (hier zitten ze verbonden met dezelfde console).

Het credential systeem roept feitelijk een programma aan dat los staat van Git zelf; welke dat is en hoe hangt af van de waarde die is ingevuld bij `credential.helper`.
Deze kan verschillende vormen aannemen:

[options="header"]
|======
| Configuratie waarde | Gedrag
| `foo` | Roept `git-credential-foo` aan
| `foo -a --opt=bcd` | Roept `git-credential-foo -a --opt=bcd` aan
| `/absolute/path/foo -xyz` | Roept `/absolute/path/foo -xyz` aan
| `!f() { echo "password=s3cre7"; }; f` | Code na `!` wordt in shell geëvalueerd
|======

Dus de helpers die hierboven zijn beschreven heten eigenlijk `git-credential-cache`, `git-credential-store`, en zo voorts, en we kunnen ze configureren om commando-regel argumenten te accepteren.
De algemene vorm voor dit is ``git-credential-foo [args] <actie>.''
Het stdin/stdout protocol is dezelfde als git-credential, maar deze gebruiken een iets andere set acties:

* `get` is een verzoek voor een gebruikersnaam/wachtwoord paar.
* `store` is een verzoek om een groep van inloggegevens in het geheugen van de helper op te slaan.
* `erase` verwijder de inloggegevens voor de opgegeven kenmerken uit het geheugen van deze helper.

Voor de `store` en `erase` acties, is geen antwoord nodig (Git negeert deze in elk geval).
Voor de `get` actie echter is Git zeer geïntereseerd in het antwoord van de helper.
Als de helper geen zinnig antwoord kan geven, kan het simpelweg stoppen zonder uitvoer, maar als het wel een antwoord heeft, moet het de gegeven informatie aanvullen met de gegevens die het heeft opgeslagen.
De uitvoer wordt behandeld als een reeks van toewijzigs-opdrachten; alles wat wordt aangereikt zal wat Git hierover al weet vervangen.

Hier is het zelfde voorbeeld als hierboven, maar git-credential wordt overgeslagen en er wordt direct naar git-credential-store gegaan:

[source,console]

$ git credential-store --file ~/git.store store <1> protocol=https host=mygithost username=bob password=s3cre7 $ git credential-store --file ~/git.store get <2> protocol=https host=mygithost

username=bob <3> password=s3cre7

<1> Hier vertellen we `git-credential-store` om wat inloggegevens te bewaren: de gebruikersnaam ``bob'' en het wachtwoord ``s3cre7'' moeten worden gebruikt as `https://mygithost` wordt benaderd.
<2> Nu gaan we deze inloggegevens ophalen.
    We geven de delen van de verbinding die we al weten (`https://mygithost`) en een lege regel.
<3> De `git-credential-store` antwoordt met de gebruikersnaam en wachtwoord die we hierboven hebben opgeslagen.

Hier is hoe het `~/git.store` bestand eruit zal zien:

[source]
Het is niet meer dan een reeks regels, die elk een van inloggegevens voorziene URL bevat.
De `osxkeychain` en `wincred` helpers gebruiken het eigen formaat van hun eigen achterliggende opslag, terwijl `cache` zijn eigen 'in-memory' formaat gebruikt (wat geen enkel ander proces kan lezen).

==== Een eigen inloggegevens cache

Gegeven dat `git-credential-store` en zijn vriendjes programma's zijn die los staan van Git, is het geen grote stap om te beseffen dat _elk_ programma een Git credential helper kan zijn.
De helpers die bij Git worden geleverd dekken veel gewone gebruikssituaties, maar niet alle.
Bijvoorbeeld, stel nu dat je team een aantal inloggegevens hebben die met het hele team worden gedeeld, misschien om te deployen.
Deze worden opgeslagen in een gedeelde directory, maar je wilt ze niet naar je eigen credential opslagplaats kopiëren, omdat ze vaak veranderen.
Geen van de bestaande helpers kan hierin voorzien; laten we eens kijken hoeveel moeite het kost om er zelf een te schrijven.
Er zijn een aantal sleutelkenmerken die dit programma moet hebben:

. De enige actie waar we aandacht aan moeten besteden is `get`; `store` en `erase` zijn schrijf-acties, dus we negeren deze en sluiten gewoon af als ze worden ontvangen.
. Het bestandsformaat van het gedeelde credential bestand is dezelfde als die wordt gebruikt door `git-credential-store`.
. De locatie van dat bestand is redelijk standaard, maar we moeten toestaan dat de gebruiker een aangepast pad doorgeeft, voor het geval dat.

Nogmaals, we schrijven deze extensie in Ruby, maar een andere willekeurige taal werkt ook, zolang Git het uiteindelijke product maar kan aanroepen.
Hier is de volledige broncode van onze nieuwe credential helper:

[source,ruby]
--------
#!/usr/bin/env ruby

require 'optparse'

path = File.expand_path '~/.git-credentials' (1)
OptionParser.new do |opts|
    opts.banner = 'USAGE: git-credential-read-only [options] <action>'
    opts.on('-f', '--file PATH', 'Specify path for backing store') do |argpath|
        path = File.expand_path argpath
    end
end.parse!

exit(0) unless ARGV[0].downcase == 'get' (2)
exit(0) unless File.exists? path

known = {} (3)
while line = STDIN.gets
    break if line.strip == ''
    k,v = line.strip.split '=', 2
    known[k] = v
end

File.readlines(path).each do |fileline| (4)
    prot,user,pass,host = fileline.scan(/^(.*?):\/\/(.*?):(.*?)@(.*)$/).first
    if prot == known['protocol'] and host == known['host'] and user == known['username'] then
        puts "protocol=#{prot}"
        puts "host=#{host}"
        puts "username=#{user}"
        puts "password=#{pass}"
        exit(0)
    end
end

--------

<1> Hier parsen we de commando-regel opties, waarbij we de gebruiker het invoerbestand kunnen laten aangeven. De standaardwaarde is `~/.git-credentials`.
<2> Dit programma geeft alleen antwoord als de actie `get` is, en het achterliggende bestand bestaat.
<3> In deze lus wordt stdin gelezen tot de eerste blanco regel wordt bereikt.
    De invoergegevens worden opgeslagen in de `known` hash voor later gebruik.
<4> In deze lus wordt de inhoud van het achterliggende bestand gelezen op zoek naar passende inhoud.
    Als het protocol en de host van `known` gelijk is aan deze regel, drukt het programma de resultaten af op stdout en stopt.

We zullen onze helper als `git-credential-read-only` opslaan, zetten het ergens in onze `PATH` en maken het uitvoerbaar.
Hier is hoe een interactieve sessie eruit zou zien:

[source,console]

$ git credential-read-only --file=/mnt/shared/creds get protocol=https host=mygithost

protocol=https host=mygithost username=bob password=s3cre7

Omdat de naam met ``git-'' begint, kunnen we de eenvoudige syntax voor de configuratie waarde gebruiken:

[source,console]

$ git config --global credential.helper read-only --file /mnt/shared/creds

Zoals je kunt zien, is het uitbreiden van dit systeem redelijk eenvoudig, en we kunnen een aantal gebruikelijke problemen voor jou en je team oplossen.


=== Samenvatting

Je hebt een aantal geavanceerde instrumenten gezien die je in staat stellen je commits en staging area met grotere precisie te manipuleren.
Als je problemen ziet, zou je in staat moeten zijn om eenvoudig uit te vinden welke commit deze heeft geïntroduceerd, en wie dit gedaan heeft.
Als je subprojecten wilt gebruiken in je project, heb je geleerd hoe deze een plaats te geven.
Vanaf nu zou je in staat moeten zijn om de meeste dingen in Git te doen die je dagelijks op de command line nodig hebt en je er gemakkelijk bij voelen.


[[ch08-customizing-git]]
////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
== Git aanpassen

Tot dusver hebben we de grondbeginselen behandeld van de werking van Git en hoe het te gebruiken, en we hebben een aantal instrumenten de revue laten passeren die Git je biedt om het eenvoudig en effectief te gebruiken.
In dit hoofdstuk zullen we laten zien hoe je Git op een meer aangepaste manier kan laten werken, door een aantal belangrijke configuratie instellingen te laten zien en het hooks systeem.
Met deze gereedschapppen is het eenvoudig om Git te laten werken op de manier waarop jij, je bedrijf of je groep het nodig heeft.

[[_git_config]]
////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Git configuratie

(((git commando's, config)))
Zoals je al kort heb kunnen zien in <<ch01-getting-started>>, kan je Git configuratie settings aangeven met het `git config` commando.
Een van de eerste dingen die je hebt gedaan was het inrichten van je naam en e-mail adres:

[source,console]

$ git config --global user.name "John Doe" $ git config --global user.email johndoe@example.com

Nu gaan we je een aantal van de meer interessante opties laten zien die je op deze manier kunt instellen om jouw Git gebruik aan te passen.

Eerst, een korte terugblik: Git gebruikt een reeks van configuratie bestanden om te bepalen welke niet standaard gedragingen je hebt aangevraagd.
De eerste plek waar Git naar kijkt voor deze waarden is in een `/etc/gitconfig` bestand, welke de waarden bevat voor elke gebruiker op het systeem en al hun repositories.
Als je de optie `--system` doorgeeft aan `git config`, leest en schrijft deze naar dit specifieke bestand.

De volgende plaats waar Git kijkt is het`~/.gitconfig` (of `~/.config/git/config`) bestand, die voor elke gebruiker anders kan zijn.
Je kunt Git naar dit bestand laten lezen en schrijven door de `--global` optie door te geven.

Als laatste kijkt Git naar configuratie waarden in het configuratie bestand in de Git directory (`.git/config`) van de repository die je op dat moment gebruikt.
Deze waarden zijn gebonden aan alleen die ene repository, en vertegenwoordigen het doorgeven van de `--local` optie aan `git config`.
(Als je geen waarde doorgeeft voor het niveau, is dit de standaardwaarde.)

Elk van deze ``levels'' (systeem, globaal, lokaal) overtroeft de waarden van de vorige, dus waarden in ` .git/config` overtroeven die in `/etc/gitconfig` bijvoorbeeld.

[NOTE]
====
De configuratie bestanden van Git zijn gewone tekstbestanden, dus je kunt deze waarden ook aanpassen door het bestand handmatig te bewerken en de juiste syntax te gebruiken.
Het is echter over het algemeen eenvoudiger om het `git config` commando te gebruiken.
====

==== Basis werkstation configuratie

De configuratie opties die door Git worden herkend vallen uiteen in twee categoriën: de kant van het werkstation en die van de server.
De meerderheid van de opties zitten aan de kant van het werkstation -- welke jouw persoonlijke voorkeuren voor de werking inrichten.
Er worden heel, _heel_ erg veel configuratie opties ondersteund, maar een groot gedeelte van deze zijn alleen nuttig in bepaalde uitzonderingsgevallen.
We zullen alleen de meest voorkomende en nuttigste hier behandelen.
Als je een lijst wilt zien van alle opties die jouw versie van Git ondersteunt, kan je dit aanroepen:

[source,console]

$ man git-config

Dit commando geeft je een lijst van alle beschikbare opties met nogal wat detail informatie.
Je kunt dit referentie-materiaal ook vinden op http://git-scm.com/docs/git-config.html[].

===== `core.editor`

((($EDITOR)))((($VISUAL, zie $EDITOR)))
Standaard gebruikt Git hetgeen je hebt ingesteld als je standaard tekstverwerker (`$VISUAL` of `$EDITOR`) en anders valt het terug op de `vi` tekstverwerker om jouw commit en tag berichten te maken en te onderhouden.
Om deze standaard naar iets anders te verandereen, kan je de `core.editor` instelling gebruiken:

[source,console]

$ git config --global core.editor emacs

Nu maakt het niet meer uit wat je standaard shell editor is, Git zal Emacs opstarten om je berichten te wijzigen.

===== `commit.template`

(((commit templates)))
Als je dit instelt op het pad van een bestand op je systeem, zal Git dat bestand gebruiken als het standaard bericht als je commit.
De waarde van het maken van een standaard bericht voorbeeld is dat je dit kan gebruiken om jezelf (of anderen) eraan kan herinneren om de juiste formattering en stijl te gebruiken voor het maken van een commit-bericht.

Bijvoorbeeld, stel dat je een sjabloon bestand op `~/.gitmessage.txt` hebt gemaakt dat er zo uitziet:

[source,text]

Subject line (try to keep under 50 characters)

Multi-line description of commit, feel free to be detailed.

Zie hoe dit sjabloon de committer eraan herinnert om de onderwerpregel kort te houden (ten behoeve van de `git log --oneline`-uitvoer), verdere details eronder te vermelden en om naar een issue of bug-tracker systeem (mocht die bestaan) referentie te verwijzen.

Om Git te vertellen dat het dit als het standaard bericht moet gebruiken dat in je tekstverwerker verschijnt als je `git commit` aanroept, zet dan de `commit.template` configuratie waarde:

[source,console]

$ git config --global commit.template ~/.gitmessage.txt $ git commit

Dan zal je tekstverwerker het volgende als je commit bericht sjabloon gebruiken als je gaat committen:

[source,text]

Subject line (try to keep under 60 characters)

Multi-line description of commit, feel free to be detailed.

# Please enter the commit message for your changes. Lines starting # with # will be ignored, and an empty message aborts the commit. # On branch master # Changes to be committed: # (use "git reset HEAD <file>…​" to unstage) # # modified: lib/test.rb # ~ ~ ".git/COMMIT_EDITMSG" 14L, 297C

Als je team een commit-bericht voorschrift heeft, dan zal het inrichten van een sjabloon voor dat voorschrift op jouw systeem en het inrichten van Git om dit standaard te gebruiken helpen met het vergroten van de kans dat dat voorschrift ook daadwerkelijk wordt opgevolgd.

===== `core.pager`

(((pager)))
Het instellen van deze waarde bepaalt welke pagineerhulp wordt gebruikt als Git uitvoer als `log` en `diff` gaat pagineren.
Je kunt dit op `more` of jouw favoriete pagineerhulp instellen (standaard is het `less`), of je kunt het uitzetten door een lege waarde te geven:

[source,console]

$ git config --global core.pager ''

Als je dat aanroept, zal Git de gehele uitvoer van alle commando's tonen, onafhankelijk van de lengte van deze uitvoer.

===== `user.signingkey`

(((GPG)))
Als je getekende geannoteerde tags aanmaakt (zoals behandeld in <<_signing>>), zal het inrichten van je GPG handtekening als configuratie setting dingen eenvoudiger maken.
Stel jouw sleutel ID als volgt in:

[source,console]

$ git config --global user.signingkey <gpg-key-id>

Nu kan je tags tekenen zonder elke keer je sleutel op te hoeven geven als je het `git tag` commando aanroept:

[source,console]

$ git tag -s <tag-name>

===== `core.excludesfile`

(((excludes)))(((.gitignore)))
Je kunt patronen in het `.gitignore` bestand van je project zetten om Git deze niet als untracked bestanden te laten beschouwen of deze zal proberen te stagen als je `git add` op ze aanroept, zoals beschreven in <<_ignoring>>.

Maar soms wil je bepaalde bestanden negeren voor alle repositories waar je in werkt.
Als je computer onder Mac OS X draait, ben je waarschijnlijk bekend met `.DS_Store` bestanden.
Als je voorkeurs-tekstverwerker Emacs of Vim is, zullen bestanden die eindigen op een `~` of `.swp` je bekend voorkomen.

Deze instelling laat je een soort globale `.gitingore` bestand aanmaken.
Als je een `~/.gitignore_global` bestand aanmaakt met deze inhoud:

[source,ini]

~ ..swp .DS_Store

...en je roept `git config --global core.excludesfile ~/.gitignore_global` aan, zal Git je nooit meer lastig vallen over deze bestanden.

===== `help.autocorrect`

(((autocorrect)))
Als je een typefout maakt in een commando, laat het je iets als dit zien:

[source,console]

$ git chekcout master git: chekcout is not a git command. See git --help.

Did you mean this? checkout

Git probeert behulpzaam uit te vinden wat je bedoeld zou kunnen hebben, maar weigert het wel dit uit te voeren.
Als je `help.autocorrect` op 1 instelt, zal Git het commando daadwerkelijk voor je uitvoeren:

[source,console]

$ git chekcout master WARNING: You called a Git command named chekcout, which does not exist. Continuing under the assumption that you meant checkout in 0.1 seconds automatically…​

Merk het ``0.1 seconden'' gedoe op. `help.autocorrect` is eigenlijk een integer waarde die tienden van een seconde vertegenwoordigt.
Dus als je het op 50 zet, zal Git je 5 seconden geven om van gedachten te veranderen voordat het automatisch gecorrigeerde commando wordt uitgevoerd.

==== Kleuren in Git

(((kleur)))
Git ondersteunt gekleurde terminal uitvoer volledig, wat enorm helpt in het snel en eenvoudig visueel verwerken van de uitvoer van commando's.
Een aantal opties kunnen je helpen met het bepalen van jouw voorkeurs-kleuren.

===== `color.ui`

Git geeft automatisch de meeste van haar uitvoer in kleur weer, maar er is een hoofdschakelaar als dit gedrag je niet bevalt.
Om alle kleuren uitvoer van Git uit te zetten, voer je dit uit:

[source,console]

$ git config --global color.ui false

De standaard waarde is `auto`, wat de uitvoer kleur geeft als het direct naar een terminal wordt gestuurd, maar laat de kleuren-stuurcodes achterwege als de uitvoer naar een pipe of een bestand wordt omgeleid.

Je kunt het ook instellen op `always` om het onderscheid tussen terminals en pipes te negeren.
Je zult dit zelden willen in de meeste gevallen, als je kleuren-stuurcodes in je omgeleide uitvoer wilt, kan je in plaats daarvan een `--color` vlag aan het Git commando doorgeven om het te dwingen om kleurcodes te gebruiken.
De standaard instelling is vrijwel altijd hetgeen je zult willen.

===== `color.*`

Als je meer controle wilt hebben over welke commando's gekleurd worden en hoe, heeft Git argument-specifieke kleuren-instellingen.
Elk van deze kan worden gezet op `true`, `false` of `always`:

  color.branch
  color.diff
  color.interactive
  color.status

Daarenboven heeft elk van deze specifiekere instellingen die je kunt gebruiken om specifieke kleuren voor delen van de uitvoer te bepalen, als je elke kleur zou willen herbepalen.
Bijvoorbeeld, om de meta-informatie in je diff uitvoer op een blauwe voorgrond, zwarte achtergrond en vetgedrukte tekst in te stellen, kan je het volgende uitvoeren:

[source,console]

$ git config --global color.diff.meta "blue black bold"

Je kunt de kleur instellen op elk van de volgende waarden: `normal`, `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, of `white`.
Als je een attribuut als vetgedrukt (bold) in het vorige voorbeeld wilt, kan je kiezen uit `bold`, `dim` (minder helder), `ul` (onderstrepen, underline), `blink` (knipperen), en `reverse` (verwissel voor- en achtergrond).

[[_external_merge_tools]]
==== Externe merge en diff tools

(((mergetool)))(((difftool)))
Hoewel Git een interne implementatie van diff heeft, dat is wat we hebben laten zien in dit boek, kan je een externe tool inrichten.
Je kunt ook een grafische merge-conflict-oplosgereedschap inrichten in plaats van het handmatig oplossen van de conflicten.
We zullen je het inrichten van het Perforce Visual Merge Tool (P4Merge) laten zien om je diffs en merge resoluties te laten doen, omdat het een fijne grafische tool is en omdat het is gratis.

Als je dit wilt proberen, P4Merge werkt op alle meest gebruikte platformen, dus je zou het op deze manier moeten kunnen doen.
We zullen in de voorbeelden pad-namen gaan gebruiken die op Mac en Linux systemen werken; voor Windows zal je `/usr/local/bin` in een uitvoerbaar pad moeten veranderen in jouw omgeving.

Om te beginnen, https://www.perforce.com/product/components/perforce-visual-merge-and-diff-tools[download P4Merge van Perforce].
Vervolgens ga je externe wrapper scripts maken om je commando's uit te voeren.
We zullen het Mac pad voor de executable gebruiken; op andere systemen zal het zijn waar je het `p4merge` binary bestand hebt geïnstalleerd.
Maak een merge wrapper script genaamd `extMerge` dat je binary aanroept met alle gegeven argumenten:

[source,console]

$ cat /usr/local/bin/extMerge #!/bin/sh /Applications/p4merge.app/Contents/MacOS/p4merge $*

De diff wrapper verifiëert dat er zeven argumenten worden doorgegeven en geeft twee ervan door aan je merge script.
Standaard geeft Git de volgende argumenten door aan het diff programma:

[source]

path old-file old-hex old-mode new-file new-hex new-mode

Omdat je alleen de `old-file` en `new-file` argumenten wilt, gebruik je het wrapper script om degene door te geven
 die je nodig hebt.

[source,console]

$ cat /usr/local/bin/extDiff !/bin/sh [ $ -eq 7 ] && /usr/local/bin/extMerge "$2" "$5"

Je moet ook ervoor zorgen dat deze scripts uitvoerbaar worden gemaakt:

[source,console]

$ sudo chmod +x /usr/local/bin/extMerge $ sudo chmod +x /usr/local/bin/extDiff

Nu kan je jouw configuratie bestand inrichten om jouw aangepaste merge oplossing en diff instrumenten worden gebruikt.
Dit vergt een aantal aangepaste instellingen: `merge.tool` om Git te vetellen welke strategie er gebruikt dient te worden, `mergetool.<tool>.cmd` om aan te geven hoe het commando aan te roepen, `mergetool.<tool>.trustExitCode` om Git te vertellen of de uitvoercode van dat programma een succesvolle merge oplossing aangeeft of niet, en `diff.external` om Git te vertellen welk commando het moet aanroepen voor diffs.
Dus je kunt kiezen om vier config commando's aan te roepen

[source,console]

$ git config --global merge.tool extMerge $ git config --global mergetool.extMerge.cmd \ extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED" $ git config --global mergetool.extMerge.trustExitCode false $ git config --global diff.external extDiff

of je kunt je `~/.gitconfig` bestand aanpassen door deze regels toe te voegen:

[source,ini]
tool = extMerge
cmd = extMerge "$BASE" "$LOCAL" "$REMOTE" "$MERGED"
trustExitCode = false
external = extDiff
Als dit ingericht is, zal, als je diff commando's als deze aanroept:

[source,console]

$ git diff 32d1776b1^ 32d1776b1

Git zal, in plaats van de diff uitvoer op de commando-regel te geven, P4Merge opstarten wat er ongeveer zo uit zal zien:

.P4Merge.
image::images/p4merge.png[P4Merge.]

Als je probeert om twee branches te mergen en je krijgt vervolgens merge conflicten, kan je het commando `git mergetool` aanroepen; Git zal P4Merge opstarten om je de conflicten middels dat grafische gereedschap op te laten lossen.

Het mooie van deze wrapper inrichting is dat je je diff en merge gereedschappen eenvoudig kan wijzigen.
Bijvoorbeeld, om je `extDiff` en `extMerge` gereedschappen te wijzigen om het KDiff3 gereedschap aan te roepen, is het enige wat je hoeft te doen het wijzigen van je `extMerge` bestand:

[source,console]

$ cat /usr/local/bin/extMerge #!/bin/sh /Applications/kdiff3.app/Contents/MacOS/kdiff3 $*

Nu zal Git het KDiff3 gereedschap gebruiken voor het bekijken van diffs en het oplossen van merge conflicten.

Git is voor-ingericht om een aantal andere merge-oplossings gereedschappen te gebruiken waarbij er geen noodzaak is om de cmd-configuratie op te zetten.
Om een lijst te zien van de ondersteunde gereedschappen, probeer eens dit:

[source,console]

$ git mergetool --tool-help git mergetool --tool=<tool> may be set to one of the following: emerge gvimdiff gvimdiff2 opendiff p4merge vimdiff vimdiff2

The following tools are valid, but not currently available: araxis bc3 codecompare deltawalker diffmerge diffuse ecmerge kdiff3 meld tkdiff tortoisemerge xxdiff

Some of the tools listed above only work in a windowed environment. If run in a terminal-only session, they will fail.

Als je niet geïntereseerd bent in het gebruik van KDiff3 voor diff, maar dit liever wilt gebruiken voor alleen het oplossen van merge conflicten, en het kdiff3 commando is op je pad, dan kan je dit aanroepen:

[source,console]

$ git config --global merge.tool kdiff3

Als je dit uitvoert in plaats van de `extMerge` en `extDiff` bestanden te maken, zal Git KDiff3 gebruiken voor merge conflict oplossingen en het reguliere Git diff gereedschap voor diffs.

==== Formatering en witruimtes

(((witruimtes, whitespace)))
Formatering en witruimte problemen zijn een paar van de meest frustrerende en subtiele problemen die veel ontwikkelaars tegenkomen wanneer ze samenwerken, vooral bij verschillende platforms.
Het is erg eenvoudig om middels patches of ander gedeeld werk om subtiele witruimte wijzigingen te introduceren omdat tekstverwerkers deze stilletjes invoeren, en als je bestanden ooit in aanraking komen met een Windows systeem, zullen de regeleinden mogelijk vervangen zijn.
Git heeft een aantal configuratie opties om je bij deze problemen te helpen.

===== `core.autocrlf`

(((crlf)))(((regel einden, line endings)))
Als je in Windows programmeert en werkt met mensen die dat niet doen (of vice-versa), zal je waarschijnlijk op enig moment met regel-einde problematiek te maken krijgen.
Dit is omdat Windows zowel een wagen-terugvoer teken (carriage-return) en een regelopvoer teken (linefeed) gebruikt voor nieuwe regels in haar bestanden, waar Mac en Linux systemen alleen het linefeed teken gebruiken.
Dit is een subtiel maar ongelofelijk ergerlijk feit van het werken op verschillende platforms; veel tekstverwerkers op Windows vervangen stilletjes bestaande LF-stijl regeleinden met CRLF, of voegen beide regel-einde karakters in als de gebruiker de enter toets gebruikt.

Git kan dit verwerken door automatisch CRLF regel-einden te converteren in een LF als je een bestand in de index toevoegt, en vice-versa als je code uitcheckt naar je bestandssysteem.
Je kunt deze functionaliteit aanzetten met de `core.autocrlf` instelling.
Als je op een Windows machine werkt, zet dit dan op `true` - dit zal LF einden naar CRLF vertalen als je code uitcheckt:

[source,console]

$ git config --global core.autocrlf true

Als je op een Linux of Mac systeem werkt die LF regeleinden gebruikt, dan wil je niet dat Git deze automatisch vertaalt als je bestanden uitcheckt, echter als een bestand met CRLF regeleinden per ongeluk binnenkomt, dan zou je wellicht willen dat Git dit rechttrekt.
Je kunt Git vertellen om CRLF naar LF te vertalen bij een commit, maar niet omgekeerd door de instelling `core.autocrlf` op input te zetten:

[source,console]

$ git config --global core.autocrlf input

Met deze instelling zou je uit moeten komen met CRLF regeleinden in Windows checkouts, maar LF regeleinden op Mac enLinux systemen en in de repository.

Als je een Windows programmeur bent die aan een project werkt voor alleen Windows, kan je deze functionaliteit uitzetten, waarbij carriage returns in de repository worden vastgelegd door de configuratie waarde op `false` te zetten:

[source,console]

$ git config --global core.autocrlf false

===== `core.whitespace`

Git wordt geleverd met de instelling om een aantal witruimte problemen te detecteren en op te lossen.
Het kan zoeken naar zes meestvoorkomende witruimte problemen -- drie ervan zijn standaard ingeschakeld en kunnen worden uitgeschakeld, en drie zijn uitgeschakeld maar kunnen worden geactiveerd.

Degenen die standaard aanstaan zijn `blank-at-eol`, die naar spaties aan het eind van een regel kijkt; `blank-at-eof`, die blanco regels opmerkt aan het eind van een bestand; en `space-before-tab`, die kijkt naar spaties voor tabs aan het begin van een regel.

De drie die standaard uitstaan, maar die kunnen worden aangezet, zijn `indent-with-non-tab`, die kijkt naar regels die beginnen met spaties in plaats van tabs (en wordt geregeld met de `tabwidth` optie); `tab-in-indent`, die kijkt naar tabs in het inspring-gedeelte van een regel; en `cr-at-eol`, welke Git vertelt dat carriage returns aan het eind van regels worden geaccepteerd.

Je kunt Git vertellen welke van deze je ingeschakeld wilt hebben door `core.whitespace` op de waarden te zetten die je aan of uit wilt hebben, gescheiden met komma's.
Je kunt instellingen uitzetten door ze weg te laten uit de instelling-reeks of ze vooraf te laten gaan met een `-`.
Bijvoorbeeld, als je alles behalve `cr-at-eol` wilt inschakelen kan je dit doen (met `trailing-space` als afkorting die zowel `blank-at-eol` als `blank-at-eof`afdekt):

[source,console]

$ git config --global core.whitespace \ trailing-space,-space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol

Of je kunt alleen het specificerende gedeelte aangeven:

[source,console]

$ git config --global core.whitespace \ -space-before-tab,indent-with-non-tab,tab-in-indent,cr-at-eol

Git zal deze problemen opsporen als je een `git diff` commando aanroept en ze proberen met een kleur aan te geven zodat je ze mogelijk kunt oplossen voordat je commit.
Het zal deze waarden ook gebruiken om je te helpen als je patches toepast met `git apply`.
Als je patches aan het toepassen bent, kan je Git ook vragen om je te waarschuwen als het patches toepast met de aangegeven witruimte problematiek:

[source,console]

$ git apply --whitespace=warn <patch>

Of je kunt Git het probleem automatisch laten proberen op te lossen voordat het de patch toepast:

[source,console]

$ git apply --whitespace=fix <patch>

Deze opties zijn ook van toepassing voor het `git rebase` commando.
Als je witruimte problemen hebt gecommit, maar je hebt ze nog niet stroomopwaarts gepusht, kan je `git rebase --whitespace=fix` aanroepen om Git deze problemen automatisch te laten oplossen als het de patches herschrijft.

==== Server configuratie

Er zijn lang niet zoveel configuratie opties beschikbaar voor de server kant van Git, maar er zijn een aantal interessante waar je wellicht naar wilt kijken.

===== `receive.fsckObjects`

Git is in staat om te verifiëren dat elk ontvangen object bij een push nog steeds een passende SHA-1 checksum heeft en naar geldige objecten wijst.
Dit doet het echter niet standaard; het is een relatief dure operatie, en kan het uitvoeren van de operatie vertragen, zeker bij grote repositories of pushes.
Als je wilt dat Git object consistentie bij elke push controleert, kan je dit afdwingen door `receive.fsckObjects` op true te zetten:

[source,console]

$ git config --system receive.fsckObjects true

Nu zal Git de integriteit van je repository controleren voordat elke push wordt geaccepteerd om er zeker van te zijn dat defecte (of kwaadwillende) werkstations geen corrupte gegevens aanleveren.

===== `receive.denyNonFastForwards`

Als je commits rebased die je al gepusht hebt en dan weer probeert te pushen, of op een andere manier probeert eencommit te pushen naar een remote branch die de commit niet bevat waar de remote branch op dit moment naar wijst, zal dit geweigerd worden.
Dit is normaalgesproken een goed beleid, maar in het geval van de rebase, kan je besluiten dat je weet wat je aan het doen bent en de remote branch geforceerd updaten met een `-f` vlag bij je push commando.

Om Git te vertellen om geforceerd pushen te weigeren, zet je `receive.denyNonFastForwards`:

[source,console]

$ git config --system receive.denyNonFastForwards true

De andere manier waarop je dit kunt doen is via receive hooks aan de kant van de server, waar we straks meer over gaan vertellen.
Die aanpak stelt je ook in staat om iets complexere dingen te doen als het weigeren van non-fast-forwards bij een bepaalde groep van gebruikers.

===== `receive.denyDeletes`

Een van de manieren om om het `denyNonFastForwards` beleid heen te werken is om als gebruiker de branch te verwijderen en deze dan weer te pushen met de nieuwe referenties.
Om dit te voorkomen, zet `receive.denyDeletes` op true:

[source,console]

$ git config --system receive.denyDeletes true

Dit weigert het verwijderen van branches of tags -- geen enkele gebruiker kan dit doen.
Om remote branches te verwijderen, moet je de ref-bestanden handmatig van de server verwijderen.
Er zijn nog meer interessante manieren om dit te doen op een gebruikersgerichte manier via ACL's, zoals je zult zien in <<_an_example_git_enforced_policy>>.


////
Laatst bijgewerkt van progit/progit2 referentie: 7836cfed
////
=== Git attributen

(((attributen)))
Een aantal van deze settings kunnen ook worden ingericht voor een pad, zodat Git deze instellingen alleen zal gebruiken voor een subdirectory of een subset van bestanden.
Deze pad-specifieke instellingen worden Git attributen genoemd, en worden bewaard in een `.gitattributes` bestand in een van je directories (normaal gesproken de root van je project) of in het `.git/info/attribures` bestand als je dit attributen bestand niet met je project wilt committen.

Met het gebruik van attributen, kan je dingen doen als het specificeren van separate merge strategieën voor individuele bestanden of directories in je project, Git vertellen hoe niet-tekst bestanden moeten worden gedifft, of Git inhoud laten filteren voordat je het naar of van Git in- of uitcheckt.
In deze paragraaf zullen we je een en ander vertellen over de attributen die je kunt inrichten op je paden in je Git project en je zult een aantal voorbeelden zien hoe deze mogelijkheden in de praktijk gebruikt kunnen worden.

==== Binaire bestanden

(((binaire bestanden)))
Een aardig voorbeeld waar je Git attributen voor kunt gebruiken is Git vertellen welke bestanden binair zijn (in die gevallen waar het niet zelf kan bepalen) en Git speciale instructies te geven over hoe deze bestanden te behandelen.
Bijvoorbeeld, sommige tekst bestanden kunnen door applicaties zijn gegenereerd en daarmee niet diff-baar, terwijl sommige binaire bestanden juist wel kunnen worden gedifft.
We zullen je laten zien hoe je Git kunt vertellen het onderscheid te maken.

===== Binaire bestanden herkennen

Sommige bestanden zien er uit als tekst bestanden, maar moeten praktisch gezien gewoon behandeld worden als binaire gegevens.
Als voorbeeld, Xcode projecten op de Mac bevatten een bestand dat eindigt op `.pbxproj`, wat eigenlijk gewoon een JSON (platte tekst Javascript gegevens formaat) dataset is die door de IDE naar schijf geschreven wordt, waarin je bouwinstellingen staan en zo voorts.
Alhoewel het technisch gesproken een tekst bestand is (omdat het allemaal UTF-8 is), wil je het niet als dusdanig behandelen omdat het eigenlijk een lichtgewicht database is - je kunt de inhoud niet mergen als twee personen het wijzigen, en diffs zijn over het algemeen niet echt nuttig.
De bedoeling van dit bestand is om door een machine te worden verwerkt.
Het komt erop neer dat je het wilt behandelen als een binair bestand.

Om Git te vertellen om alle `pbxproj` bestanden als binaire gegevens te behandelen, voeg je de volgende regel to aan je `.gitattributes` bestand:

[source]

*.pbxproj binary

Nu zal Git niet proberen om CRLF gevallen te converteren of te repareren, en ook zal het niet proberen om een diff te bepalen of af te drukken voor wijzigingen in dit bestand als je `git show` of `git diff` op je project aanroept.

===== Binaire bestanden diffen

Je kunt de Git attributen functionaliteit ook gebruiken om binaire bestanden feitelijk te diffen.
Je kunt dit doen door Git te vertellen hoe het je binaire gegevens moet converteren naar een tekst formaat die weer via een normale diff kan worden vergeleken.

Allereerst zal je deze techniek gebruiken om een van de meest ergerlijke problemen die er voor de mensheid bestaan op te lossen: het onder versie beheer plaatsen van Microsoft Word documenten.
Iedereen weet dat Word een van de afschuwwekkendste tekstverwerkers is die bestaat maar, die vreemd genoeg, nog steeds door iedereen gebruikt wordt.
Als je Word documenten onder versie beheer wilt brengen, kan je ze in een Git repository stoppen en eens in de zoveel tijd committen, maar wat is het nut hiervan?
Als je `git diff` gewoon zou aanroepen, zou je alleen maar iets als dit zien:

[source,console]

$ git diff diff --git a/chapter1.docx b/chapter1.docx index 88839c4..4afcb7c 100644 Binary files a/chapter1.docx and b/chapter1.docx differ

Je kunt twee versies niet rechtstreeks vergelijken tenzij je ze uitcheckt en dan handmatig vergelijkt, toch?
We zullen laten zien dat je dit redelijk goed kunt doen met Git attributen.
Zet de volgende regel in je `.gitattributes` bestand:

[source]

*.docx diff=word

Dit vertelt Git dat elk bestand dat past in het patroon (`.docx`) het ``word'' filter moet gebruiken als je een diff probeert te bekijken waar verschillen in zitten.
Wat is het ``word'' filter?
Je moet het zelf inrichten.
Hier ga je Git configureren om het `docx2txt` programma te gebruiken om Word documenten in leesbare tekstbestanden te converteren, die vervolgens juist kunnen worden gedifft.

Allereerst moet je `docx2txt` installeren, je kunt het downloaden van http://docx2txt.sourceforge.net[].
Volg de instructies in het `INSTALL` bestand om het ergens neer te zetten waar je shell het kan vinden.
Vervolgens, schrijf je een wrapper script om de uitvoer te converteren naar het formaat dat Git verwacht.
Maak een bestand ergens in je pad en noem dezer `docx2txt` en zet dit hierin:

[source,console]

#!/bin/bash docx2txt.pl "$1" -

Vergeet dit niet op dit bestand `chmod a+x` aan te roepen.
Tot slot kan je Git configureren om dit script te gebruiken:

[source,console]

$ git config diff.word.textconv docx2txt

Nu weet Git dat als het een diff probeert uit te voeren tussen twee snapshots, en een van de bestandsnamen eindigt op `.docx`, dat het deze bestanden door het ``word'' filter moet halen die als het `docx2txt` programma gedefinieerd is.
Effectief maakt dit mooie tekst-gebaseerde versies van je Word bestanden voordat het probeert deze te diffen.

Hier is een voorbeeld: Hoofdstuk 1 van di boek was geconverteerd naar een Word formaat en gecommit in een Git repository.
Toen is er een nieuwe sectie toegevoegd.
Dit is wat `git diff` laat zien:

[source,console]

$ git diff diff --git a/chapter1.docx b/chapter1.docx index 0b013ca..ba25db5 100644 --- a/chapter1.docx + b/chapter1.docx @@ -2,6 +2,7 @@ This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so. 1.1. About Version Control What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer. +Testing: 1, 2, 3. If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead. 1.1.1. Local Version Control Systems Many people’s version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they’re clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you’re in and accidentally write to the wrong file or copy over files you don’t mean to.

Git vertelt ons succesvol en bondig dat we de tekenreeks ``Testing: 1, 2, 3,'' hebben toegevoegd, wat juist is.
Het is niet perfect - wijzigingen in formattering worden niet zichtbaar - maar het werkt wel.

Een ander interessant probleem wat je op deze manier kunt oplossen betreft het diffen van bestanden met afbeeldingen.
Een manier om dit te doen is om afbeeldingen door een filter te halen die hun EXIF informatie extraheert - metadata die bij de meeste grafische bestanden worden vastgelegd.
Als je het `exiftool` downloadt en installeert, kan je deze gebruiken om je afgeeldingen naar tekst over de metadata te converteren, zodat de diff je in elk geval een tekstuele representatie van wijzigingen kan laten zien:
Zet de volgende regel in je `.gitattributes` bestand:

[source,ini]

*.png diff=exif

Configureer Git om dit tool te gebruiken:

[source,console]

$ git config diff.exif.textconv exiftool

Als je een afbeelding in je project vervangt en dan `git diff` aanroept, zie je iets als dit:

[source,diff]

diff --git a/image.png b/image.png index 88839c4..4afcb7c 100644 --- a/image.png + b/image.png @@ -1,12 +1,12 @@ ExifTool Version Number : 7.74 -File Size : 70 kB -File Modification Date/Time : 2009:04:21 07:02:45-07:00 +File Size : 94 kB +File Modification Date/Time : 2009:04:21 07:02:43-07:00 File Type : PNG MIME Type : image/png -Image Width : 1058 -Image Height : 889 +Image Width : 1056 +Image Height : 827 Bit Depth : 8 Color Type : RGB with Alpha

Je kunt eenvoudig zien dat de bestandsgrootte en de dimensies van je afbeelding beide zijn veranderd.

[[_keyword_expansion]]
==== Sleutelwoord expansie (Keyword expansion)

(((keyword expansion)))(((sleutelwoord expansie)))
Keyword expansion zoals je dit kan vinden bij SVN of CVS wordt vaak gewenst door ontwikkelaars die gewend zijn aan die systemen.
Het grote probleem met dit is in Git is dat je een bestand niet kunt aanvullen met informatie over de commit nadat je het hebt gecommit, omdat Git eerst een checksum van het bestand maakt.
Je kunt echter tekst in een bestand injecteren als het uitgecheckt wordt en het weer verwijderen voordat het aan een commit wordt toegevoegd.
Git attributen geven je twee manieren om dit te bereiken.

Als eerste kan je automatisch de SHA-1 checksum van een blob in een `$Id$` veld injecteren.
Als je dit attribuut op een bestand of een aantal bestanden zet dan zal Git, als je die branch een volgende keer
 uitcheckt. dat veld vervangen met de SHA-1 van de blob.
Het is belangrijk om op te merken dat het niet de SHA-1 van de commit is, maar van de blob zelf:

[source,console]

$ echo *.txt ident >> .gitattributes $ echo $Id$ > test.txt

De volgende keer als je dit bestand uitcheckt, zal Git de SHA-1 van de blob injecteren:

[source,console]

$ rm test.txt $ git checkout — test.txt $ cat test.txt $Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

Dit resultaat is echter van beperkt nut.
Als je keyword substitutie in CVS of Subversion gebruikt hebt, kan je een datestamp bijsluiten - de SHA-1 is eigenlijk niet zo nuttig, omdat het redelijk willekeurig is en je kunt aan een SHA-1 niet zien of het ouder of jonger is dan een andere door alleen er naar te kijken.

Je kunt echter je eigen filters schrijven om substituties in bestanden doen bij commit/checkout.
Dit worden ``clean'' (kuis) en ``smudge'' (besmeur) filters genoemd.
In het `.gitattributes` bestand kan je een filter instellen voor specifieke paden en scripts maken die de bestanden bewerken vlak voordat ze worden uitgechecked (``smudge'', zie <<filters_a>>) en vlak voordat ze worden gestaged (``clean'', zie <<filters_b>>).
Deze filters kunnen worden verteld om allerhande leuke dingen te doen.

[[filters_a]]
.Het ``smudge'' filter wordt bij checkout aangeroepen.
image::images/smudge.png[Het ``smudge'' filter wordt bij checkout aangeroepen.]

[[filters_b]]
.Het ``clean'' filter wordt aangeroepen als bestanden worden gestaged.
image::images/clean.png[Het ``clean'' filter wordt aangeroepen als bestanden worden gestaged.]

Het orginele commit bericht voor deze functionaliteit geeft een simpel voorbeeld van al je C broncode door het `indent` programma te laten bewerken voor het committen.
Je kunt het inrichten door het filter attribuut in je `.gitattributes` bestand `*.c` bestanden te laten filteren met het ``indent'' filter:

[source,ini]

*.c filter=indent

Daarna vertel je Git wat het ``indent'' filter moet doen bij smudge en clean:

[source,console]

$ git config --global filter.indent.clean indent $ git config --global filter.indent.smudge cat

In dit geval, als je bestanden commit die lijken op `*.c`, zal Git deze door het indent programma halen voordat het deze staget en ze door het `cat` programma halen voordat het ze weer naar schijf uitcheckt.
Het `cat` programma doet eigenlijk niets: het geeft de gegevens die binnenkomen ongewijzigd door.
Deze combinatie zal effectief alle C broncode door `indent` laten filteren voor het te committen.

Een ander interessant voorbeeld veroorzaakt `$Date$` keyword expansie, zoals bij RCS.
Om dit goed te doen heb je een klein scriptje nodig dat een bestandsnaam neemt, de laatste commit datum vindt voor dit project en dan de datum in dat bestand injecteren.
Hier is een kort Ruby script dat precies dit doet:

[source,ruby]

#! /usr/bin/env ruby data = STDIN.read last_date = git log --pretty=format:"%ad" -1 puts data.gsub($Date$, $Date: ' + last_date.to_s + '$)

Al wat dit script doet is de laatste commit datum van het `git log` commando uitlezen, en dat in elke `$Date$` tekenreeks die het in stdin ziet zetten, en drukt het resultaat af - het zou eenvoudig moeten zijn om dit te doen in een taal waar je het meest vertrouwd mee bent.
Je kunt dit bestand `expand_date` noemen en het op je pad plaatsen.
Nu moet je een filter opzetten in Git (noem het `dater`) en het vertellen om je `expand_date` filter te gebruiken om de bestanden te besmeuren bij uitchecken.
Je kunt een Perl expressie gebruiken om dat bij commit weer op te kuisen:

[source,console]

$ git config filter.dater.smudge expand_date $ git config filter.dater.clean perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"

Dit stukje Perl haalt alles weg wat het ziet in een `$Date$` tekenreeks, om terug te halen waar je mee was begonnen.
Nu je filter gereed is, kan je het uitproberen door een bestand te maken met je `$Date$` keyword en daarna een Git attribuut op te zetten voor dat bestand die je nieuwe filter activeert:

[source,ini]

date*.txt filter=dater

[source,console]

$ echo # $Date$ > date_test.txt

Als je deze wijzigingen commit en het bestand weer uitcheckt, zal je het keyword correct vervangen zien:

[source,console]

$ git add date_test.txt .gitattributes $ git commit -m "Testing date expansion in Git" $ rm date_test.txt $ git checkout date_test.txt $ cat date_test.txt # $Date: Tue Apr 21 07:26:52 2009 -0700$

Je kunt zien hoe krachtig deze techniek kan zijn voor eigen toepassingen.
Je moet echter wel voorzichtig zijn, omdat het `.gitattributes` bestand gecommit wordt en met het project wordt verspreid, maar dat het uitvoerende element (in dit geval `dater`) dat niet wordt, zodat het niet overal zal werken.
Als je deze filters ontwerpt, moeten ze in staat zijn om netjes te falen en het project nog steeds goed te laten werken.

==== Je repository exporteren

(((archiveren)))
De Git attribute gegevens staan je ook toe om interessante dingen te doen als je een archief van je project exporteert.

===== `export-ignore`

Je kunt Git vertellen dat sommige bestanden of directories niet geëxporteerd moeten worden bij het genereren van een archief.
Als er een subdirectory of bestand is waarvan je niet wilt dat het wordt meegenomen in je archief bestand, maar dat je wel in je project ingecheckt wil hebben, kan je die bestanden benoemen met behulp van het `export-ignore` attribuut.

Bijvoorbeeld, stel dat je wat testbestanden in een `test/` subdirectory hebt, en dat het geen zin heeft om die in de tarball export van je project mee te nemen.
Dan kan je de volgende regel in je Git attributes bestand toevoegen:

[source,ini]

test/ export-ignore

Als je nu `git archive` uitvoert om een tarball van je project te maken, zal die directory niet meegenomen worden in het archief.

===== `export-subst`

Als je bestanden exporteert voor deployment kan je de formattering en sleutelwoord expansie van `git log` toepassen om delen van bestanden selecteren die met het `export-subst` attribuut zijn gemarkeerd.

Bijvoorbeeld, als je een bestand genaamd `LAST_COMMIT` wilt meenemen in je project, waarin de laatste commit datum op van het moment dat `git archive` liep automatisch wordt geïnjecteerd, kan je het bestand als volgt instellen:

[source,ini]

LAST_COMMIT export-subst

[source,console]

$ echo Last commit date: $Format:%cd by %aN$ > LAST_COMMIT $ git add LAST_COMMIT .gitattributes $ git commit -am adding LAST_COMMIT file for archives

Als je `git archive` uitvoert, zal de inhoud van dat bestand er zo uit zien:

[source,console]

$ git archive HEAD | tar xCf ../deployment-testing - $ cat ../deployment-testing/LAST_COMMIT Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon ---

De vervangingen kunnen bijvoorbeeld het commit bericht en elke git note omvatten, en git log kan eenvoudige word-wrapping uitvoeren:

$ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT
$ git commit -am 'export-subst uses git log'\''s custom formatter

git archive uses git log'\''s `pretty=format:` processor
directly, and strips the surrounding `$Format:` and `$`
markup from the output.
'
$ git archive @ | tar xfO - LAST_COMMIT
Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
       export-subst uses git log's custom formatter

         git archive uses git log's `pretty=format:` processor directly, and
         strips the surrounding `$Format:` and `$` markup from the output.

Het archief wat hieruit komt is bruikbaar voor deployment werkzaamheden, maar zoals elk geëxporteerd archief is het niet echt toepasbaar voor verdere ontwikkel werk.

==== Merge strategieën

Je kunt Git attributen ook gebruiken om Git te vertellen dat het verschillende merge strategieën moet gebruiken voor specifieke bestanden in je project. Een erg handige toepassing is Git te vertellen dat het bepaalde bestanden niet moet proberen te mergen als ze conflicten hebben, maar jouw versie moeten gebruiken in plaats van die van de ander.

Dit is handig als een branch in jouw project uiteen is gelopen of gespecialiseerd is, maar je wel in staat wilt zijn om veranderingen daarvan te mergen, en je wilt bepaalde bestanden negeren. Stel dat je een database instellingen-bestand hebt dat database.xml heet en tussen twee branches verschilt, en je wilt de andere branch mergen zonder jouw database bestand overhoop te halen. Je kunt dan een attribuut als volgt instellen:

database.xml merge=ours

En daarna een loze ours merge strategie definiëren met:

$ git config --global merge.ours.driver true

Als je in de andere branch merget, dan zul je in plaats van merge conflicten met het database.xml bestand zoiets als dit zien:

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

In dit geval blijft database.xml staan op de versie die je oorspronkelijk al had.

=== Git Hooks

Net als veel andere versie beheer systemen, heeft Git een manier om eigen scripts aan te roepen als er bepaalde belangrijke gebeurtenissen plaatsvinden. Er zijn twee groepen van deze haken (hooks): aan de kant van het werkstation en aan de kant van de server. De hooks aan de kant van het werkstation worden afgetrapt door operaties zoals committen en mergen, terwijl die aan de server kant worden afgetrapt door netwerk operaties zoals het ontvangen van gepushte commits. Je kunt deze hooks voor allerhande doeleinden gebruiken.

==== Het installeren van een hook

De hooks worden allemaal opgeslagen in de hooks subdirectory van de Git directory. In de meeste projecten is dat .git/hooks. Als je een nieuwe repository met git init initialiseert, vult Git de hooks directory met een aantal voorbeeld scripts, vele ervan zijn op zichzelf al nuttig; maar ze beschrijven ook de invoer waarden van elk script. Al de voorbeelden zijn geschreven als shell scripts, met af en toe wat Perl erdoorheen, maar elk uitvoerbaar script met een goede naam zal prima werken - je kunt ze in Ruby of Python schrijven of verzin maar een taal. Als je de meegeleverde hook scripts wilt gebruiken, zul je ze moeten hernoemen; de bestandsnamen eindigen allemaal met .sample.

Om een hook script te activeren, zet een bestand in de hooks subdirectory van je Git directory met een juiste naam (geen enkele extensie) en zorg dat 'ie uitvoerbaar is. Vanaf dat moment zou het aangeroepen moeten worden. We zullen de meest belangrijke hook bestandsnamen hier behandelen.

==== Hooks bij werkstations

Er zijn veel hooks aan de kant van een werkstation. Deze paragraaf verdeelt ze in hooks voor een committing-workflow, scripts voor een e-mail-workflow en al het andere.

Note

Het is belangrijk om op te weten dat hooks bij werkstations niet worden gekopieerd als je een repository kloont. Als het de bedoeling is van deze scripts om een beleid af te dwingen, moet je dat waarschijnlijk aan de kant van de server doen; zie het voorbeeld in [_an_example_git_enforced_policy].

===== Committing-Workflow Hooks

De eerste vier hooks hebben te maken met het proces van committen.

De pre-commit hook wordt het eerst aangeroepen, voordat je zelfs een commit bericht begint te typen. Het wordt gebruikt om de snapshot die op het punt staat te worden gecommit te inspecteren, om te zien of je iets vergeten bent, om er zeker van te zijn dat de tests slagen of om de code te controleren op iets waar je het op gecontroleerd wilt hebben. Als dit script met niet-nul wordt beëindigd breekt de commit af, al kan je er altijd nog omheen met git commit --no-verify. Je kunt dingen doen als de code op stijl controleren (lint of iets vergelijkbaar aanroepen), controleren op witruimtes aan het eind van regels (de standaard hook doet precies dat), of controleren of nieuwe methoden goed zijn gedocumenteerd.

De prepare-commit-msg hook wordt aangeroepen voordat de commit bericht tekstverwerker wordt opgestart, maar nadat het standaard bericht gemaakt is. Dit geeft je de ruimte om het standaard bericht aan te passen voordat de commit auteur deze ziet. Deze hook heeft een aantal parameters: het pad naar het bestand dat het commit bericht op dat moment bevat, het type commit en de SHA-1 van de commit als het een amended (gewijzigde) commit betreft. Deze hook is over het algemeen niet nuttig voor normale commits; maar is daarentegen goed voor commits waar het standaard bericht automatisch is gegenereerd, zoals commit berichten van een sjabloon, merge commits, squashed commits en amended commits. Je kunt het gebruiken in combinatie met een commit sjabloon om via een programma informatie in te voegen.

De commit-msg hook krijgt één parameter, en dat is weer het pad naar een tijdelijk bestand dat het commit bericht bevat die door de ontwikkelaar is geschreven. Als dit script met een niet-nul waarde eindigt, zal Git het commit proces afbreken, en daarmee kan je dit gebruiken om de status van het project of het commit bericht te valideren voordat je toestaat dat een commit doorgaat. In de laatste paragraaf van dit hoofdstuk zullen we laten zien hoe deze hook te gebruiken om te controleren dat het commit bericht voldoet aan een vereist patroon.

Als het gehele commit proces is voltooid wordt de post-commit hook aangeroepen. Deze krijgt geen enkele parameter, maar je kunt de laatste commit eenvoudig te pakken krijgen door git log -1 HEAD aan te roepen. Meestal wordt dit script gebruikt voor het doen van meldingen of iets vergelijkbaars.

===== E-mail workflow hooks

Je kunt drie hooks aan de kant van het werkstation inrichten voor een op e-mail gebaseerde workflow. Ze worden alle aangeroepen door het git am commando, dus als je dat commando niet gebruikt in je workflow, kan je dit gedeelte rustig overslaan en doorgaan naar de volgende paragraaf. Als je patches per e-mail ontvangt die door git format-patch zijn gemaakt, kunnen een aantal van deze behulpzaam zijn.

De eerste hook die wordt aangeroepen is applypatch-msg. Deze krijgt een enkele argument: de naam van het tijdelijke bestand dat het voorgestelde commit bericht bevat. Git breekt het patchen af als dit script met niet-nul eindigt. Je kunt deze gebruiken om je ervan te verzekeren dat het commit bericht juist geformatteerd is, of het bericht normaliseren door met het script deze ter plaatse te bewerken.

De volgende hook die aangeroepen wordt als patches met git am worden toegepast is pre-applypatch. Ietwat verwarrend, wordt deze na het toepassen van een patch aangreoepen maar voordat een commit gemaakt wordt, dus je kunt het gebruiken om de snapshot te inspacteren voordat de commit gemaakt wordt. Je kunt tests uitvoeren of op een andere manier de werk-tree met dit script inspecteren. Als er iets ontbreekt of de tests slagen niet, zal het eindigen met niet-nul het git am script afbreken zonder de patch te committen.

De laatste hook die wordt aangeroepen met een git am operatie is post-applypatch, die wordt aangeroepen nadat de commit gemaakt is. Je kunt deze gebruiken om een groep of de auteur van de patch een bericht te sturen van dat je de patch hebt gepulld. Je kunt het patching proces niet stoppen met dit script.

===== Overige werkstation hooks

De pre-rebase hook wordt aangeroepen voordat je ook maar iets rebaset en kan het proces stoppen door met niet-nul te eindigen. Je kunt deze hook gebruiken om het rebasen van commits die al zijn gepusht te weigeren. Het voorbeeld pre-rebase hook die Git installeert dit dit, al doet het een aantal aannamen die misschien niet van toepassing zijn op jouw workflow.

De post-rewrite hook wordt aangeroepen door commando’s die commits vervangen, zoals git commit --amend en git rebase (echter weer niet door git filter-branch). Het enige argument is het commando dat herschrijven heeft veroorzaakt, en het ontvangt een lijst van herschrijvingen op stdin. Deze hook heeft veel wat gebruik betreft overeenkomsten met het gebruik van de post-checkout en post-merge hooks.

Nadat je succesvol een git checkout hebt aangeroepen, wordt de post-checkout hook aangeroepen; je kunt deze gebruiken om je werk directory juist in te richten voor jouw project omgeving. Dit zou het binnenhalen van grote binaire bestanden die je niet in versie beheer wilt hebben kunnen inhouden, het automatisch genereren van documentatie of iets van dien aard.

De post-merge hook wordt aangeroepen na een succesvolle merge commando. Je kunt deze gebruiken om gegevens terug te zetten in de werk tree die Git niet kan tracken, zoals permissie gegevens. Deze hook kan op vergelijkbare manier de aanwezigheid van bestanden buiten Git valideren die je misschien naar binnen wilt kopieren als de werk tree wijzigt.

De pre-push hook wordt aangeroepen tijdens git push, nadat de remote refs zijn ge-update maar voordat er objecten zijn verstuurd. Het ontvangt de naam en locatie van de remote als parameters, en een lijst met refs die op het punt staan te worden geactualiseerd via stdin. Je kunt dit gebruiken om een aantal ref updates te valideren voordat er een push plaatsvindt (een niet-nul einde zal de push afbreken).

Git zal nu en dan afval verzamelen (garbage collection) als onderdeel van zijn normale taken, door git gc --auto aan te roepen. De pre-auto-gc hook wordt aangeroepen vlak voordat de garbage collection plaatsvindt, en kan worden gebruikt om aan te geven dat het staat te gebeuren, of deze operatie af te breken als het nu niet goed uitkomt.

==== Hooks aan de kant van de server

Naast de hooks aan de kant van de werkstation, kan je als systeem beheerder een aantal belangrijke hooks op de server gebruiken om bijna alle vormen van beleid af te dwingen voor je project. Deze scripts worden voor en na pushes naar de server aangeroepen. De pre hooks kunnen op elk moment niet-nul aflopen om de push af te wijzen zowel als een fout bericht afdrukken die naar het werkstation worden gestuurd; je kunt een push beleid opzetten die zo complex is als je zelf wenst.

===== pre-receive

Het eerste script die wordt aangeroepen als een push wordt afgehandeld van een werkstation is pre-receive. Deze krijgt een lijst van referenties die worden gepusht vanuit stdin; als het niet-nul eindigt wordt geen enkele geaccepteerd. Je kunt deze hook gebruiken om zaken te doen als het verzekeren dat geen van de bijgewerkte referenties non-fast-forwards zijn, of toegangscontroles uit te voeren voor alle refs en bestanden die met de push worden gewijzigd.

===== update

Het update script lijkt erg op het pre-receive script, behalve dat het eenmaal wordt aangroepen voor elke branch die de pusher probeert bij te werken. Als de pusher meerdere branches probeert te pushen, wordt pre-receive maar één keer aangeroepen, terwijl update één keer per branch waarnaar wordt gepusht wordt aangeroepen. In plaats van van stdin te lezen, krijgt dit script drie argumenten: de naam van de referentie (branch), de SHA-1 waar die referentie naar wees voor de push en de SHA-1 die de gebruiker probeert te pushen. Als het update script niet-nul eindigt, wordt alleen die referentie geweigerd, de andere referenties kunnen nog steeds worden geüpdatet.

===== post-receive

De post-receive hook wordt aangeroepen zodra het hele proces gereed is, en kan worden gebruikt om andere diensten bij te werken of gebruikers een bericht te sturen. Het krijgt dezelfde stdin gegevens als de pre-receive hook. Voorbeelden omvatten het mailen naar een lijst, continuous integratie services notificeren of een ticket-traceer systeem bij werken - je kunt zelfs de commit berichten doorlezen om te zien of er tickets geopend, gewijzigd of gesloten moeten worden. Dit script kan het push proces niet stoppen, maar het werkstation zal de verbinding niet verbreken voordat dit script geëindigd is, dus wees voorzichtig als je iets probeert te doen wat veel tijd in beslag neemt.

=== Een voorbeeld van Git-afgedwongen beleid

In deze paragraaf ga je gebruiken wat je geleerd hebt om een Git workflow te maken, die controleert op een specifiek en alleen bepaalde gebruikers toestaat om bepaalde subdirectories te wijzigen in een project. Je zult client scripts maken die de ontwikkelaar helpen te ontdekken of hun push geweigerd zal worden en server scripts die het beleid afdwingen.

We hebben Ruby gebruikt om deze te schrijven, deels vanwege onze intellectuele inertie, maar ook omdat Ruby eenvoudig te lezen, zelfs als je er niet in zou kunnen schrijven. Echter, elke taal voldoet - alle voorbeeld hook-scripts die met Git geleverd worden zijn in Perl of Bash geschreven, dus je kunt ook genoeg voorbeelden van hooks in deze talen zien door naar deze voorbeelden te kijken.

==== Server-kant hook

Al het werk aan de server kant zal in het update bestand in je hooks directory gaan zitten. De update hook zal eens per gepushte branch uitgevoerd worden en accepteert drie argumenten:

  • de naam van de referentie waarnaar gepusht wordt

  • de oude revisie waar die branch was

  • de nieuwe gepushte revisie.

Je hebt ook toegang tot de gebruiker die de push doet als de push via SSH gedaan wordt. Als je iedereen hebt toegestaan om connectie te maken als één gebruiker (zoals “git”) via publieke sleutel authenticatie, dan moet je wellicht die gebruiker een shell wrapper geven die bepaalt welke gebruiker er connectie maakt op basis van de publieke sleutel, en dan een omgevingsvariabele instellen waarin die gebruiker wordt gespecificeerd. Wij gaan er hier van uit dat de gebruiker in de $USER omgevingsvariabele staat, dus begint je update script met het verzamelen van alle gegevens die het nodig heeft:

#!/usr/bin/env ruby

$refname = ARGV[0]
$oldrev  = ARGV[1]
$newrev  = ARGV[2]
$user    = ENV['USER']

puts "Enforcing Policies..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"

Ja, dat zijn globale variabelen. Niets zeggen - het is eenvoudiger om op deze manier dingen te demonstreren.

===== Een specifiek commit-bericht formaat afdwingen

Je eerste uitdaging is afdwingen dat elke commit bericht moet voldoen aan een specifiek formaat. Om iets te hebben om mee te werken, neem even aan dat elk bericht een tekenreeks moet bevatten die er uit ziet als “ref: 1234” omdat je wilt dat iedere commit gekoppeld is aan een werkonderdeel in je ticket systeem. Je moet dus kijken naar iedere commit die gepusht wordt, kijken of die tekst in de commit boodschap zit en als de tekst in één van de commits ontbreekt, met niet nul eindigen zodat de push geweigerd wordt.

Je kunt de lijst met alle SHA-1 waarden van alle commits die gepusht worden verkrijgen door de $newrev en $oldrev waarden te pakken en ze aan een Git binnenwerk commando genaamd git rev-list te geven. Dit is min of meer het git log commando, maar standaard voert het alleen de SHA-1 waarden uit en geen andere informatie. Dus, om een lijst te krijgen van alle commit SHA-1’s die worden geïntroduceerd tussen één commit SHA-1 en een andere, kun je zoiets als dit uitvoeren:

$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475

Je kunt die uitvoer pakken, door elk van die commit SHA’s heen lopen, de boodschap daarvan pakken en die boodschap testen tegen een reguliere expressie die op een bepaald patroon zoekt.

Je moet uit zien te vinden hoe je de commit boodschap kunt krijgen van alle te testen commits. Om de echte commit gegevens te krijgen, kun je een andere binnenwerk commando genaamd git cat-file gebruiken. We zullen alle binnenwerk commando’s in detail behandelen in [ch10-git-internals], maar voor nu is dit wat het commando je geeft:

$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

changed the version number

Een simpele manier om de commit boodschap uit een commit waarvan je de SHA-1 waarde hebt te krijgen, is naar de eerste lege regel gaan en alles wat daarna komt pakken. Je kunt dat doen met het sed commando op Unix systemen:

$ git cat-file commit ca82a6 | sed '1,/^$/d'
changed the version number

Je kunt die toverspreuk gebruiken om de commit boodschap te pakken van iedere commit die geprobeerd wordt te pushen en eindigen als je ziet dat er iets is wat niet past. Om het script te eindigen en de push te weigeren, eindig je met niet nul. De hele methode ziet er zo uit:

$regex = /\[ref: (\d+)\]/

# enforced custom commit message format
def check_message_format
  missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
  missed_revs.each do |rev|
    message = `git cat-file commit #{rev} | sed '1,/^$/d'`
    if !$regex.match(message)
      puts "[POLICY] Your message is not formatted correctly"
      exit 1
    end
  end
end
check_message_format

Door dat in je update script te stoppen, zullen updates geweigerd worden die commits bevatten met berichten die niet aan jouw beleid voldoen.

===== Een gebruiker-gebaseerd ACL systeem afdwingen

Stel dat je een mechanisme wil toevoegen dat gebruik maakt van een toegangscontrole lijst (ACL) die specificeert welke gebruikers wijzigingen mogen pushen naar bepaalde delen van je project. Sommige mensen hebben volledige toegang, en anderen hebben alleen toestemming om wijzigingen te pushen naar bepaalde subdirectories of specifieke bestanden. Om dit af te dwingen zul je die regels schrijven in een bestand genaamd acl dat in je bare Git repository op de server zit. Je zult de update hook naar die regels laten kijken, bekijken welke bestanden worden geïntroduceerd voor elke commit die gepusht wordt en bepalen of de gebruiker die de push doet toestemming heeft om al die bestanden te wijzigen.

Het eerste dat je zult doen is de ACL schrijven. Hier zul je een formaat gebruiken wat erg lijkt op het CVS ACL mechanisme: het gebruikt een serie regels, waarbij het eerste veld avail of unavail is, het volgende veld een komma gescheiden lijst van de gebruikers is waarvoor de regel geldt en het laatste veld het pad is waarvoor deze regel geldt (leeg betekent open toegang). Alle velden worden gescheiden door een pipe (|) karakter.

In dit geval heb je een aantal beheerders, een aantal documentatie schrijvers met toegang tot de doc map, en één ontwikkelaar die alleen toegang heeft tot de lib en test mappen, en je ACL bestand ziet er zo uit:

avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests

Je begint met deze gegevens in een structuur in te lezen die je kunt gebruiken. In dit geval, om het voorbeeld eenvoudig te houden, zul je alleen de avail richtlijnen handhaven. Hier is een methode die je een associatieve array teruggeeft, waarbij de sleutel de gebruikersnaam is en de waarde een array van paden waar die gebruiker toegang tot heeft:

def get_acl_access_data(acl_file)
  # read in ACL data
  acl_file = File.read(acl_file).split("\n").reject { |line| line == '' }
  access = {}
  acl_file.each do |line|
    avail, users, path = line.split('|')
    next unless avail == 'avail'
    users.split(',').each do |user|
      access[user] ||= []
      access[user] << path
    end
  end
  access
end

Met het ACL bestand dat je eerder bekeken hebt, zal deze get_acl_access_data methode een gegevensstructuur opleverendie er als volgt uit ziet:

{"defunkt"=>[nil],
 "tpw"=>[nil],
 "nickh"=>[nil],
 "pjhyett"=>[nil],
 "schacon"=>["lib", "tests"],
 "cdickens"=>["doc"],
 "usinclair"=>["doc"],
 "ebronte"=>["doc"]}

Nu je de rechten bepaald hebt, moet je bepalen welke paden de commits die gepusht worden hebben aangepast, zodat je kunt controleren dat de gebruiker die de push doet daar ook toegang toe heeft.

Je kunt eenvoudig zien welke bestanden gewijzigd zijn in een enkele commit met de --name-only optie op het git log commando (kort besproken in Git Basics):

$ git log -1 --name-only --pretty=format:'' 9f585d

README
lib/test.rb

Als je gebruik maakt van de ACL structuur die wordt teruggegeven door de get_acl_access_data methode en dat gebruikt met de bestanden in elk van de commits, dan kun je bepalen of de gebruiker toegang heeft om al hun commits te pushen:

# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
  access = get_acl_access_data('acl')

  # see if anyone is trying to push something they can't
  new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
  new_commits.each do |rev|
    files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
    files_modified.each do |path|
      next if path.size == 0
      has_file_access = false
      access[$user].each do |access_path|
        if !access_path  # user has access to everything
           || (path.start_with? access_path) # access to this path
          has_file_access = true
        end
      end
      if !has_file_access
        puts "[POLICY] You do not have access to push to #{path}"
        exit 1
      end
    end
  end
end

check_directory_perms

Je krijgt een lijst met nieuwe commits die gepusht worden naar je server met git rev-list. Daarna vind je, voor elk van deze commits, de bestanden die aangepast worden en stelt vast of de gebruiker die pusht toegang heeft tot alle paden die worden aangepast.

Nu kunnen je gebruikers geen commits pushen met slecht vormgegeven berichten of met aangepaste bestanden buiten hun toegewezen paden.

===== Het geheel testen

Als je nu chmod u+x .git/hooks/update aanroept, wat het bestand is waar je al deze code zou moeten hebben gezet, en dan een commit probeert te pushen met een ongeldig bericht, krijg je zoets als dit:

$ git push -f origin master
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 323 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Enforcing Policies...
(refs/heads/master) (8338c5) (c5b616)
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

Hier zijn een aantal interessante zaken. Allereerst, je ziet dit zodra de hook begint te lopen.

Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)

Onthoud dat je dit afgedrukt hebt helemaal aan het begin van je update script. Alles wat je script naar stdout echo’t wordt naar het werkstation gestuurd.

Het volgende wat je zult opmerken is het foutbericht.

[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master

De eerste regel was door jou afgedrukt, de andere twee waren van Git die je vertelt dat het update script met niet-nul is geëindigd en dat dat hetgeen is wat je push afkeurt. Tot slot heb je dit:

To git@gitserver:project.git
 ! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'

Je zult een remote afwijsbericht zien voor elke referentie die je hook heeft afgewezen, en dat vertelt je specifiek dat het was afgewezen vanwege een hook-falen.

Verder, als iemand probeert een bestand te wijzigen waar ze geen toegang tot hebben en een commit pushen die zo een bestand bevat, zien ze iets vergelijkbaar. Bijvoorbeeld, als een documentatie auteur een commit probeert te pushen waarin hij iets als de lib directory wijzigt, ziet deze

[POLICY] You do not have access to push to lib/test.rb

Vanaf nu, zolang als dat update script aanwezig is en uitvoerbaar, zal je repository nooit een commit bericht zonder jouw patroon bevatten, en je gebruikers worden beperkt in hun vrijheid.

==== Hooks aan de kant van het werkstation

Het nadeel van deze aanpak is het zeuren dat geheid zal beginnen zodra de commits van je gebruikers geweigerd worden. Het feit dat hun zorgzaam vervaardigde werk op het laatste moment pas geweigerd wordt kan enorm frustrerend en verwarrend zijn, ze zullen hun geschiedenis moeten aanpassen om het te corrigeren, wat niet altijd geschikt is voor de meer onzekere mensen.

Het antwoord op dit dilemma is een aantal werkstation hooks te leveren, die gebruikers kunnen gebruiken om hen te waarschuwen dat ze iets doen dat de server waarschijnlijk gaat weigeren. Op die manier kunnen ze alle problemen corrigeren voordat ze gaan committen en voordat die problemen lastiger te herstellen zijn. Omdat hooks niet overgebracht worden bij het klonen van een project, moet je deze scripts op een andere manier distribueren en je gebruikers ze in hun .git/hooks map laten zetten en ze uitvoerbaar maken. Je kunt deze hooks in je project of in een apart project distribueren, maar Git zal ze niet automatisch opzetten.

Om te beginnen zou je de commit boodschap moeten controleren vlak voordat iedere commit opgeslagen wordt, zodat je weet dat de server je wijzigingen niet gaat weigeren omdat de commit boodschap een verkeerd formaat heeft. Om dit te doen, kun je de commit-msg hook toevoegen. Als je dat de commit boodschap laat lezen uit het bestand dat als eerste argument opgegeven wordt, en dat vergelijkt met het patroon dan kun je Git dwingen om de commit af te breken als het niet juist is:

#!/usr/bin/env ruby
message_file = ARGV[0]
message = File.read(message_file)

$regex = /\[ref: (\d+)\]/

if !$regex.match(message)
  puts "[POLICY] Your message is not formatted correctly"
  exit 1
end

Als dat script op z’n plaats staat (in .git/hooks/commit-msg), uitvoerbaar is en je commit met een verkeerd geformateerd bericht, dan zie je dit:

$ git commit -am 'test'
[POLICY] Your message is not formatted correctly

In dat geval is er geen commit gedaan. Maar als je bericht het juiste patroon bevat, dan staat Git je toe te committen:

$ git commit -am 'test [ref: 132]'
[master e05c914] test [ref: 132]
 1 file changed, 1 insertions(+), 0 deletions(-)

Vervolgens wil je er zeker van zijn dat je geen bestanden buiten je ACL scope aanpast. Als de .git directory van je project een kopie van het ACL bestand bevat dat je eerder gebruikte, dan zal het volgende pre-commit script die beperkingen voor je controleren:

#!/usr/bin/env ruby

$user    = ENV['USER']

# [ insert acl_access_data method from above ]

# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
  access = get_acl_access_data('.git/acl')

  files_modified = `git diff-index --cached --name-only HEAD`.split("\n")
  files_modified.each do |path|
    next if path.size == 0
    has_file_access = false
    access[$user].each do |access_path|
    if !access_path || (path.index(access_path) == 0)
      has_file_access = true
    end
    if !has_file_access
      puts "[POLICY] You do not have access to push to #{path}"
      exit 1
    end
  end
end

check_directory_perms

Dit is grofweg hetzelfde script als aan de server kant, maar met twee belangrijke verschillen. Als eerste staat het ACL bestand op een andere plek, omdat dit script vanuit je werkdirectory draait, en niet vanuit je .git directory. Je moet het pad naar het ACL bestand wijzigen van dit

access = get_acl_access_data('acl')

naar dit:

access = get_acl_access_data('.git/acl')

Het andere belangrijke verschil is de manier waarop je een lijst krijgt met bestanden die gewijzigd is. Omdat de server kant methode naar de log van commits kijkt en nu je commit nog niet opgeslagen is, moet je de bestandslijst in plaats daarvan uit het staging area halen. In plaats van

files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`

moet je dit gebruiken

files_modified = `git diff-index --cached --name-only HEAD`

Maar dat zijn de enige twee verschillen - verder werkt het script op dezelfde manier. Een aandachtspunt is dat het van je verlangt dat je lokaal werkt als dezelfde gebruiker als waarmee je pusht naar de remote machine. Als dat anders is, moet je de $user variabele handmatig instellen.

Het andere wat je moet doen is het controleren dat je niet probeert non-fast-forward referenties te pushen. Om een referentie te krijgen dat non-fast-forward is, moet je voorbij een commit rebasen die je al gepusht hebt, of een andere lokale branch naar dezelfde remote branch proberen te pushen.

We mogen aannemen dat de server l ingericht met receive.denyDeletes en receive.denyNonFastForwards om ditbeleid af te dwingen, dus het enige wat je kunt proberen af te vangen het abusievelijk rebasen van commits die je al gepusht hebt.

Hier is een voorbeeld pre-rebase script dat daarop controleert. Het haalt een lijst met alle commits die je op het punt staat te herschrijven, en controleert of ze al ergens bestaan in één van je remote referenties. Als het er een ziet die bereikbaar is vanuit een van je remote referenties, dan stopt het de rebase.

#!/usr/bin/env ruby

base_branch = ARGV[0]
if ARGV[1]
  topic_branch = ARGV[1]
else
  topic_branch = "HEAD"
end

target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n")
remote_refs = `git branch -r`.split("\n").map { |r| r.strip }

target_shas.each do |sha|
  remote_refs.each do |remote_ref|
    shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
    if shas_pushed.split("\n").include?(sha)
      puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
      exit 1
    end
  end
end

Dit script gebruikt een syntax dat niet behandeld is in Revisie Selectie. Je krijgt een lijst van commits die al gepusht zijn door dit uit te voeren:

`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`

De SHA^@ syntax wordt vervangen door alle ouders van die commit. Je bent op zoek naar een commit die bereikbaar is vanuit de laatste commit op de remote en die niet bereikbaar is vanuit enige ouder van alle SHA’s die je probeert te pushen - wat betekent dat het een fast-forward is.

Het grote nadeel van deze aanpak is dat het erg traag kan zijn en vaak onnodig is, als je de push niet probeert te forceren met de -f optie, dan zal de server je al waarschuwen en de push niet accepteren. Maar, het is een aardige oefening en kan je in theorie helpen om een rebase te voorkomen die je later zult moeten herstellen.

=== Samenvatting

We hebben de meeste van de belangrijkste manieren behandeld waarop je jouw Git werkstation en server kunt aanpassen zodat deze jouw workflow en projecten het beste ondersteunt. Je hebt over allerhande soorten configuratie instellingen kunnen lezen, bestands-gebaseerde attributen en event hooks, en je hebt een voorbeeld beleidsbewakende server gebouwd. Je zou nu in staat moeten zijn om Git in bijna alle soorten workflows die je kunt verzinnen in te passen.

== Git en andere systemen

Het is geen perfecte wereld. Meestal kan je niet meteen elk project waar je mee in aanraking komt omzetten naar Git. Soms zit je vast op een project dat een ander VCS gebruikt, en wensen dat het Git zou zijn. We zullen het eerste deel van dit hoofdstuk hebben over over manieren om Git als client te gebruiken als het project waar je op werkt op een ander systeem wordt gehost.

Op een gegeven moment zal je een bestaande project misschien willen omzetten naar Git. Het tweede gedeelte van dit hoofdstuk beschrijft hoe je projecten naar Git kunt migreren uit verschillende specifieke systemen, zowel als een manier die je kan helpen als er geen standaard import hulpmiddelen zijn.

=== Git als een client

Git biedt zo’n prettige ervaring voor ontwikkelaars dat veel mensen een manier hebben gevonden om het te gebruiken op hun werkstation, zelfs als de rest van hun team een compleet andere VCS gebruikt. Er zijn een aantal van deze adapters, die “bridges” worden genoemd, beschikbaar. We zullen hier degenen behandelen die je het meest waarschijnlijk in de praktijk zult tegenkomen.

==== Git en Subversion

Een groot deel van open source ontwikkel projecten en een groot aantal van bedrijfsprojecten gebruiken Subversion om hun broncode te beheren. Het bestaat meer dan 10 jaar en voor een groot gedeelte van die tijd was het de de facto VCS keuze voor open source projecten. Het lijkt in vele aspecten ook erg op CVS, die daarvoor een grote naam was in de source-control wereld.

Een van de mooie functies van Git is de bidirectionele brug naar Subversion genaamd git svn. Dit instrument staat je toe om Git te gebruiken als een volwaardig client naar een Subversion server, zodat je alle lokale mogelijkheden van Git kunt gebruiken en dan naar een Subversion server kunt pushen alsof je lokaal Subversion gebruikt. Dit houdt in dat je lokaal kunt branch en mergen, de staging area kunt gebruiken, rebasing en cherry-picking kunt gebruiken, enzovoorts, terwijl de mensen waarmee je samenwerkt blijven werken met hun middelen uit de stenen tijdperk. Het is een goede manier om Git in de bedrijfsomgeving te smokkelen en je mede-ontwikkelaars te helpen om effectiever te worden terwijl jij een lobby voert om de infrastructuur zover te krijgen dat Git volledig wordt ondersteund. De Subversion bridge is de manier om naar de DVCS wereld te groeien.

===== git svn

Het basis commando in Git voor alle Subversion bridging commando’s is git svn. Het accepteert best wel veel commando’s, dus we laten de meest gebruikte zien terwijl we een aantal eenvoudige workflows behandelen.

Het is belangrijk om op te merken dat wanneer je git svn gebruikt, je interacteert met Subversion, wat een systeem is dat behoorlijk anders werkt dan Git. Alhoewel je lokaal kunt branchen en mergen, is het over het algemeen het beste om je historie zo lineair als mogelijk te houden door je werk te rebasen, en te vermijden dat je zaken doet als het tegelijkertijd interacteren met een remote repository in Git.

Ga niet je historie overschrijven en dan weer proberen te pushen, en push niet tegelijk naar een parallelle Git repository om samen te werken met andere Git ontwikkelaars. Subversion kan alleen maar een lineaire historie aan, en het is eenvoudig om het in de war te brengen. Als je met een team samenwerkt, en sommigen gebruiken SVN en anderen gebruiken Git, zorg er dan voor dat iedereen de SVN server gebruikt om samen te werken - als je dit doet wordt je leven een stuk aangenamer.

===== Inrichten

Om deze functionaliteit te laten zien, heb je een typische SVN repository nodig waar je schrijfrechten op hebt. Als je deze voorbeelden wilt kopiëren, moet je een schrijfbare kopie maken van een SVN test repository. Om dat eenvoudig te doen, kan je een tool svnsync genaamd gebruiken die bij Subversion wordt geleverd.

Om het te volgen, moet je eerst een nieuwe lokale Subversion repository maken:

$ mkdir /tmp/test-svn
$ svnadmin create /tmp/test-svn

Daarna moet je alle gebruikers toestaan om revprops te wijzigen - een makkelijke manier is om een pre-revprop-change toe te voegen die altijd met 0 afsluit:

$ cat /tmp/test-svn/hooks/pre-revprop-change
#!/bin/sh
exit 0;
$ chmod +x /tmp/test-svn/hooks/pre-revprop-change

Je kunt dit project nu synchroniseren naar je lokale machine door svnsync init aan te roepen met de naar en van repositories.

$ svnsync init file:///tmp/test-svn \
  http://your-svn-server.example.org/svn/

Dit richt de properties in om de synchronisatie te laten lopen. Je kunt dan de code clonen door het volgende te doen

$ svnsync sync file:///tmp/test-svn
Committed revision 1.
Copied properties for revision 1.
Transmitting file data .............................[...]
Committed revision 2.
Copied properties for revision 2.
[…]

Alhoewel deze handeling maar enkele minuten in beslag neemt, zal het proces, als je de orginele repository naar een andere remote repository probeert te kopiëren, bijna een uur in beslag nemen, zelfs als er minder dan 100 commits zijn. Subversion moet een revisie per keer kopiëren en deze dan naar de andere repository pushen - het is belachelijk inefficiënt, maar het is de enige makkelijke manier om dit te doen.

===== Aan de gang gaan

Nu je een Subversion repository hebt waar je schrijfrechten op hebt, kan je een typische workflow gaan volgen. Je begint met het git svn clone commando, die een hele Subversion repository importeert naar een lokale Git repository. Onthoud dat als je van een echte gehoste Subversion repository importeert, je de file:///tmp/test-svn moet vervangen met de URL van je Subversion repository:

$ git svn clone file:///tmp/test-svn -T trunk -b branches -t tags
Initialized empty Git repository in /private/tmp/progit/test-svn/.git/
r1 = dcbfb5891860124cc2e8cc616cded42624897125 (refs/remotes/origin/trunk)
    A	m4/acx_pthread.m4
    A	m4/stl_hash.m4
    A	java/src/test/java/com/google/protobuf/UnknownFieldSetTest.java
    A	java/src/test/java/com/google/protobuf/WireFormatTest.java
...
r75 = 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae (refs/remotes/origin/trunk)
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/my-calc-branch, 75
Found branch parent: (refs/remotes/origin/my-calc-branch) 556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae
Following parent with do_switch
Successfully followed parent
r76 = 0fb585761df569eaecd8146c71e58d70147460a2 (refs/remotes/origin/my-calc-branch)
Checked out HEAD:
  file:///tmp/test-svn/trunk r75

Dit roept het equivalent van twee commando’s aan - git svn init gevolgd door git svn fetch - op de URL die je opgeeft. Dit kan even duren. Bijvoorbeeld, als het project maar ongeveer 75 commits heeft en de codebase is niet zo groot, maar Git moet elke versie uitchecken, een voor een, en deze allemaal individueel committen. Voor een project met honderden of duizenden commits, kan dit letterlijk uren of zelfs dagen in beslag nemen voor het klaar is.

Het -T trunk -b branches -t tags gedeelte vertelt Git dat deze Subversion repository de normale branch en tag conventies volgt. Als je jouw trunk, branches of tags andere namen geeft, kan je deze opties veranderen. Omdat dit zo gewoonlijk is, kan je dit gehele gedeelte vervangen met -s, wat standaard indeling betekent en al die opties impliceert. Het volgende commando doet hetzelfde:

$ git svn clone file:///tmp/test-svn -s

Op dit punt zou je een valide Git repository moeten hebben die jouw branches en tags heeft geïmporteerd.

$ git branch -a
* master
  remotes/origin/my-calc-branch
  remotes/origin/tags/2.0.2
  remotes/origin/tags/release-2.0.1
  remotes/origin/tags/release-2.0.2
  remotes/origin/tags/release-2.0.2rc1
  remotes/origin/trunk

Merk op hoe dit instrument Subversion tags als remote refs beheert. Laten we het Git binnenwerk commando show-ref gebruiken om het iets nauwkeuriger te bekijken:

$ git show-ref
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/heads/master
0fb585761df569eaecd8146c71e58d70147460a2 refs/remotes/origin/my-calc-branch
bfd2d79303166789fc73af4046651a4b35c12f0b refs/remotes/origin/tags/2.0.2
285c2b2e36e467dd4d91c8e3c0c0e1750b3fe8ca refs/remotes/origin/tags/release-2.0.1
cbda99cb45d9abcb9793db1d4f70ae562a969f1e refs/remotes/origin/tags/release-2.0.2
a9f074aa89e826d6f9d30808ce5ae3ffe711feda refs/remotes/origin/tags/release-2.0.2rc1
556a3e1e7ad1fde0a32823fc7e4d046bcfd86dae refs/remotes/origin/trunk

Git doet dit niet als het van een Git server kloont; zo ziet een repository met tags eruit na een verse clone:

$ git show-ref
c3dcbe8488c6240392e8a5d7553bbffcb0f94ef0 refs/remotes/origin/master
32ef1d1c7cc8c603ab78416262cc421b80a8c2df refs/remotes/origin/branch-1
75f703a3580a9b81ead89fe1138e6da858c5ba18 refs/remotes/origin/branch-2
23f8588dde934e8f33c263c6d8359b2ae095f863 refs/tags/v0.1.0
7064938bd5e7ef47bfd79a685a62c1e2649e2ce7 refs/tags/v0.2.0
6dcb09b5b57875f334f61aebed695e2e4193db5e refs/tags/v1.0.0

Git fetched de tags direct naar refs/tags, in plaats van ze te behandelen als remote branches.

===== Terug naar Subversion committen

Nu je een werkende repository hebt, kan je wat werk doen op het project en je commits terug stroomopwaarts pushen, waarbij je Git feitelijk als een SVN client gebruikt. Als je een van de bestanden hebt gewijzigd en deze commit, heb je een commit die lokaal in Git bestaat, maar die niet op de Subversion server bestaat:

$ git commit -am 'Adding git-svn instructions to the README'
[master 4af61fd] Adding git-svn instructions to the README
 1 file changed, 5 insertions(+)

Nu moet je jouw wijziging stroomopwaarts pushen. Merk op dat dit de manier waarop je met Subversion werkt wijzigt - je kunt verschillende commits offline doen en ze dan allemaal in een keer naar de Subversion server pushen. Om naar een Subversion server te pushen, roep je het git svn dcommit commando aan:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r77
    M	README.txt
r77 = 95e0222ba6399739834380eb10afcd73e0670bc5 (refs/remotes/origin/trunk)
No changes between 4af61fd05045e07598c553167e0f31c84fd6ffe1 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Dit pakt alle commits die je hebt gemaakt bovenop de Subversion server code, maakt een Subversion commit voor elk van deze, en herschrijft je lokale Git commit om een unieke referentienummer in te voegen. Dit is belangrijk omdat dit betekent dat al de SHA-1 checksums voor je lokale commits zal wijzigen. Deels om deze reden, is het werken met Git-gebaseerde remote versies van je project tegelijk met een Subversion server geen goed idee. Als je naar de laatste commit kijkt, kan je het nieuwe git-svn-id zien die was toegevoegd:

$ git log -1
commit 95e0222ba6399739834380eb10afcd73e0670bc5
Author: ben <ben@0b684db3-b064-4277-89d1-21af03df0a68>
Date:   Thu Jul 24 03:08:36 2014 +0000

    Adding git-svn instructions to the README

    git-svn-id: file:///tmp/test-svn/trunk@77 0b684db3-b064-4277-89d1-21af03df0a68

Merk op dat de SHA-1 checksum die oorspronkelijk begon met 4af61fd toen je ging committen nu begint met 95e0222. Als je zowel naar een Git server als een Subversion server wilt pushen, moet je eerst anar de Subversion server pushen (dcommit), omdat deze actie je commit gegevens wijzigt.

===== Nieuwe wijzigingen pullen

Als je met andere ontwikkelaars werkt, dan zal op een gegeven moment iemand van jullie gaan pushen, en dan zal de ander een wijziging proberen te pushen die conflicteert. Die wijziging zal afgewezen worden totdat je hun werk merget. In git svn ziet dit er zo uit:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: d5837c4b461b7c0e018b49d12398769d2bfc240a and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 f414c433af0fd6734428cf9d2a9fd8ba00ada145 c80b6127dd04f5fcda218730ddf3a2da4eb39138 M	README.txt
Current branch master is up to date.
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

Om deze situatie op te lossen, kan je git svn rebase uitvoeren, die alle wijzigingen op de server pullt die je nog niet hebt, en rebaset al het werk dat je hebt bovenop hetgeen op de server is:

$ git svn rebase
Committing to file:///tmp/test-svn/trunk ...

ERROR from SVN:
Transaction is out of date: File '/trunk/README.txt' is out of date
W: eaa029d99f87c5c822c5c29039d19111ff32ef46 and refs/remotes/origin/trunk differ, using rebase:
:100644 100644 65536c6e30d263495c17d781962cfff12422693a b34372b25ccf4945fe5658fa381b075045e7702a M	README.txt
First, rewinding head to replay your work on top of it...
Applying: update foo
Using index info to reconstruct a base tree...
M	README.txt
Falling back to patching base and 3-way merge...
Auto-merging README.txt
ERROR: Not all changes have been committed into SVN, however the committed
ones (if any) seem to be successfully integrated into the working tree.
Please see the above messages for details.

Nu is al jouw werk uitgevoerd bovenop hetgeen wat op de Subversion server staat, dus je kunt met goed gevolg dcommit doen:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	README.txt
Committed r85
    M	README.txt
r85 = 9c29704cc0bbbed7bd58160cfb66cb9191835cd8 (refs/remotes/origin/trunk)
No changes between 5762f56732a958d6cfda681b661d2a239cc53ef5 and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Merk op dat, in tegenstelling tot Git die vereist dat je werk van stroomopwaarts dat je lokaal nog niet hebt merget voordat je kunt pushen, git svn je dat alleen verplicht te doen als de wijzigingen conflicteren (vergelijkbaar met hoe Subversion werkt). Als iemand een wijziging op een bestand pushed en jij pushed een wijziging op een ander bestand, zal je dcommit prima werken:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	configure.ac
Committed r87
    M	autogen.sh
r86 = d8450bab8a77228a644b7dc0e95977ffc61adff7 (refs/remotes/origin/trunk)
    M	configure.ac
r87 = f3653ea40cb4e26b6281cec102e35dcba1fe17c4 (refs/remotes/origin/trunk)
W: a0253d06732169107aa020390d9fefd2b1d92806 and refs/remotes/origin/trunk differ, using rebase:
:100755 100755 efa5a59965fbbb5b2b0a12890f1b351bb5493c18 e757b59a9439312d80d5d43bb65d4a7d0389ed6d M	autogen.sh
First, rewinding head to replay your work on top of it...

Dit is belangrijk om te onthouden, omdat de uitkomst een project status is die niet eerder bestond op een van jullie computers toen jij pushde. Als de wijzigingen niet compatible zijn, maar geen conflict veroorzaken, kan je problemen krijgen die moeilijk te diagnostiseren zijn. Dit verschilt met de manier van werken met een Git server - in Git kan je de situatie op je lokale werkstation testen voordat je het publiceert, terwijl in SVN, je er nooit zeker van kunt zijn dat de situatie direct voor en na een commit gelijk zijn.

Je kunt ook dit commando aanroepen om wijzigingen binnen te halen van de Subversion server, zelfs als je zelf nog niet klaar bent om te committen. Je kunt git svn fetch aanroepen om de nieuwe gegevens te pakken, maar git svn rebase doet de fetch en werkt daarna je lokale commits bij.

$ git svn rebase
    M	autogen.sh
r88 = c9c5f83c64bd755368784b444bc7a0216cc1e17b (refs/remotes/origin/trunk)
First, rewinding head to replay your work on top of it...
Fast-forwarded master to refs/remotes/origin/trunk.

Regelmatig git svn rebase aanroepen verzekert je ervan dat je code altijd is bijgewerkt. Je moet er echter zeker van zijn dat je werk directory schoon is als je dit aanroept. Als je lokale wijzigingen hebt, moet je je werk stashen of tijdelijk committen voordat je git svn rebase aanroept - anders zal het commando stoppen als het ziet dat de rebase in een merge conflict zal resulteren.

===== Git branching problemen

Als je je op je gemak voelt met een Git workflow, zal je waarschijnlijk topic branches maken, daar werk op doen en ze dan weer in mergen. Als je anar een Subversion server pushed met git svn, dan is het waarschijnlijk verstandig om je werk elke keer op een enkele branch te rebasen in plaats van branches samen te mergen. De achterliggende reden om rebasen te gebruiken is dat Subversion een lineaire historie kent en niet met merges omgaat zoals Git dit doet, dus git svn volgt alleen de eerste ouder als het de snapshots naar Subversion commits converteert.

Stel dat je historie er als volgt uitziet: je hebt een experiment-branch gemaakt, heb daar twee commits gedaan, en deze daarna terug in master gemerged. Als je dcommit doet, zie je uitvoer als dit:

$ git svn dcommit
Committing to file:///tmp/test-svn/trunk ...
    M	CHANGES.txt
Committed r89
    M	CHANGES.txt
r89 = 89d492c884ea7c834353563d5d913c6adf933981 (refs/remotes/origin/trunk)
    M	COPYING.txt
    M	INSTALL.txt
Committed r90
    M	INSTALL.txt
    M	COPYING.txt
r90 = cb522197870e61467473391799148f6721bcf9a0 (refs/remotes/origin/trunk)
No changes between 71af502c214ba13123992338569f4669877f55fd and refs/remotes/origin/trunk
Resetting to the latest refs/remotes/origin/trunk

Het aanroepen van dcommit op een branch met gemergde historie werkt prima, behalve als je naar je Git project historie kijkt, heeft het geen van beide commits die je op de experiment-branch gemaakt hebt herschreven - in plaats daarvan komen al deze wijzigingen als een enkele merge commit in de SVN versie.

Als iemand anders dat werk kloont, is alles wat ze zien de merge commit met al het werk erin gepropt, alsof je git merge --squash aangeroepen hebt; ze zien niet de commit gegevens over waar het vandaan kwam of wanneer het was gecommit.

===== Subversion Branching

Branches maken in Subversion is niet hetzelfde als branches maken in Git; als je kunt voorkomen dat je het vaak doet, is dat eigenlijk wel het beste. Echter, je kunt in Subversion branches maken en ernaar committen met git svn.

===== Een nieuwe SVN branch maken

Om een nieuwe branch in Subversion te maken, roep je git svn branch [branchnaam] aan:

$ git svn branch opera
Copying file:///tmp/test-svn/trunk at r90 to file:///tmp/test-svn/branches/opera...
Found possible branch point: file:///tmp/test-svn/trunk => file:///tmp/test-svn/branches/opera, 90
Found branch parent: (refs/remotes/origin/opera) cb522197870e61467473391799148f6721bcf9a0
Following parent with do_switch
Successfully followed parent
r91 = f1b64a3855d3c8dd84ee0ef10fa89d27f1584302 (refs/remotes/origin/opera)

Dit is het equivalent van het svn copy trunk branches/opera commando in Subversion en wordt uitgevoerd op de Subversion server. Het is belangrijk op te merken dat dit je niet uitcheckt in die branch; als je op dat moment gaat committen, dan zal die commit naar trunk gaan op de server, niet opera.

===== Actieve branches switchen

Git probeert uit te vinden naar welke branch je dcommits gaan door te kijken naar de punt van al je Subversion branches in je historie - je zou er maar een moeten hebben, en het zou de laatste moeten zijn met een git-svn-id in je huidige branch historie.

Als je op meer dan een branch tegelijk wilt werken, kan je lokale branches inrichten om te dcommit-ten naar specifieke Subversion branches door ze te beginnen op de geïmporteerde Subversion commit voor die branch. Als je een opera-branch wilt waar je apart op kunt werken, kan je het volgende aanroepen:

$ git branch opera remotes/origin/opera

Vervolgens, als je je opera-branch wilt mergen naar trunk (je master-branch), kan je dat doen met een gewone git merge. Maar als je een beschrijvende commit bericht (via -m) moeten meegeven, anders zal de merge “Merge branch opera” vermelden in plaats van iets nuttigs.

Onthoud dat hoewel je git merge gebruikt om deze handeling uit te voeren, en de merge waarschijnlijk veel makkelijker zal zijn dan het in Subversion zou zijn (omdat Git automatisch de juiste merge basis voor je zal uitzoeken), dit geen normale Git merge commit is. Je zult deze gegevens naar een Subversion server terug moeten pushen die niet in staat is een commit te verwerken die naar meer dan een ouder terug te herleiden is; dus, nadat je het gepusht hebt, zal het eruit zien als een enkele commit waarin al het werk van een andere branch is gepropt onder een enkele commit. Nadat je een branch in de een andere hebt gemerged, kan je niet simpelweg doorgaan met werken op die branch, zoals je in Git zou doen. Het dcommit commando dat je hebt aangeroepen verwijdert alle informatie die aangeeft welke branch erin was gemerged, dus daarop volgende merge-basis berekeningen zullen fout gaan - de dcommit maakt dat je git merge resultaat eruit ziet alsof je git merge --squash had aangeroepen. Jammergenoeg is er geen goede manier om deze situatie te vermijden - Subversion kan deze informatie niet opslaan, dus je zult altijd gehinderd worden door de beperkingen zolang je het gebruikt als je server. Om problemen te voorkomen, moet je de lokale branch verwijderen (in dit geval, opera) nadat je het in de trunk hebt gemerged.

===== Subversion commando’s

De git svn toolset biedt een aantal commando’s om de overgang naar Git te vergemakkelijken door wat functionaliteit te leveren die vergelijkbaar is met wat je in Subversion had. Hie zijn een paar commando’s die je geven wat Subversion normaalgesproken deed.

====== Historie op de manier van SVN

Als je Subversion gewend bent en je wilt je historie zien op de uitvoermanier van SVN, kan je git svn log aanroepen om je commit historie in SVN formaat te zien.

$ git svn log
------------------------------------------------------------------------
r87 | schacon | 2014-05-02 16:07:37 -0700 (Sat, 02 May 2014) | 2 lines

autogen change

------------------------------------------------------------------------
r86 | schacon | 2014-05-02 16:00:21 -0700 (Sat, 02 May 2014) | 2 lines

Merge branch 'experiment'

------------------------------------------------------------------------
r85 | schacon | 2014-05-02 16:00:09 -0700 (Sat, 02 May 2014) | 2 lines

updated the changelog

Je moet twee belangrijke dingen weten over git svn log. Ten eerste, het werkt offline, in tegenstelling tot het echte svn log commando, die de Subversion server om de gegevens vraagt. Ten tweede, het laat je alleen commits zien die gecommit zijn naar de Subversion server. Lokale Git commits die je niet ge-dcommit hebt worden niet getoond; noch de commits die mensen in de tussentijd naar de Subversion server hebben gemaakt. Je moet het meer zien als de laatst bekende stand van commits op de Subversion server.

====== SVN annotatie

Net zoals het git svn log commando het svn log commando offline simuleert, kan je het equivalent van svn annotate krijgen door git svn blame [FILE] aan te roepen. De uitvoer ziet er als volgt uit:

$ git svn blame README.txt
 2   temporal Protocol Buffers - Google's data interchange format
 2   temporal Copyright 2008 Google Inc.
 2   temporal http://code.google.com/apis/protocolbuffers/
 2   temporal
22   temporal C++ Installation - Unix
22   temporal =======================
 2   temporal
79    schacon Committing in git-svn.
78    schacon
 2   temporal To build and install the C++ Protocol Buffer runtime and the Protocol
 2   temporal Buffer compiler (protoc) execute the following:
 2   temporal

Nogmaals, het laat je niet de commits zien die je lokaal in Git gemaakt hebt of die in de tussentijd naar Subversion zijn gepusht.

====== SVN server informatie

Je kunt ook de zelfde soort informatie krijgen die svn info je geeft door git svn info aan te roepen:

$ git svn info
Path: .
URL: https://schacon-test.googlecode.com/svn/trunk
Repository Root: https://schacon-test.googlecode.com/svn
Repository UUID: 4c93b258-373f-11de-be05-5f7a86268029
Revision: 87
Node Kind: directory
Schedule: normal
Last Changed Author: schacon
Last Changed Rev: 87
Last Changed Date: 2009-05-02 16:07:37 -0700 (Sat, 02 May 2009)

Dit is gelijk aan blame en log in die zin dat het offline loopt en dat het alleen is bijgewerkt tot de laatste keer dat je met de Subversion server contact had.

====== Negeren wat Subversion negeert

Als je een Subversion repository cloont die een svn:ignore property ergens heeft, zal je waarschijnlijk vergelijkbare .gitignore bestanden willen krijgen zodat je niet per ongeluk bestanden commit die je niet had moeten doen. git svn heeft twee commando’s die je helpen met dit scenario. De eerste is git svn create-ignore, die automatisch vergelijkbare .gitignore bestanden voor je maakt zodat je volgende commit deze kan bevatten.

Het tweede commando is git svn show-ignore, die de regels die je in een .gitignore bestand moet zetten naar stdout uitvoert, zodat je deze uitvoer naar je het exclusie bestand in je project kunt leiden:

$ git svn show-ignore > .git/info/exclude

Op deze manier, vervuil je het project niet met .gitignore bestanden. Dit is een goed alternatief als je de enige Git gebruiker in een Subversion team bent, en je teamgenoten geen .gitignore bestanden in het project willen hebben.

===== Git-Svn samenvatting

De git svn instrumenten zijn nuttig als je vastzit aan een Subversion server, of op een andere manier in een ontwikkelteam zit waar het gebruik van een Subversion server noodzakelijk is. Je moet het echter als een gemankeerde Git beschouwen, of je loopt al snel tegen terminologie-verschillen aan die jou en je medewerkers zullen verwarren. Probeer, om niet in de problemen te komen, de volgende richtlijnen te volgen:

  • Houd een lineaire Git historie aan die geen merge commits bevat die door git merge zijn aangemaakt. Rebase al het werk die je buiten je hoofd-branch doet hierop terug; merge het niet in.

  • Richt niets in voor het samenwerken op een aparte Git server, en werk niet samen met zo’n server. Je kunt er misschien een bijhouden om clones voor nieuwe ontwikkelaars te versnellen, maar ga er niets naar pushen dat geen git-svn-id regel heeft. Je zou zelfs een pre-receive hook kunnen aanmaken die controleert dat elke commit bericht op een git-svn-id controleert en pushes afwijst die commits bevatten die dit niet hebben.

Als je deze richtlijnen volgt, wordt het werken met een Subversion server misschien iets dragelijker. Echter, als het ook maar enigszins mogelijk is om naar een echte Git server te gaan, zal dit je team veel meer opleveren.

==== Git en Mercurial

Het DVCS universum is groter dan alleen Git. Eigenlijk zijn er vele andere systemen in deze ruimte, elk met hun eigen aanpak om hoe op de juiste manier om te gaan met gedistribueerd versiebeheer. Buiten Git, is de meest populaire Mercurial, en de twee zijn op vele vlakken erg vergelijkbaar.

Het goede nieuws is, als je het gedrag van Git aan de kant van het werkstation de voorkeur geeft, maar als je werkt met een project waarvan de broncode wordt beheerd met Mercurial, dat er een manier is om Git als een client voor een repository die op een Mercurial-host draait te gebruiken. Omdat de manier voor Git om te praten met server repositories via remotes loopt, moet het niet als een verrassing komen dat deze brug ("bridge") geïmplementeerd is als een remote helper. De naam van het project is git-remote-hg, en het kan worden gevonden op https://github.com/felipec/git-remote-hg.

===== git-remote-hg

Allereerst moet je git-remote-hg installeren. Dit houdt niet veel meer in dan het bestand ergens op je pad neer te zetten, op deze manier:

$ curl -o ~/bin/git-remote-hg \
  https://raw.githubusercontent.com/felipec/git-remote-hg/master/git-remote-hg
$ chmod +x ~/bin/git-remote-hg

…aangenomen dat ~/bin op je $PATH staat. Git-remote-hg heeft een andere afhankelijkheid: de mercurial library voor Python. Als je Python geïnstalleerd hebt, is dit zo simpel als:

$ pip install mercurial

(Als je geen Python geïnstalleerd hebt, bezoek dan https://www.python.org/ en haal dat eerst op.)

Het laatste wat je nodig hebt is de Mercurial client. Ga naar https://www.mercurial-scm.org/ en installeer dit als je dat al niet hebt gedaan.

Nu is alles klaar voor gebruik. Al wat je nodig hebt is een Mercurial repository waar je naar kunt pushen. Gelukkig kan elke Mercurial repository zich op deze manier gedragen, dus we hoeven alleen de "hello world" repository die iedereen gebruikt om Mercurial te leren gebruiken:

$ hg clone http://selenic.com/repo/hello /tmp/hello

===== Aan de gang gaan

Nu we een passende “server-side” repository hebben, kunnen we een normale workflow gaan volgen. Zoals je zult zien zijn deze twee systemen voldoende vergelijkbaar dat er niet veel wrijving zal zijn.

Zoals altijd met Git, gaan we eerst clonen:

$ git clone hg::/tmp/hello /tmp/hello-git
$ cd /tmp/hello-git
$ git log --oneline --graph --decorate
* ac7955c (HEAD, origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master, master) Create a makefile
* 65bb417 Create a standard "hello, world" program

Je zult opgemerkt hebben dat je bij het werken met een Mercurial repository het standaard git clone commando gebruikt. Dat is omdat git-remote-hg op een behoorlijk laag niveau werkt, en gebruik maakt van een vergelijkbaar mechanisme als het HTTP/S protocol dat in Git geïmplementeerd is (remote helpers). Omdat Git en Mercurial beide zijn ontworpen vanuit het principe dat elk werkstation een volledige kopie van de historie van de repository heeft, maakt dit commando een volledige kloon, inclusief alle historie van het project en doet dit redelijk snel.

Het log commando laat twee commits zien, maar naar de laatste daarvan wordt door een hele sloot refs verwezen. Nu is het zo dat een aantal van deze er eigenlijk helemaal niet zijn. Laten we kijken naar wat er eigenlijk in de .git directory staat:

$ tree .git/refs
.git/refs
├── heads
│   └── master
├── hg
│   └── origin
│       ├── bookmarks
│       │   └── master
│       └── branches
│           └── default
├── notes
│   └── hg
├── remotes
│   └── origin
│       └── HEAD
└── tags

9 directories, 5 files

Git-remote-hg probeert dingen meer letterlijk op z’n Gits te maken, maar onder de motorkap beheert het de conceptuele mapping tussen twee marginaal verschillende systemen. De refs/hg directory is waar de echte remote refs worden opgeslagen. Bijvoorbeeld, de refs/hg/origin/branche/default is een Git ref bestand die de SHA-1 bevat die begint met “ac7955c”, wat de commit is waar master naar wijst. Dus de refs/hg directory is een soort van nep refs/remotes/origin, maar het heeft het toegevoegde onderscheid tussen boekenleggers ("bookmarks") en branches.

Het notes/hg bestand is het beginpunt van hoe git-remote-hg Git commit hashes mapt op Mercurial changeset IDs. Laten we dit een beetje verkennen:

$ cat notes/hg
d4c10386...

$ git cat-file -p d4c10386...
tree 1781c96...
author remote-hg <> 1408066400 -0800
committer remote-hg <> 1408066400 -0800

Notes for master

$ git ls-tree 1781c96...
100644 blob ac9117f...	65bb417...
100644 blob 485e178...	ac7955c...

$ git cat-file -p ac9117f
0a04b987be5ae354b710cefeba0e2d9de7ad41a9

Dus refs/notes/hg wijst naar een tree, wat in de Git object database een lijst van andere objecten met namen is. git ls-tree produceert de mode, type, object hash en bestandsnamen voor items in een tree. Zodra we gaan graven naar een van de tree items, vinden we dat hierbinnen een blog zit met de naam “ac9117f” (de SHA-1 hash van de commit waar master naar wijst), met de inhoud “0a04b98” (wat de ID is van de Mercurial changeset aan de punt van de default-branch).

Het goede nieuwe is dat we ons over het algemeen hierover geen zorgen hoeven te maken. De typische workflow zal niet veel verschillen van het werken met een Git remote.

Er is een extra onderwerp waar we even aandacht aan moeten schenken voordat we doorgaan: ignores. Mercurial en Git gebruiken een erg vergelijkbare mechanisme hiervoor, maar het is erg aannemelijk dat je een .gitignore niet echt in een Mercurial repository wilt committen. Gelukkig heeft Git een manier om bestanden te negeren die lokaal is voor een repository die op schijf staat, en het formaat van Mercurial is compatibel met die van Git, dus je moet het ernaartoe kopiëren:

$ cp .hgignore .git/info/exclude

Het .git/info/exclude bestand gedraagt zich als een .gitignore, maar wordt niet in commits meegenomen.

===== Workflow

Laten we aannemen dat we wat werk gedaan hebben en een aantal commits op de master-branch uitgevoerd hebben, en je bent klaar om dit naar de remote repository te pushen. Zo ziet onze repository eruit op dit moment:

$ git log --oneline --graph --decorate
* ba04a2a (HEAD, master) Update makefile
* d25d16f Goodbye
* ac7955c (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Create a makefile
* 65bb417 Create a standard "hello, world" program

Onze master-branch loopt twee commits voor op origin/master, maar deze twee commits bestaan alleen op onze lokale machine. Laten we kijken of iemand anders belangrijk werk heeft gedaan in de tussentijd:

$ git fetch
From hg::/tmp/hello
   ac7955c..df85e87  master     -> origin/master
   ac7955c..df85e87  branches/default -> origin/branches/default
$ git log --oneline --graph --decorate --all
* 7b07969 (refs/notes/hg) Notes for default
* d4c1038 Notes for master
* df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
| * ba04a2a (HEAD, master) Update makefile
| * d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard "hello, world" program

Omdat we de --all vlag gebruikt hebben, zien we de “notes” refs die intern gebruikt worden door git-remote-hg, maar deze kunnen we negeren. De rest is zoals we hadden verwacht; origin/master is een commit naar voren gegaan, en onze histories zijn nu uiteengelopen. In tegenstelling tot andere systemen waar we mee werken in dit hoofdstuk, is Mercurial in staat om merges te verwerken, dus we gaan nu geen moeilijke dingen doen.

$ git merge origin/master
Auto-merging hello.c
Merge made by the 'recursive' strategy.
 hello.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --decorate
*   0c64627 (HEAD, master) Merge remote-tracking branch 'origin/master'
|\
| * df85e87 (origin/master, origin/branches/default, origin/HEAD, refs/hg/origin/branches/default, refs/hg/origin/bookmarks/master) Add some documentation
* | ba04a2a Update makefile
* | d25d16f Goodbye
|/
* ac7955c Create a makefile
* 65bb417 Create a standard "hello, world" program

Perfect. We laten de tests draaien en alles slaagt, dus we zijn klaar om ons werk te delen met de rest van het team:

$ git push
To hg::/tmp/hello
   df85e87..0c64627  master -> master

Dat is alles! Als je de Mercurial repository bekijkt, zul je zien dat dit gedaan heeft wat we mogen verwachten:

$ hg log -G --style compact
o    5[tip]:4,2   dc8fa4f932b8   2014-08-14 19:33 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   64f27bcefc35   2014-08-14 19:27 -0700   ben
| |    Update makefile
| |
| o  3:1   4256fc29598f   2014-08-14 19:27 -0700   ben
| |    Goodbye
| |
@ |  2   7db0b4848b3c   2014-08-14 19:30 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

De wijzigingsset ("changeset") met nummer 2 is gemaakt door Mercurial, en de changesets nummers 3 en 4 zijn door git-remote-hg gemaakt, door het pushen van de met Git gemaakte commits.

===== Branches en Boekenleggers ("Bookmarks")

Git heeft maar een soort branch: een referentie die verplaatst wordt als commits worden gemaakt. In Mercurial, worden deze soorten referenties een “bookmark” genoemd, en het gedraagt zich grotendeels vergelijkbaar met een branch in Git.

Het concept van een “branch” in Mercurial heeft iets meer voeten in de aarde. De branch waar een changeset op is gebaseerd wordt opgeslagen met de changeset, wat inhoudt dat het altijd in de historie van de repository aanwezig is. Hier is een voorbeeld van een commit die gemaakt is op de develop-branch:

$ hg log -l 1
changeset:   6:8f65e5e02793
branch:      develop
tag:         tip
user:        Ben Straub <ben@straub.cc>
date:        Thu Aug 14 20:06:38 2014 -0700
summary:     More documentation

Merk de regel op die begint met “branch”. Git kan dit niet echt simuleren (en hoeft dit ook niet; beide soorten branches kunnen in Git als een ref worden weergegeven), maar git-remote-hg moet het onderscheid kunnen maken, omdat Mercurial hier om geeft.

Het aanmaken van Mercurial bookmarks is net zo eenvoudig als het maken van Git branches. Aan de Git kant:

$ git checkout -b featureA
Switched to a new branch 'featureA'
$ git push origin featureA
To hg::/tmp/hello
 * [new branch]      featureA -> featureA

En dat is alles wat nodig is. Aan de kant van Mercurial ziet het er zo uit:

$ hg bookmarks
   featureA                  5:bd5ac26f11f9
$ hg log --style compact -G
@  6[tip]   8f65e5e02793   2014-08-14 20:06 -0700   ben
|    More documentation
|
o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
|\     Merge remote-tracking branch 'origin/master'
| |
| o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| |    update makefile
| |
| o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |    goodbye
| |
o |  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

Merk de nieuwe [featureA] tag in revisie 5 op. Deze gedragen zich precies als Git branches aan de Git kant, met een uitzondering: je kunt een bookmark niet van de Git kant verwijderen (dit is een beperking van remote helpers).

Je kunt ook op een “zwaargewicht” Mercurial branch werken: gewoon een branch in de branches naamsruimte ("namespace") zetten:

$ git checkout -b branches/permanent
Switched to a new branch 'branches/permanent'
$ vi Makefile
$ git commit -am 'A permanent change'
$ git push origin branches/permanent
To hg::/tmp/hello
 * [new branch]      branches/permanent -> branches/permanent

En hier is hoe het er aan de kant van Mercurial uit ziet:

$ hg branches
permanent                      7:a4529d07aad4
develop                        6:8f65e5e02793
default                        5:bd5ac26f11f9 (inactive)
$ hg log -G
o  changeset:   7:a4529d07aad4
|  branch:      permanent
|  tag:         tip
|  parent:      5:bd5ac26f11f9
|  user:        Ben Straub <ben@straub.cc>
|  date:        Thu Aug 14 20:21:09 2014 -0700
|  summary:     A permanent change
|
| @  changeset:   6:8f65e5e02793
|/   branch:      develop
|    user:        Ben Straub <ben@straub.cc>
|    date:        Thu Aug 14 20:06:38 2014 -0700
|    summary:     More documentation
|
o    changeset:   5:bd5ac26f11f9
|\   bookmark:    featureA
| |  parent:      4:0434aaa6b91f
| |  parent:      2:f098c7f45c4f
| |  user:        Ben Straub <ben@straub.cc>
| |  date:        Thu Aug 14 20:02:21 2014 -0700
| |  summary:     Merge remote-tracking branch 'origin/master'
[...]

De branchnaam “permanent” is opgeslagen met de changeset gemarkeerd met 7.

Aan de kant van Git, is het werken met beide stijlen branch gelijk: gewoon checkout, commit, fetch, merge, pull en push zoals je gewoonlijk zou doen. Een ding wat je moet weten is dat Mercurial het overschrijven van historie niet ondersteunt, alleen eraan toevoegen. Dit is hoe onze Mercurial repository eruit ziet na een interactieve rebase en een force-push:

$ hg log --style compact -G
o  10[tip]   99611176cbc9   2014-08-14 20:21 -0700   ben
|    A permanent change
|
o  9   f23e12f939c3   2014-08-14 20:01 -0700   ben
|    Add some documentation
|
o  8:1   c16971d33922   2014-08-14 20:00 -0700   ben
|    goodbye
|
| o  7:5   a4529d07aad4   2014-08-14 20:21 -0700   ben
| |    A permanent change
| |
| | @  6   8f65e5e02793   2014-08-14 20:06 -0700   ben
| |/     More documentation
| |
| o    5[featureA]:4,2   bd5ac26f11f9   2014-08-14 20:02 -0700   ben
| |\     Merge remote-tracking branch 'origin/master'
| | |
| | o  4   0434aaa6b91f   2014-08-14 20:01 -0700   ben
| | |    update makefile
| | |
+---o  3:1   318914536c86   2014-08-14 20:00 -0700   ben
| |      goodbye
| |
| o  2   f098c7f45c4f   2014-08-14 20:01 -0700   ben
|/     Add some documentation
|
o  1   82e55d328c8c   2005-08-26 01:21 -0700   mpm
|    Create a makefile
|
o  0   0a04b987be5a   2005-08-26 01:20 -0700   mpm
     Create a standard "hello, world" program

Changesets 8, 9 en 10 zijn gemaakt voor en behoren bij de permanent-branch, maar de oude changesets zijn er nog steeds. Dit kan erg verwarrend worden voor je teamgenoten die Mercurial gebruiken, dus probeer dit te vermijden.

===== Mercurial samenvatting

Git en Mercurial zijn gelijk genoeg dat het werken over deze grenzen redelijk goed gaat. Als je het wijzigen van historie die achterblijft op je machine vermijdt (zoals over het algemeen aangeraden wordt), zou je niet eens kunnen zeggen dat Mercurial aan de andere kant staat.

==== Git and Bazaar

Onder de DVCSsen, is een andere beroemde: Bazaar. Bazaar is gratis en open source, en het is onderdeel van het GNU Project. Het gedraagt zich heel anders dan Git. Soms, om hetzelfde te bereiken als met Git, gebruik je een ander keyword, en sommige keywords die overeenkomen hebben niet dezelfde betekenis. In het bijzonder, is het beheren van branches erg anders en kan verwarring scheppen, in het bijzonder wanneer iemand uit het Git universum komt. Niettemin is het mogelijk om op een Bazaar repository te werken vanuit een Git omgeving.

Er zijn veel projecten die toestaan om een Git te gebruiken als een Bazaar client. Hier zullen we het project van Felipe Contreras gebruiken die je kunt vinden op https://github.com/felipec/git-remote-bzr. Om het te installeren, hoef je alleen het bestand git-remote-bzr in een folder te downloaden die in je $PATH staat:

$ wget https://raw.github.com/felipec/git-remote-bzr/master/git-remote-bzr -O ~/bin/git-remote-bzr
$ chmod +x ~/bin/git-remote-bzr

Je zult ooko Bazaar geinstalleerd moeten hebben. Dat is alles!

===== Maak een Git repository van een Bazaar repository

Het is eenvoudig in gebruik. Het is voldoende om een Bazaar repository te klonen door het vooraf te laten gaan bzr::. Omdat Git en Bazaar beide een volledige kloon doen naar je machine, is het mogelijk om een Git kloon aan je lokale Bazaar kloon te knopen, maar dat wordt niet aangeraden. Het is veel eenvoudiger om je Git kloon direct aan dezelfde plaats te knopen als waar je Bazaar kloon aan verbonden is - de centrale repository.

Laten we er vanuit gaan dat je gewerkt heb met een remote repository die staat op het adres bzr+ssh://developer@mybazaarserver:myproject. Dan moet je het op de volgende manier klonen:

$ git clone bzr::bzr+ssh://developer@mybazaarserver:myproject myProject-Git
$ cd myProject-Git

Nu is je Git repository gemaakt, maar het is nog niet geoptimaliseerd voor schijf gebruik. Dat is waarom je ook je Git repository moet schoonmaken en optimaliseren, zeker als het een grote is:

$ git gc --aggressive

===== Bazaar branches

Bazaar staat alleen toe om branches te klonen, maar een repository kan verscheidene branches bevatten, en git-remote-bzr kan beide klonen. Bijvoorbeeld: om een branch te klonen:

$ git clone bzr::bzr://bzr.savannah.gnu.org/emacs/trunk emacs-trunk

En om de gehele repository te klonen:

$ git clone bzr::bzr://bzr.savannah.gnu.org/emacs emacs

The second command clones all the branches contained in the emacs repository; nevertheless, it is possible to point out some branches:

$ git config remote-bzr.branches 'trunk, xwindow'

Some remote repositories don’t allow you to list their branches, in which case you have to manually specify them, and even though you could specify the configuration in the cloning command, you may find this easier:

$ git init emacs
$ git remote add origin bzr::bzr://bzr.savannah.gnu.org/emacs
$ git config remote-bzr.branches 'trunk, xwindow'
$ git fetch

===== Ignore what is ignored with .bzrignore

Since you are working on a project managed with Bazaar, you shouldn’t create a .gitignore file because you may accidentally set it under version control and the other people working with Bazaar would be disturbed. The solution is to create the .git/info/exclude file either as a symbolic link or as a regular file. We’ll see later on how to solve this question.

Bazaar uses the same model as Git to ignore files, but also has two features which don’t have an equivalent into Git. The complete description may be found in the documentation. The two features are:

  1. "!!" allows you to ignore certain file patterns even if they’re specified using a "!" rule.

  2. "RE:" at the beginning of a line allows you to specify a Python regular expression (Git only allows shell globs).

As a consequence, there are two different situations to consider:

  1. If the .bzrignore file does not contain any of these two specific prefixes, then you can simply make a symbolic link to it in the repository: ln -s .bzrignore .git/info/exclude

  2. Otherwise, you must create the .git/info/exclude file and adapt it to ignore exactly the same files in .bzrignore.

Whatever the case is, you will have to remain vigilant against any change of .bzrignore to make sure that the .git/info/exclude file always reflects .bzrignore. Indeed, if the .bzrignore file were to change and contained one or more lines starting with "!!" or "RE:", Git not being able to interpret these lines, you’ll have to adapt your .git/info/exclude file to ignore the same files as the ones ignored with .bzrignore. Moreover, if the .git/info/exclude file was a symbolic link, you’ll have to first delete the symbolic link, copy .bzrignore to .git/info/exclude and then adapt the latter. However, be careful with its creation because with Git it is impossible to re-include a file if a parent directory of that file is excluded.

===== Fetch the changes of the remote repository

To fetch the changes of the remote, you pull changes as usually, using Git commands. Supposing that your changes are on the master-branch, you merge or rebase your work on the origin/master-branch:

$ git pull --rebase origin

===== Push your work on the remote repository

Because Bazaar also has the concept of merge commits, there will be no problem if you push a merge commit. So you can work on a branch, merge the changes into master and push your work. Then, you create your branches, you test and commit your work as usual. You finally push your work to the Bazaar repository:

$ git push origin master

===== Caveats

Git’s remote-helpers framework has some limitations that apply. In particular, these commands don’t work:

  • git push origin :branch-to-delete (Bazaar can’t accept ref deletions in this way.)

  • git push origin old:new (it will push old)

  • git push --dry-run origin branch (it will push)

===== Summary

Since Git’s and Bazaar’s models are similar, there isn’t a lot of resistance when working across the boundary. As long as you watch out for the limitations, and are always aware that the remote repository isn’t natively Git, you’ll be fine.

==== Git en Perforce

Perforce is een erg populaire versie-beheer systeem in bedrijfsomgevingen. Het bestaal al sinds 1995, wat het het oudste systeem maakt dat we in dit hoofdstuk behandelen. Zoals het is, is het ontworpen met de beperkingen van die tijd; het gaat er vanuit dat je altijd verbonden bent met een enkele centrale server, en er wordt maar één versie bewaard op je lokale schijf. Het valt niet te ontkennen dat de mogelijkheden en beperkingen goed afgestemd zijn op een aantal specifieke werksituaties, maar er zijn veel projecten die Perforce gebruiken waar Git eigenlijk veel beter zou werken.

Er zijn twee opties als je Perforce en Git samen wilt gebruiken. De eerste die we gaan behandelen is de “Git Fusion” bridge van de makers van Perforce, die je de subtrees van je Perforce depot ter beschikking stelt als lees-schrijf Git repositories. De tweede is git-p4, een bridge op het werkstation die je Git als een Perforce client laat werken, zonder een herconfiguratie van de Perforce server af te dwingen.

===== Git Fusion

Perforce stelt een product ter beschikking met de naam Git Fusion (beschikbaar op http://www.perforce.com/git-fusion), welke een Perforce server synchroniseert met Git repositories aan de kant van de server.

====== Inrichten

Voor onze voorbeelden, zullen we de eenvoudigste installatie methode voor Git Fusion gebruiken, en dat is het downloaden van een virtual machine die de Perforce daemon en Git Fusion draait. Je kunt deze virtual machine image krijgen op http://www.perforce.com/downloads/Perforce/20-User, en als het eenmaal gedownload is, importeer je het in je favoriete virturalisatie software (wij zullen VirtualBox gebruiken).

Als de machine voor het eerst opstart, vraag het je om het wachtwoord van de drie Linux gebuikers (root, perforce en git) te wijzigen, en een instantie naam op te geven die kan worden gebruikt om deze installatie van andere te onderscheiden op hetzelfde netwerk. Als dat alles gereed is, zal je het volgende zien:

Het Git Fusion virtual machine opstart scherm.
Figure 143. Het Git Fusion virtual machine opstart scherm.

Let even specifiek op het IP adres dat hier wordt getoond, we zullen deze later gaan gebruiken. Vervolgens maken we een Perforce gebruiker aan. Kies de “Login” optie onder aan het scherm en druk op enter (of SSH naar de machine), en log in als root. Gebruik deze commando’s om een gebruiker aan te maken:

$ p4 -p localhost:1666 -u super user -f john
$ p4 -p localhost:1666 -u john passwd
$ exit

Het eerste opent een VI editor om de gebruiker aan te passen, maar je kunt de standaard-instellingen accepteren door :wq te typen en enter te drukken. Het tweede zal je twee keer vragen om een wachtwoord in te typen. Dat is alles wat we hebben te doen met een shell prompt, dus beëindigen we deze sessie.

Wat je daarna moet doen om ons te volgen is Git te vertellen om geen SSL certificaten te verifiëren. Het Git Fusion image wordt met een certificaat geleverd, maar dat is voor een domain die niet zal overeenkomen met het IP adres van je virtuele machine, dus Git zal de HTTPS connectie weigeren. Als het de bedoeling is dat dit een permanente installatie gaat worden, raadpleeg dan het handboek van Perforce Git Fusion om een ander certificaat te installeren; voor het doel van ons voorbeeld zal dit voldoende zijn:

$ export GIT_SSL_NO_VERIFY=true

Nu kunnen we gaan testen of alles werkt.

$ git clone https://10.0.1.254/Talkhouse
Cloning into 'Talkhouse'...
Username for 'https://10.0.1.254': john
Password for 'https://john@10.0.1.254':
remote: Counting objects: 630, done.
remote: Compressing objects: 100% (581/581), done.
remote: Total 630 (delta 172), reused 0 (delta 0)
Receiving objects: 100% (630/630), 1.22 MiB | 0 bytes/s, done.
Resolving deltas: 100% (172/172), done.
Checking connectivity... done.

Het virtuele machine image komt met een voorinstalleerd voorbeeld project dat je kunt klonen. Hier klonen we over HTTPS, met de john gebruiker die we hierboven aangemaakt hebben; Git vraagt om de inloggegevens voor deze connectie, maar de credential cache staat ons toe om deze stap voor de hierop volgende aanvragen over te slaan.

====== Fusion Configuratie

Als je eenmaal Git Fusion geïnstalleerd hebt, zal je de configuratie hier en daar willen aanpassen. Dit is eigenlijk behoorlijk eenvoudig te doen met gebruik van je favoriete Perforce client; map eenvoudigweg de //.git-fusion directory op de Perforce server naar je werkruimte. De bestandsstructuur zie er als volgt uit:

$ tree
.
├── objects
│   ├── repos
│   │   └── [...]
│   └── trees
│       └── [...]
│
├── p4gf_config
├── repos
│   └── Talkhouse
│       └── p4gf_config
└── users
    └── p4gf_usermap

498 directories, 287 files

De objects directory wordt door Git Fusion intern gebruikt om Perforce objecten op Git te mappen en andersom, je zou niet hoeven te rommelen met de inhoud daarvan. Er is een globaal p4gf_config bestand in deze directory, zowel als een voor elke repository – dit zijn de configuratie bestanden die bepalen hoe Git Fusion zich gedraagt. Laten we het bestand in de root eens bekijken:

[repo-creation]
charset = utf8

[git-to-perforce]
change-owner = author
enable-git-branch-creation = yes
enable-swarm-reviews = yes
enable-git-merge-commits = yes
enable-git-submodules = yes
preflight-commit = none
ignore-author-permissions = no
read-permission-check = none
git-merge-avoidance-after-change-num = 12107

[perforce-to-git]
http-url = none
ssh-url = none

[@features]
imports = False
chunked-push = False
matrix2 = False
parallel-push = False

[authentication]
email-case-sensitivity = no

We zullen niet ingaan op de betekenissen van al deze vlaggen, maar merk op dat dit niet meer is dan een INI-geformatteerd tekstbestand, vergelijkbaar met wat Git gebruikt voor configuratie. Dit bestand bepaalt de globale opties, die kunnen worden overschreven door repository-specifieke configuratie bestanden, zoals repos/Talkhouse/p4gf_config. Als je dat bestand opent, zal je een [@repo] sectie zien met wat instellingen die anders zijn dan de globale standaard instellingen. Je zult ook secties zien die er zo uit zien:

[Talkhouse-master]
git-branch-name = master
view = //depot/Talkhouse/main-dev/... ...

Dit is een mapping tussen een Perforce branch en een Git branch. De sectie kan elke naam zijn die je maar kunt verzinnen, zo lang als het maar uniek is. git-branch-name stelt je in staat een depot pad die maar moeizaam zou zijn in Git te converteren naar een handigere naam. De view instelling bepaalt hoe Perforce bestanden zijn gemapt op de Git repository, waarbij de standaard view mapping syntax wordt gebruikt. Er kunnen meer dan één mapping worden opgegeven, zoals in dit voorbeeld:

[multi-project-mapping]
git-branch-name = master
view = //depot/project1/main/... project1/...
       //depot/project2/mainline/... project2/...

Op deze manier kan je, als je reguliere werkruimte mapping wijzigingen in de struktuur van de directories in zich heeft, dat met een Git repository repliceren.

Het laatste bestand dat we zullen behandelen is users/p4gv_usermap, wat Perforce gebruikers op Git gebruikers mapt, en je zult deze waarschijnlijk niet eens nodig hebben. Bij het converteren van een Perforce changeset naar een Git commit, is het standaard gedrag van Git Fusion om de Perforce gebruiker op te zoeken, en het email adres en volledige naam die daar is opgeslagen voor het auteur/committer veld van Git te gebruiken. Bij het converteren de andere kant op, is de standaard om de Perforce gebruiker op te zoeken met het email adres dat is opgeslagen in het auteur veld in de Git commit, en om de changeset op te sturen als die gebruiker (waarbij de geldende permissies worden gerespecteerd). In de meeste gevallen, zal dit gedrag prima werken, maar bekijk nu eens het volgende mapping bestand:

john john@example.com "John Doe"
john johnny@appleseed.net "John Doe"
bob employeeX@example.com "Anon X. Mouse"
joe employeeY@example.com "Anon Y. Mouse"

Elke regel is van het formaat <user> <email> "<volledige naam>", en vormt de mapping van een enkele gebruiker. De eerste twee regels mappen twee verschillende email adressen naar de naam van dezelfde Perforce gebruiker. Dit is handig als je onder verschillende email adressen Git commits hebt gemaakt (of van email adres bent veranderd), en je deze naar dezelfde Perforce gebruiker wilt mappen. Bij het maken van een Git commit van een Perforce changeset, wordt de eerste regel die met de Perforce gebruiker overeenkomt in Git gebruikt voor informatie over het auteurschap.

De laatste twee regels voorkomen dat de echte namen en email adressen van Bob en Joe in de commits terechtkomen die voor Git worden gemaakt. Dit is handig als je een intern project openbaar wilt maken, maar je niet de hele personeelsbestand aan de wereld wilt blootstellen. Merk op dat de email adressen en volledige namen uniek moeten zijn, tenzij je wilt dat alle Git commits worden toegeschreven aan een enkele virtuele auteur.

====== Workflow

Perforce Git Fusion is een tweewegs bridge tussen Perforce en Git versie beheer. Laten we een kijken hoe het voelt om er vanaf de Git kant mee te werken. We zullen aannemen dat de het “Jam” project gemapt hebben met een configuratie bestand zoals hierboven, en die we kunnen klonen als volgt:

$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://ben@10.0.1.254':
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (1704/1704), done.
Receiving objects: 100% (2070/2070), 1.21 MiB | 0 bytes/s, done.
remote: Total 2070 (delta 1242), reused 0 (delta 0)
Resolving deltas: 100% (1242/1242), done.
Checking connectivity... done.
$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/rel2.1
$ git log --oneline --decorate --graph --all
* 0a38c33 (origin/rel2.1) Create Jam 2.1 release branch.
| * d254865 (HEAD, origin/master, origin/HEAD, master) Upgrade to latest metrowerks on Beos -- the Intel one.
| * bd2f54a Put in fix for jam's NT handle leak.
| * c0f29e7 Fix URL in a jam doc
| * cc644ac Radstone's lynx port.
[...]

De eerste keer dat je dit doet kan het eventjes duren. Wat er gebeurt is dat Git Fusion alle van toepassing zijnde changesets in de Perforce historie naar Git commits converteert. Dit gebeurt lokaal op de server, dus het is relatief snel, maar als je veel historie hebt, kan het nog steeds lang duren. Toekomstige fetches voeren incrementele conversies uit, dus zal het meer als de normale snelheid van Git aanvoelen.

Zoals je kunt zien, lijkt onze repository precies op elke andere Git repository waar je mee zou kunnen werken. Er zijn drie branches, en Git heeft heel behulpzaam een lokale master-branch gemaakt die origin/master trackt. Laten we eens wat werk doen, en een paar nieuwe commits maken:

# ...
$ git log --oneline --decorate --graph --all
* cfd46ab (HEAD, master) Add documentation for new feature
* a730d77 Whitespace
* d254865 (origin/master, origin/HEAD) Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

We hebben twee nieuwe commits. Laten we nu eens controleren of iemand anders aan het werk is geweest:

$ git fetch
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://10.0.1.254/Jam
   d254865..6afeb15  master     -> origin/master
$ git log --oneline --decorate --graph --all
* 6afeb15 (origin/master, origin/HEAD) Update copyright
| * cfd46ab (HEAD, master) Add documentation for new feature
| * a730d77 Whitespace
|/
* d254865 Upgrade to latest metrowerks on Beos -- the Intel one.
* bd2f54a Put in fix for jam's NT handle leak.
[...]

Het ziet er naar uit dat dat het geval was! Je zou het met deze uitvoer niet zeggen, maar de 6afeb15 commit was in gewoon gemaakt met behulp van een Perforce client. Het ziet er net zo uit als elke andere commit wat Git betreft, en dat is nu net de bedoeling. Laten we kijken hoe de Perforce gebruiker met een merge commit omgaat:

$ git merge origin/master
Auto-merging README
Merge made by the 'recursive' strategy.
 README | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (9/9), 917 bytes | 0 bytes/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Perforce: 100% (3/3) Loading commit tree into memory...
remote: Perforce: 100% (5/5) Finding child commits...
remote: Perforce: Running git fast-export...
remote: Perforce: 100% (3/3) Checking commits...
remote: Processing will continue even if connection is closed.
remote: Perforce: 100% (3/3) Copying changelists...
remote: Perforce: Submitting new Git commit objects to Perforce: 4
To https://10.0.1.254/Jam
   6afeb15..89cba2b  master -> master

Git denkt dat het gelukt is. Laten we eens kijken naar de historie van het README bestand vanuit het oogpunt van Perforce, met het revisiegraaf gereedschap p4v.

Perforce revisie graaf als resultaat van Git push.
Figure 144. Perforce revisie graaf als resultaat van Git push.

Als je dit scherm nog nooit gezien hebt, kan het er nogal verwarrend uitzien, maar het laat dezelfde concepten zien als een grafisch programma voor Git historie. We kijken naar de geschiedenis van het README bestand, dus de directory tree links boven laat alleen dat bestand zien zoals deze voorkomt in de diverse branches. Rechtsboven hebben we een grafische kijk op hoe verschillende revisies van het bestand aan elkaar zijn gerelateerd, en het hoog-overzicht van dit plaatje is rechts onder. De rest van dit scherm wordt gegeven aan de details-scherm voor de geselecteerde revisie (2 in dit geval).

Een ding om op te merken is dat de graaf er precies hetzelfde uitziet als die in de historie van Git. Perforce had geen benoemde branch om de 1 en 2 commits in op te slaan, dus heeft het een “anonymous” branch aangemaakt in de .git-fusion directory om deze in op te slaan. Dit zal ook gebeuren voor Git branches met een naam die niet overeenkomen met een branchnaam in Perforce (en je kunt deze later op een Perforce branch mappen met gebruik van het configuratie bestand).

Het leeuwendeel van dit alles gebeurt achter de schermen, maar het eindresultaat is dat de ene persoon in een team Git kan gebruiken, een ander kan Perforce gebruiken, en geen van beiden heeft weet van de keuze van de ander.

====== Git-Fusion Samenvatting

Als je toegang hebt (of kan krijgen) naar je Perforce server, is Git Fusion een hele goede manier om Git en Perforce met elkaar te laten samenwerken. Het vergt een beetje configuratie, maar de leercurve is niet heel erg steil. Dit is een van de weinige paragrafen in dit hoofdstuk waar waarschuwingen over de volledige kracht van Git niet zullen voorkomen. Dat wil niet zeggen dat Perforce erg blij gaat zijn met alles wat je er naartoe stuurt – als je probeert de geschiedenis herschrijven die al gepusht is, zal Git Fusion dit weigeren – maar Git Fusion doet erg z’n best om natuurlijk aan te voelen. Je kunt zelfs Git submodulen gebruiken (al zullen ze er voor Perforce gebruikers vreemd uitzien), en branches mergen (dit wordt aan de Perforce kant als een integratie opgeslagen).

Als je de beheerder niet kunt overtuigen om Git Fusion op te zetten, is er nog steeds een manier om deze instrumenten samen te laten werken.

===== Git-p4

Git-p4 is een tweewegs bridge tussen Git en Perforce. Het draait volledig binnen je Git repository, dus je hebt geen enkele vorm van toegang tot de Perforce server nodig (buiten de login gegevens natuurlijk). Git-p4 is niet zo flexibel of compleet als oplossing als Git Fusion, maar het stelt je wel in staat om het meeste wat je zou willen doen uit te voeren zonder afbreuk te doen aan de omgeving van de server.

Note

Je zult de p4 tool ergens in je PATH moeten zetten om te kunnen werken met git-p4. Op het moment van schrijven is het voor iedereen te verkrijgen op http://www.perforce.com/downloads/Perforce/20-User.

====== Inrichting

Voor het voorbeeld, zullen we de Perforce server van het Git Fusion OVA als boven gebruiken, maar we slaan de Git Fusion server over en gaan direct naar het versie beheer van Perforce.

Om de p4 commando-regel client te gebruiken (waar git-p4 van afhankelijk is), zal je een aantal omgevingsvariabelen moeten inrichten:

$ export P4PORT=10.0.1.254:1666
$ export P4USER=john

====== Op gang komen

Zoals altijd in Git, is het eerste commando het klonen:

$ git p4 clone //depot/www/live www-shallow
Importing from //depot/www/live into www-shallow
Initialized empty Git repository in /private/tmp/www-shallow/.git/
Doing initial import of //depot/www/live/ from revision #head into refs/remotes/p4/master

Dit maakt wat in Git terminologie een “shallow” is; alleen de allerlaatste Perforce revisie wordt in Git geïmporteerd; onthoud dat Perforce niet ontworpen is om elke revisie aan elke gebruiker te geven. Dit is genoeg om Git te laten werken als een Perforce client, maar voor andere toepassingen is dit niet genoeg.

Als dit eenmaal klaar is, hebben we een volledig werkende Git repository:

$ cd myproject
$ git log --oneline --all --graph --decorate
* 70eaf78 (HEAD, p4/master, p4/HEAD, master) Initial import of //depot/www/live/ from the state at revision #head

Merk op dat er een “p4” remote is voor de Perforce server, maar al het overige ziet eruit als een standaard clone. Dit is echter een beetje misleidend; er is in het echt geen remote aanwezig.

$ git remote -v

Er bestaan in deze repository helemaal geen remotes. Git-p4 heeft een aantal refs gemaakt om de staat van de server te vertegenwoordigen, en ze zien er uit als remote refs voor git log, maar ze worden niet door Git onderhouden, en je kunt er niet naar pushen.

====== Workflow

Okay, laten we wat werk doen. Laten we aannemen dat je wat vorderingen gemaakt hebt op een zeer belangrijke feature, en je bent klaar om het te laten zien aan de rest van je team.

$ git log --oneline --all --graph --decorate
* 018467c (HEAD, master) Change page title
* c0fb617 Update link
* 70eaf78 (p4/master, p4/HEAD) Initial import of //depot/www/live/ from the state at revision #head

We hebben twee nieuwe commits gemaakt die klaar zijn om te worden gestuurd naar de Perforce server. Laten we kijken of er iemand anders aan het werk is geweest vandaag:

$ git p4 sync
git p4 sync
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12142 (100%)
$ git log --oneline --all --graph --decorate
* 75cd059 (p4/master, p4/HEAD) Update copyright
| * 018467c (HEAD, master) Change page title
| * c0fb617 Update link
|/
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Het ziet er naar uit van wel, en master en p4/master zijn uiteen gelopen. Het branching systeem van Perforce lijkt in niets op die van Git, dus het aanleveren van merge commits zal nergens op slaan. Git-p4 raadt aan dat je je commits rebaset, en levert zelfs een manier om dit snel te doen:

$ git p4 rebase
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
No changes to import!
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
Applying: Update link
Applying: Change page title
 index.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Je kunt het uit de uitvoer waarschijnlijk wel afleiden, maar git p4 rebase is kort voor git p4 sync gevolgd door een git rebase p4/master. Het is iets slimmer dan dat, vooral in het geval dat je met meerdere branches werkt, maar dit is een goede benadering.

Nu is onze historie weer lineair, en we zijn klaar om onze wijzigingen te delen met Perforce. Het git p4 submit commando zal proberen om een nieuwe Perforce revisie te maken voor elke Git commit tussen p4/master en master. Als we dit aanroepen komen we in onze favoriete editor, en de inhoud van het bestand ziet er ongeveer zo uit:

# A Perforce Change Specification.
#
#  Change:      The change number. 'new' on a new changelist.
#  Date:        The date this specification was last modified.
#  Client:      The client on which the changelist was created.  Read-only.
#  User:        The user who created the changelist.
#  Status:      Either 'pending' or 'submitted'. Read-only.
#  Type:        Either 'public' or 'restricted'. Default is 'public'.
#  Description: Comments about the changelist.  Required.
#  Jobs:        What opened jobs are to be closed by this changelist.
#               You may delete jobs from this list.  (New changelists only.)
#  Files:       What opened files from the default changelist are to be added
#               to this changelist.  You may delete files from this list.
#               (New changelists only.)

Change:  new

Client:  john_bens-mbp_8487

User: john

Status:  new

Description:
   Update link

Files:
   //depot/www/live/index.html   # edit


######## git author ben@straub.cc does not match your p4 account.
######## Use option --preserve-user to modify authorship.
######## Variable git-p4.skipUserNameCheck hides this message.
######## everything below this line is just the diff #######
--- //depot/www/live/index.html  2014-08-31 18:26:05.000000000 0000
+++ /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/index.html   2014-08-31 18:26:05.000000000 0000
@@ -60,7 +60,7 @@
 </td>
 <td valign=top>
 Source and documentation for
-<a href="http://www.perforce.com/jam/jam.html">
+<a href="jam.html">
 Jam/MR</a>,
 a software build tool.
 </td>

Dit is voor een groot gedeelte dezelfde inhoud die je zou zien bij het aanroepen van p4 submit, behalve het spul aan het eind dat git-p4 behulpzaam heeft toegevoegd. Git-p4 probeert jouw Git instellingen en die van Perforce elk te volgen als het een naam moet geven voor een commit of een changeset, maar in sommige gevallen zal je het willen overschrijven. Bijvoorbeeld, als de Git commit die je aan het importeren was geschreven is door iemand die geen Perforce gebruiker account heeft, zal je nog steeds de resulterende changeset eruit willen laten zien alsof zij het geschreven hebben (en niet jij).

Git-p4 heeft het bericht van de Git commit heel behulpzaam geïmporteerd als de inhoud voor deze Perforce changeset, dus alles wat we hoeven te doen is bewaren en stoppen, twee keer (een keer voor elke commit). De uiteindelijke shell uitvoer zal er ongeveer zo uit zien:

$ git p4 submit
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Synchronizing p4 checkout...
... - file(s) up-to-date.
Applying dbac45b Update link
//depot/www/live/index.html#4 - opened for edit
Change 12143 created with 1 open file(s).
Submitting change 12143.
Locking 1 files ...
edit //depot/www/live/index.html#5
Change 12143 submitted.
Applying 905ec6a Change page title
//depot/www/live/index.html#5 - opened for edit
Change 12144 created with 1 open file(s).
Submitting change 12144.
Locking 1 files ...
edit //depot/www/live/index.html#6
Change 12144 submitted.
All commits applied!
Performing incremental import into refs/remotes/p4/master git branch
Depot paths: //depot/www/live/
Import destination: refs/remotes/p4/master
Importing revision 12144 (100%)
Rebasing the current branch onto remotes/p4/master
First, rewinding head to replay your work on top of it...
$ git log --oneline --all --graph --decorate
* 775a46f (HEAD, p4/master, p4/HEAD, master) Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Het resultaat is alsof we zojuist een git push gedaan hebben, wat beste analogie is van hetgeen er in werkelijkheid is gebeurd.

Merk op dat bij dit proces elke Git commit wordt omgezet in een Perforce changeset; als je deze naar een enkele changeset wilt terugbrengen, kan je dat doen met een intereactieve rebase voordat je git p4 submit aanroept. Merk ook op dat de SHA-1 hashes van alle commits die als changesets zijn opgestuurd gewijzigd zijn; dit is omdat git-p4 een regel toevoegt aan het eind van elke commit die het converteert:

$ git log -1
commit 775a46f630d8b46535fc9983cf3ebe6b9aa53145
Author: John Doe <john@example.com>
Date:   Sun Aug 31 10:31:44 2014 -0800

    Change page title

    [git-p4: depot-paths = "//depot/www/live/": change = 12144]

Wat gebeurt er als je probeert een merge commit op te sturen? Laten we het eens proberen. Hier is de situatie waar we ons in gewerkt hebben:

$ git log --oneline --all --graph --decorate
* 3be6fd8 (HEAD, master) Correct email address
*   1dcbf21 Merge remote-tracking branch 'p4/master'
|\
| * c4689fc (p4/master, p4/HEAD) Grammar fix
* | cbacd0a Table borders: yes please
* | b4959b6 Trademark
|/
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

De Git en Perforce historie lopen uiteen na 775a46f. Aan de Git kant zijn er twee commits, daarna een merge commit met de Perforce head, en daarna een andere commit. We zullen proberen deze in te sturen bovenop een enkele changeset aan de kant van Perforce. Laten we eens kijken wat er gebeurt als we proberen deze nu in te sturen:

$ git p4 submit -n
Perforce checkout for depot path //depot/www/live/ located at /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would synchronize p4 checkout in /Users/ben/john_bens-mbp_8487/john_bens-mbp_8487/depot/www/live/
Would apply
  b4959b6 Trademark
  cbacd0a Table borders: yes please
  3be6fd8 Correct email address

De -n vlag staat voor --dry-run, wat probeert te rapporteren wat er zou gaan gebeuren als het submit commando in het echt zou worden aangeroepen. In dit geval ziet het er naar uit dat we drie Perforce changesets zouden gaan aanmaken, wat overeenkomt met de drie non-merge commits die nog niet bestaan op de Perforce server. Dat klinkt precies als wat we willen, laten we kijken hoe het uitpakt:

$ git p4 submit
[…]
$ git log --oneline --all --graph --decorate
* dadbd89 (HEAD, p4/master, p4/HEAD, master) Correct email address
* 1b79a80 Table borders: yes please
* 0097235 Trademark
* c4689fc Grammar fix
* 775a46f Change page title
* 05f1ade Update link
* 75cd059 Update copyright
* 70eaf78 Initial import of //depot/www/live/ from the state at revision #head

Onze historie werd lineair, precies alsof we hadden gerebased voor het insturen (en dat is precies wat er is gebeurd). Dit houdt in dat je zonder problemen branches kunt maken, erop werken, weggooien en mergen aan de kant van Git zonder bang te zijn dat je historie op de een of andere manier niet meer compatible is met Perforce. Als je het kunt rebasen, kan je het insturen naar een Perforce server.

====== Branchen

Als je Perforce project meerdere branches heeft, is niet alles voor je verloren; git-p4 kan het op een manier behandelen die je doet denken aan Git. Laten we zeggen dat je Perforce depot zo is ingericht:

//depot
  |__ project
      |__ main
      |__ dev

En stel nu dat je een dev-branch hebt, die een view spec heeft die er zo uit ziet:

//depot/project/main/... //depot/project/dev/...

Git-p4 kan deze situatie automatisch herkennen en de juiste handeling uitvoeren:

$ git p4 clone --detect-branches //depot/project@all
Importing from //depot/project@all into project
Initialized empty Git repository in /private/tmp/project/.git/
Importing revision 20 (50%)
    Importing new branch project/dev

    Resuming with change 20
Importing revision 22 (100%)
Updated branches: main dev
$ cd project; git log --oneline --all --graph --decorate
* eae77ae (HEAD, p4/master, p4/HEAD, master) main
| * 10d55fb (p4/project/dev) dev
| * a43cfae Populate //depot/project/main/... //depot/project/dev/....
|/
* 2b83451 Project init

Merk de “@all” specificatie in het depot pad op; dat vertelt git-p4 om niet alleen de laatste changeset voor die subtree te klonen, maar alle changesets die ooit in aanraking zijn geweest met deze paden. Dit zit dichter bij het Git-concept van een klone, maar als je aan een project werkt met een lange historie, kan dit wel even duren.

De --detect-branches vlag instrueert git-p4 om de Perforce branch specificaties te gebruiken om de branches op Git refs te mappen. Als deze mappings niet aanwezig zijn op de Perforce server (wat een heel valide manier is om Perforce te gebruiken), kan je git-p4 aangeven wat de branch mappings zijn, en je krijgt hetzelfde resultaat:

$ git init project
Initialized empty Git repository in /tmp/project/.git/
$ cd project
$ git config git-p4.branchList main:dev
$ git clone --detect-branches //depot/project@all .

Door de git-p4.branchList configuratie variabele op main:dev te zetten wordt git-p4 geïnstrueerd dat “main” en “dev” beide branches zijn, en dat de tweede een kind is van de eerste.

Als we nu git checkout -b dev p4/project/dev doen en een aantal commits maken, is git-p4 slim genoeg om de juiste branch aan te spreken als we git p4 submit uitvoeren. Jammergenoeg kan git-p4 geen shallow clones en meerdere branches tegelijk aan; als je een enorm groot project hebt en je wilt aan meer dan één branch werken, zal je git p4 clone voor elke branch waarnaar je wilt submitten moeten uitvoeren.

Voor het maken of integreren van branches, zal je een Perforce client moeten gebruiken. Git-p4 kan alleen met bestaande branches synchroniseren of daarnaar submitten, en het kan dit alleen doen voor één lineaire changeset per keer. Als je twee branches in Git merget en probeert de nieuwe changeset in te sturen, is alles wat er wordt opgeslagen een stel bestandswijzigingen; de metadata over welke branches er zijn betrokken bij de integratie gaat verloren.

===== Git en Perforce samenvatting

Git-p4 maakt het mogelijk om een Git workflow te gebruiken met een Perforce server, en het is er best wel goed in. Echter, het is belangrijk om te onthouden dat Perforce de baas is over de broncode, en dat je Git alleen maar gebruikt om er lokaal mee te werken. Wees vooral erg voorzichtig om Git commits te delen; als je een remote hebt die andere mensen ook gebruiken, push dan geen enkele commit die niet al eerder naar die Perforce server zijn gestuurd.

Als je vrijelijk zowel de Perforce en Git clients tegelijk wilt gebruiken voor broncode beheer, en je kunt de beheerder van de server ervan overtuigen om het te installeren, zal Git Fusion Git een eersterangs versiebeheer client maken voor een Perforce server.

==== Git en TFS

Git wordt steeds populairder onder Windows ontwikkelaars, en als je code schrijft op Windows, is er een grote kans dat je de Team Foundation Server (TFS) van Microsoft gebruikt. TFS is een samenwerkings pakket die een defect- en werkbonvolgsysteem bevat, procesondersteuning voor Scrum en dergelijke, code review en versiebeheer. Er gaat wat verwarring aankomen: TFS is de server, die broncode beheer ondersteunt met behulp van zowel Git als hun eigen VCS, die ze TFVC (Team Foundation Version Control) hebben gedoopt. Git ondersteuning is een nogal nieuwe mogelijkheid voor TFS (geleverd met de 2013 versie), dus alle instrumenten die van voor die datum stammen verwijzen naar het versie-beheer gedeelte als “TFS”, zelfs als ze voor het grootste gedeelte werken met TFVC.

Als je jezelf in een team zit dat TFVC gebruikt, en je zou liever Git gebruiken als je versie-beheer client, is dit een mooi project voor jou.

===== Welk instrument

Er zijn er in feite twee: git-tf en git-tfs.

Git-tfs (te vinden op https://github.com/git-tfs/git-tfs) is een .NET project, en (op het tijdstip van schrijven) kan alleen onder Windows draaien. Om met Git repositories te werken, gebruikt het de .NET bindings voor libgit2, een library-oriented implementatie van Git die zeer hoog performant is en die een hoge mate van flexibiliteit biedt met de pretentie van een Git repository. Libgit2 is geen volledige implementatie van Git, dus om dit verschil te compenseren zal git-tfs gewoon de commando-regel versie van de Git client aanroepen om bepaalde operaties uit te voeren, dus er zijn geen kunstmatige beperkingen in wat het met Git repositories kan doen. De ondersteuning van de TFVC functies is zeer volwassen, omdat het de mogelijkheden van Visual Studio gebruikt voor operaties met servers. Dit houdt in dat je toegang nodig hebt tot deze mogelijkheden, wat betekent dat je een recente versie van Visual Studio moet installeren (elke editie sinds versie 2010, inclusief Express sinds versie 2012), of de Visual Studio SDK.

Git-tf (welke huist op https://gittf.codeplex.com) is een Java project, en als zodanig loopt het op elke computer met een Java runtime omgeving. Het interacteert met Git repositories middels JGit (een JVM implementatie van Git), wat inhoudt dat het vrijwel geen beperkingen kent in termen van Git functies. Echter, de ondersteuning voor TFVC is beperkt ten opzichte van git-tfs - het ondersteunt bijvoorbeeld geen branches.

Dus elke tool heeft z’n voors en tegens, en er zijn genoeg situaties waarbij de een te prefereren is boven de ander. We zullen het normale gebruik van beide gaan behandelen in dit boek.

Note

Je zult toegang nodig hebben tot een TFVC-gebaseerde repository om deze instructies mee te doen. Ze zijn niet zo ruim voorhanden als Git of Subversion repositories, dus je zult misschien je eigen moeten gaan maken. Codeplex (https://www.codeplex.com) of Visual Studio Online (http://www.visualstudio.com) zijn goede opties hiervoor.

===== Beginnen met: git-tf

Het eerste wat je doet, net als met elk andere Git project is klonen. Dit is hoe het er uit ziet met git-tf:

$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main project_git

Het eerste argument is de URL van een TFVC collectie, het tweede is er een in de vorm $/project/branch, en het derde is het pad naar de lokale Git repository die gemaakt moet worden (deze laatste is optioneel). Git-tf kan maar met een branch tegelijk werken; als je checkins wilt maken op een andere TFVC branch, zul je een nieuwe kloon moeten maken van die branch.

Dit maakt een volledig functionele Git repository:

$ cd project_git
$ git log --all --oneline --decorate
512e75a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Checkin message

Dit wordt een shallow clone (oppervlakkige kloon) genoemd, wat inhoudt dat alleen de laatste changeset is gedownload. TFVC is niet ontworpen met het het idee dat elk werkstation een volledige kopie van de historie heeft, dus git-tf valt terug op het ophalen van de laatste versie, wat veel sneller is.

Als je de tijd hebt, is het waarschijnlijk de moeite om de gehele project historie te klonen, met de --deep optie:

$ git tf clone https://tfs.codeplex.com:443/tfs/TFS13 $/myproject/Main \
  project_git --deep
Username: domain\user
Password:
Connecting to TFS...
Cloning $/myproject into /tmp/project_git: 100%, done.
Cloned 4 changesets. Cloned last changeset 35190 as d44b17a
$ cd project_git
$ git log --all --oneline --decorate
d44b17a (HEAD, tag: TFS_C35190, origin_tfs/tfs, master) Goodbye
126aa7b (tag: TFS_C35189)
8f77431 (tag: TFS_C35178) FIRST
0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
        Team Project Creation Wizard

Merk de tags op met namen als TFS_C35189; dit is een functionaliteit die je helpt met het relateren van Git commits aan TFVC changesets. Dit is een aardige manier om het weer te geven, omdat je met een simpele log commando kunt zien welke van jouw commits zijn gerelateerd aan een snapshot die leeft in TFVC. Ze zijn niet noodzakelijk (en je kunt ze gewoon uitschakelen met git config git-tf.tag false) - git-tf houdt de echte commit-changeset relaties bij in het .git/git-tf bestand.

===== Beginnen met: git-tfs

Het klonen met git-tfs gedraagt zich iets anders. Neem waar:

PS> git tfs clone --with-branches \
    https://username.visualstudio.com/DefaultCollection \
    $/project/Trunk project_git
Initialized empty Git repository in C:/Users/ben/project_git/.git/
C15 = b75da1aba1ffb359d00e85c52acb261e4586b0c9
C16 = c403405f4989d73a2c3c119e79021cb2104ce44a
Tfs branches found:
- $/tfvc-test/featureA
The name of the local branch will be : featureA
C17 = d202b53f67bde32171d5078968c644e562f1c439
C18 = 44cd729d8df868a8be20438fdeeefb961958b674

Merk de --with-branches vlag op. Git-tfs is in staat de TFVC branches aan Git branches te relateren, en deze vlag geeft aan dat er een lokale Git branch moet worden aangemaakt voor elke TFVC branch. Dit wordt sterk aangeraden als je ooit gebrancht of gemerged hebt in TFS, maar het gaat niet werken met een server die ouder is dan TFS 2010 - voor die versie waren “branches” gewoon folders, dus git-tfs kan ze niet onderscheiden van reguliere folders.

Laten we een kijkje nemen naar de Git repository die het resultaat is:

PS> git log --oneline --graph --decorate --all
* 44cd729 (tfs/featureA, featureA) Goodbye
* d202b53 Branched from $/tfvc-test/Trunk
* c403405 (HEAD, tfs/default, master) Hello
* b75da1a New project
PS> git log -1
commit c403405f4989d73a2c3c119e79021cb2104ce44a
Author: Ben Straub <ben@straub.cc>
Date:   Fri Aug 1 03:41:59 2014 +0000

    Hello

    git-tfs-id: [https://username.visualstudio.com/DefaultCollection]$/myproject/Trunk;C16

Er zijn twee lokale branches, master en featureA, die staan voor het initiele uitgangspunt van de kloon (Trunk in TFVC) en een afgesplitste branch (featureA in TFVC). Je kunt ook zien dat de tfs “remote” een aantal refs heeft: default en featureA, die staan voor de TFVC branches. Git-tfs relateert de branch die je hebt gekloont aan tfs/default, en de andere krijgen hun eigen namen.

Iets anders om op te merken is de git-tfs-id: regels in de commit berichten. In plaats van tags, gebruikt git-tfs deze markeringen om een relatie te leggen tussen TFVC changesets en Git commits. Dit heeft de implicatie dat je Git commits een andere SHA-1 hash zullen hebben voor- en nadat ze naar TFVC zijn gepusht.

===== Git-tf[s] Workflow

Note

Onafhankelijk van de tool die je gebruikt, moet je een aantal Git configuratie instellingen inrichten om te voorkomen dat je wat problemen gaat krijgen.

$ git config set --local core.ignorecase=true
$ git config set --local core.autocrlf=false

Het meest voor de hand liggende ding wat je hierna zult wilen doen is aan het project werken. TFVC en TFS hebben een aantal kenmerken die je workflow een stuk complexer kunnen maken:

  1. Feature branches die niet voorkomen in TFVC maken het wat ingewikkelder. Dit heeft te maken met de zeer verschillende manieren waarop TFVC en Git branches weergeven.

  2. Wees erop verdacht dat TFVC gebruikers in staat stelt om files van de server uit te checken (“checkout”), en ze op slot te zetten zodat niemand anders ze kan wijzigen. Uiteraard zal dit je niet stoppen om ze in je lokale repository te wijzigen, maar dit kan je in de weg zitten als het tijd wordt om je wijzigingen naar de TFVC server te pushen.

  3. TFS kent het concept van “gated” checkins, waarbij een TFS bouw-test stap succesvol moet zijn verlopen voordat de checkin wordt toegelaten. Dit maakt gebruik van de “shelve” functie in TFVC, waar we hier niet in detail op in zullen gaan. Je kunt dit op een handmatige manier naspelen met git-tf, en git-tfs wordt geleverd met het checkintool commando die bewust is van deze gates.

In het belang van beknoptheid, zullen we hier alleen het foutloze pad volgen, die de om de meeste van deze problemen heen leidt of die ze vermijdt.

===== Workflow: git-tf

Stel dat je wat werk gedaan hebt, je hebt een aantal Git commits op master gemaakt, en je staat gereed om je voortgang te delen op de TFVC server. Hier is onze Git repository:

$ git log --oneline --graph --decorate --all
* 4178a82 (HEAD, master) update code
* 9df2ae3 update readme
* d44b17a (tag: TFS_C35190, origin_tfs/tfs) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

We willen snapshot die in de 4178a821 commit zit nemen en die pushen naar de TFVC server. Maar laten we bij het begin beginnen: laten we eerst kijken of een van onze teamgenoten iets gedaan heeft sinds we voor het laatst verbonden waren:

$ git tf fetch
Username: domain\user
Password:
Connecting to TFS...
Fetching $/myproject at latest changeset: 100%, done.
Downloaded changeset 35320 as commit 8ef06a8. Updated FETCH_HEAD.
$ git log --oneline --graph --decorate --all
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
| * 4178a82 (HEAD, master) update code
| * 9df2ae3 update readme
|/
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

Het ziet er naar uit dat er iemand ook aan het werk is, en we hebben nu een uiteengelopen historie. Dit is waar Git uitblinkt, maar we hebben een keuze uit twee verschillende manieren om door te gaan:

  1. Het maken van een merge commit voelt als een Git gebruiker als natuurlijk aan (dat is tenslotte wat git pull doet), en git-tf kan dit voor je doen met een simpele git tf pull. Wees je er echter van bewust dat TFVC zo niet in elkaar zit, en als je merge commits pusht dat je historie er aan beide kanten anders uit gaat zien, wat verwarrend kan werken. Echter, als je van plan bent om al je wijzigingen als één changeset op te sturen, is dit waarschijnlijk de eenvoudigste optie.

  2. Rebasen maakt je commit historie lineair, wat inhoudt dat we de optie hebben om elk van onze Git commits te converteren in een TFVC changeset. Omdat dit de meeste opties openhoudt, raden we je aan om het op deze manier te doen; git-tf maakt het je zelfs makkelijk met git tf pull --rebase.

De keuze is aan jou. In dit voorbeeld zullen we rebasen:

$ git rebase FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320, origin_tfs/tfs) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

Nu zijn we gereed om een checkin te doen naar de TFVC server. Git-tf geeft je de keuze om een enkele changeset te maken die alle wijzigingen sinds de laatste (--shallow, wat standaard is) en het maken van een nieuwe changeset voor elke Git commit (--deep). In dit voorbeeld zullen we één changeset maken:

$ git tf checkin -m 'Updating readme and code'
Username: domain\user
Password:
Connecting to TFS...
Checking in to $/myproject: 100%, done.
Checked commit 5a0e25e in as changeset 35348
$ git log --oneline --graph --decorate --all
* 5a0e25e (HEAD, tag: TFS_C35348, origin_tfs/tfs, master) update code
* 6eb3eb5 update readme
* 8ef06a8 (tag: TFS_C35320) just some text
* d44b17a (tag: TFS_C35190) Goodbye
* 126aa7b (tag: TFS_C35189)
* 8f77431 (tag: TFS_C35178) FIRST
* 0745a25 (tag: TFS_C35177) Created team project folder $/tfvctest via the \
          Team Project Creation Wizard

Er is een nieuwe TFS_C35348 tag, wat aangeeft dat TFVC exact dezelfde snapshot heeft opgeslagen als de 5a0e25e commit. Het is belangrijk om op te merken dat niet elke Git commit perse een exacte evenknie in TFVC dient te hebben; de 6eb3eb5 commit bijvoorbeeld bestaat nergens op de server.

Dat is de belangrijkste workflow. Er zijn een aantal andere overwegingen die je in je achterhoofd dient te houden:

  • Er is geen branching. Git-tf kan alleen Git repositories maken van één TFVC branch per keer.

  • Werk samen met TFVC of Git, maar niet beide. Verschillende git-tf clones van dezelfde TFVC repository kunnen verschillende SHA-1 commit-hashes hebben, wat de bron is van een niet aflatende stroom ellende.

  • Als de workflow van je team samenwerken met Git inhoudt en periodiek synchroniseren met TFVC, maak met maar één van de Git repositories verbinding met TFVC.

===== Workflow: git-tfs

Laten we hetzelfde scenario doorlopen waarbij we git-tfs gebruiken. Hier zijn de nieuwe commits die we gemaakt hebben op de master-branch in onze Git repository:

PS> git log --oneline --graph --all --decorate
* c3bd3ae (HEAD, master) update code
* d85e5a2 update readme
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 (tfs/default) Hello
* b75da1a New project

En laten we nu eens kijken of iemand anders werk gedaan heeft terwijl wij lekker aan het kloppen waren:

PS> git tfs fetch
C19 = aea74a0313de0a391940c999e51c5c15c381d91d
PS> git log --all --oneline --graph --decorate
* aea74a0 (tfs/default) update documentation
| * c3bd3ae (HEAD, master) update code
| * d85e5a2 update readme
|/
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

Ja, we kunnen zien dat onze collega een nieuwe TFVC changeset heeft toegevoegd, die getoond wordt als de nieuwe aea74a0 commit, en de tfs/default remote branch is verplaatst.

Net als met git-tf, hebben we twee basis opties hoe we deze uiteengelopen histories kunnen verwerken:

  1. Rebase om een lineaire historie te behouden

  2. Mergen om te bewaren wat er daadwerkelijk gebeurd is.

In dit geval zullen we een “deep” checkin uitvoeren, waar elke Git commit een TFVC changeset wordt, dus we willen gaan rebasen.

PS> git rebase tfs/default
First, rewinding head to replay your work on top of it...
Applying: update readme
Applying: update code
PS> git log --all --oneline --graph --decorate
* 10a75ac (HEAD, master) update code
* 5cec4ab update readme
* aea74a0 (tfs/default) update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

Nu zijn we klaar om onze bijdrage te leveren door onze code in te checken bij de TFVC server. Ze zullen hier het rcheckin command gebruiken om een TFVC changeset te maken voor elke Git commit in het pad van HEAD naar de eerste tfs remote branch die gevonden wordt (het checkin commando zou slechts één changeset maken, vergelijkbaar met het squashen van Git commits).

PS> git tfs rcheckin
Working with tfs remote: default
Fetching changes from TFS to minimize possibility of late conflict...
Starting checkin of 5cec4ab4 'update readme'
 add README.md
C20 = 71a5ddce274c19f8fdc322b4f165d93d89121017
Done with 5cec4ab4b213c354341f66c80cd650ab98dcf1ed, rebasing tail onto new TFS-commit...
Rebase done successfully.
Starting checkin of b1bf0f99 'update code'
 edit .git\tfs\default\workspace\ConsoleApplication1/ConsoleApplication1/Program.cs
C21 = ff04e7c35dfbe6a8f94e782bf5e0031cee8d103b
Done with b1bf0f9977b2d48bad611ed4a03d3738df05ea5d, rebasing tail onto new TFS-commit...
Rebase done successfully.
No more to rcheckin.
PS> git log --all --oneline --graph --decorate
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

Merk op hoe na elke succesvolle checkin naar de TFVC server, git-tfs het overblijvende werk rebased op wat het zojuist heeft gedaan. Dat is omdat het git-tfs-id veld onderaan de commitberichten wordt toegevoegd, wat de SHA-1 hashes verandert. Dit is precies volgens ontwerp, en er is niets om je zorgen over te maken, maar je moet je ervan bewust zijn dat dit gebeurt, vooral als je Git commits met anderen gaat delen.

TFS heeft veel functionaliteit die integreren met zijn eigen beheer systeem, zoals werkbonnen, aangewezen reviewers, gelaagde checkins (gated checkins) en zo voorts. Het kan nogal omslachtig zijn om met deze functionaliteit te werken met alleen maar een commando-regel tool, maar gelukkig stelt git-tfs je in staat om eenvoudig een grafische checkin tool aan te roepen:

PS> git tfs checkintool
PS> git tfs ct

En dat ziet er ongeveer zo uit:

De git-tfs checkin tool.
Figure 145. De git-tfs checkin tool.

Dit ziet er vertrouwd uit voor TFS gebruikers, omdat het dezelfde dialoog is die aangeroepen wordt vanuit Visual Studio.

Git-tfs laat je ook TFVC branches beheren vanuit je Git repository. Laten we als voorbeeld er eens een maken:

PS> git tfs branch $/tfvc-test/featureBee
The name of the local branch will be : featureBee
C26 = 1d54865c397608c004a2cadce7296f5edc22a7e5
PS> git log --oneline --graph --decorate --all
* 1d54865 (tfs/featureBee) Creation branch $/myproject/featureBee
* ff04e7c (HEAD, tfs/default, master) update code
* 71a5ddc update readme
* aea74a0 update documentation
| * 44cd729 (tfs/featureA, featureA) Goodbye
| * d202b53 Branched from $/tfvc-test/Trunk
|/
* c403405 Hello
* b75da1a New project

Een branch maken in TFVC houdt het maken van een changeset in waar die branch nu in bestaat, en dit is voorgesteld als een Git commit. Merk ook op dat git-tfs de tfs/featureBee remote branch aangemaakt heeft, maar dat HEAD nog steeds naar master wijst. Als je op de nieuw aangemaakte branch wilt werken, zal je je nieuwe commits op de 1d54865 willen baseren, mogelijk door een topic branch van die commit te maken.

===== Git en TFS samenvatting

Git-tf en Git-tfs zijn beide geweldige instrumenten om met een TFVC server te interacteren. Ze stellen je in staat om de kracht van Git lokaal te gebruiken, te voorkomen dat je voortdurend met de centrale TFVC server contact moet leggen, en je leven als ontwikkelaar veel eenvoudiger te maken, zonder je hele team te dwingen over te stappen op Git. Als je op Windows werkt (wat waarschijnlijk is als je team TFS gebruikt), zal je waarschijnlijk git-tfs willen gebruiken, omdat deze de meest complete functionaliteit biedt, maar als je op een ander platform werkt, zal je git-tf gebruiken die beperkter is. Zoals de meeste gereedschappen in dit hoofdstuk, zal je een van deze versie-beheer systemen als leidend kiezen, en de andere in een volgende rol gebruiken - Git of TFVC moet het middelpunt van de samenwerking zijn, niet beide.

=== Migreren naar Git

Als je een bestaande codebase in een andere VCS hebt, en je hebt besloten om Git te gaan gebruiken, moet je je project op de een of andere manier migreren. Deze paragraaf behandelt een aantal importeerders voor veelgebruikte systemen, en laat je daarna zien hoe je je eigen importeur kunt ontwikkelen. Je zult kunnen lezen hoe gegevens uit een aantal van de grote professioneel gebruikte SCM systemen te importeren, omdat zij het leeuwendeel van de gebruikers vormen die overgaan, en omdat het eenvoudig is om instrumenten van hoge kwaliteit te pakken te krijgen.

==== Subversion

Als je de vorige paragraaf leest over het gebruik van`git svn`, kan je eenvoudigweg deze instructies gebruiken om met git svn clone een repository te maken en daarna te stoppen met de Subversion server, naar een nieuwe Git server te pushen en die beginnen te gebruiken. Als je de historie wilt, kan je dat zo snel voor elkaar krijgen als je de gegevens uit de Subversion server kunt krijgen (en dat kan even duren).

Deze import is echter niet perfect, en omdat het zo lang duurt, kan je eigenlijk ook meteen maar goed doen. Het eerste probleem is de auteur-informatie. In Subversion, heeft elke persoon die commit heeft gedaan een gebruikersnaam op het systeem die wordt opgenomen in de commit-informatie. De voorbeelden in de vorige paragraaf tonen schacon in bepaalde plaatsen, zoals de blame uitvoer en de git svn log. Als je dit beter op Git auteur-gegevens wilt mappen, moet je een relatie leggen van de Subversion gebruikers naar de Git auteurs. Maak een bestand genaamd users.txt die deze mapping-informatie heeft in een formaat als deze:

schacon = Scott Chacon <schacon@geemail.com>
selse = Someo Nelse <selse@geemail.com>

Om een lijst van auteur-namen te krijgen die SVN gebruikt, kan je dit aanroepen:

$ svn log --xml --quiet | grep author | sort -u | \
  perl -pe 's/.*>(.*?)<.*/$1 = /'

Dat maakt de loguitvoer in XML formaat aan, en behoudt vervolgens alleen de regels met auteur-informatie, verwijdert duplicaten en haalt de XML tags weg. (Dit werkt duidelijk alleen op een machine met grep, sort, en perl erop geïnstalleerd). Stuur daarna de uitvoer naar je users.txt bestand zodat je de overeenkomstige Git gebruiker gegevens naast elke regel kunt zetten.

Je kunt dit bestand aan git svn geven om het te helpen de auteur gegevens beter te mappen. Je kunt git svn ook vertellen de meta-data die Subversion normaalgesproken import niet mee te nemen, door --no-metadata mee te geven aan de clone of init commando’s. Hierdoor ziet je import commando er ongeveer zo uit:

$ git svn clone http://my-project.googlecode.com/svn/ \
      --authors-file=users.txt --no-metadata --prefix "" -s my_project
$ cd my_project

Nu zou je een mooiere Subversion import moeten hebben in je my_project directory. In plaats van commits die er uit zien als dit

commit 37efa680e8473b615de980fa935944215428a35a
Author: schacon <schacon@4c93b258-373f-11de-be05-5f7a86268029>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

    git-svn-id: https://my-project.googlecode.com/svn/trunk@94 4c93b258-373f-11de-
    be05-5f7a86268029

zien ze er zo uit:

commit 03a8785f44c8ea5cdb0e8834b7c8e6c469be2ff2
Author: Scott Chacon <schacon@geemail.com>
Date:   Sun May 3 00:12:22 2009 +0000

    fixed install - go to trunk

Niet alleen ziet het Author veld er veel beter uit, maar de git-svn-id is er ook niet meer.

Je moet ook nog wat opschonen na de import. Onder andere moet je de vreemde referenties opschonen die git svn heeft gemaakt. Allereerst ga je de tags verplaatsen zodat ze echte tags zijn, in plaats van vreemde remote branches, en daarna verplaats je de overige branches zodat ze lokaal zijn.

Om de tags te verplaatsen zodat ze echte Git tags worden, roep je dit aan

$ for t in $(git for-each-ref --format='%(refname:short)' refs/remotes/tags); do git tag ${t/tags\//} $t && git branch -D -r $t; done

Dit neemt de referenties die remote branches waren en begonnen met refs/remotes/tags/ en maakt er echte (lichtgewicht) tags van.

Daarna verplaatsen we de overige referenties onder refs/remotes om er lokale branches van te maken:

$ for b in $(git for-each-ref --format='%(refname:short)' refs/remotes); do git branch $b refs/remotes/$b && git branch -D -r $b; done

Het kan gebeuren dat je een aantal extra branches ziet die vooraf worden gegaan door @xxx (waar xxx een getal is), terwijl je in Subversion aleen maar een branch ziet. Dit is eigenlijk een Subversion kenmerk genaamd “peg-revisions”, wat iets is waar Git gewoonweg geen syntactische tegenhanger voor heeft. Vandaar dat git svn eenvoudigweg het svn versienummer aan de branchnaam toevoegt op dezelfde manier als jij dit zou hebben gedaan in svn om het peg-revisie van die branch te adresseren. Als je niet meer om de peg-revisies geeft, kan je ze simpelweg verwijderen:

$ for p in $(git for-each-ref --format='%(refname:short)' | grep @); do git branch -D $p; done

Nu zijn alle oude branches echte Git branches en alle oude tags echte Git tags.

Er is nog een laatste dinge om op te schonen: Helaas maakt git svn een extra branch aan met de naam trunk, wat overeenkomt met de standaard branch in Subversion, maar de trunk-referentie wijst naar dezelfde plek als master. Omdat master idiomatisch meer Git is, is hier de manier om die extra branch te verwijderen:

$ git branch -d trunk

Het laatste om te doen is om je nieuwe Git server als een remote toe te voegen en er naar te pushen. Hier is een voorbeeld van een server als een remote toe te voegen:

$ git remote add origin git@my-git-server:myrepository.git

Omdat al je branches en tags ernaar wilt sturen, kan je nu dit aanroepen:

$ git push origin --all
$ git push origin --tags

Al je branches en tags zouden nu op je nieuwe Git server moeten staan in een mooie, schone import.

==== Mercurial

Omdat Mercurial and Git een redelijk overeenkomend model hebben om versies te representeren en omdat Git iets flexibeler is, is het converteren van een repository uit Mercurial naar Git minder omslachtig, gebruik makend van een instrument dat "hg-fast-export" heet, waar je een kopie van nodig gaat hebben:

$ git clone https://github.com/frej/fast-export.git

De eerste stap in de conversie is om een volledige kloon van de Mercurial repository die je wilt converteren te maken:

$ hg clone <remote repo URL> /tmp/hg-repo

De volgende stap is om een auteur-mapping bestand te maken. Mercurial is wat minder streng dan Git voor wat het in het auteur veld zet voor changesets, dus dit is een goed moment om schoon schip te maken. Het aanmaken hiervan is een enkele commando regel in een bash shell:

$ cd /tmp/hg-repo
$ hg log | grep user: | sort | uniq | sed 's/user: *//' > ../authors

Dit duurt een paar tellen, afhankelijk van de lengte van de geschiedenis van je project, en nadien ziet het /tmp/authors bestand er ongeveer zo uit:

bob
bob@localhost
bob <bob@company.com>
bob jones <bob <AT> company <DOT> com>
Bob Jones <bob@company.com>
Joe Smith <joe@company.com>

In dit voorbeeld, heeft dezelfde persoon (Bob) changesets aangemaakt onder vier verschillende namen, waarvan er één er wel correct uitziet, en één ervan zou voor een Git commit helemaal niet geldig zijn. Hg-fast-export laat ons dit corrigeren door elke regel in een instructie te veranderen: "<invoer>"="<uitvoer>", waarbij <invoer> in een <uitvoer> wordt gewijzigd. Binnen de <invoer> en <uitvoer> tekenreeksen, worden alle escaped reeksen die door de python string_escape encoding worden begrepen ondersteund. Als het auteur mapping-bestand geen passende <invoer> regel heeft, wordt deze ongewijzigd doorgestuurd naar Git. Als alle gebruikersnamen er goed uitzien, hebben we dit bestand helemaal niet nodig. In ons voorbeeld, willen we dat ons bestand er zo uit ziet:

"bob"="Bob Jones <bob@company.com>"
"bob@localhost"="Bob Jones <bob@company.com>"
"bob <bob@company.com>"="Bob Jones <bob@company.com>"
"bob jones <bob <AT> company <DOT> com>"="Bob Jones <bob@company.com>"

Hetzelfde soort mapping bestand kan worden gebruikt om branches en tags te hernoemen als de Mercurial naam niet wordt toegestaan door Git.

De volgende stap is om onze nieuwe Git repository aan te maken en het volgende export script aan te roepen:

$ git init /tmp/converted
$ cd /tmp/converted
$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors

De -r vlag vertelt hg-fast-export waar het de Mercurial repository kan vinden die we willen converteren, en de -A vlag vertelt het waar het auteur-mapping bestand te vinden is. Het script verwerkt Mercurial changesets en converteert ze in een script voor de "fast-import" functie (die we iets later zullen bespreken). Dit duurt even (al is het veel sneller dan het via het netwerk zou zijn), en de uitvoer is nogal breedsprakig:

$ /tmp/fast-export/hg-fast-export.sh -r /tmp/hg-repo -A /tmp/authors
Loaded 4 authors
master: Exporting full revision 1/22208 with 13/0/0 added/changed/removed files
master: Exporting simple delta revision 2/22208 with 1/1/0 added/changed/removed files
master: Exporting simple delta revision 3/22208 with 0/1/0 added/changed/removed files
[…]
master: Exporting simple delta revision 22206/22208 with 0/4/0 added/changed/removed files
master: Exporting simple delta revision 22207/22208 with 0/2/0 added/changed/removed files
master: Exporting thorough delta revision 22208/22208 with 3/213/0 added/changed/removed files
Exporting tag [0.4c] at [hg r9] [git :10]
Exporting tag [0.4d] at [hg r16] [git :17]
[…]
Exporting tag [3.1-rc] at [hg r21926] [git :21927]
Exporting tag [3.1] at [hg r21973] [git :21974]
Issued 22315 commands
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:     120000
Total objects:       115032 (    208171 duplicates                  )
      blobs  :        40504 (    205320 duplicates      26117 deltas of      39602 attempts)
      trees  :        52320 (      2851 duplicates      47467 deltas of      47599 attempts)
      commits:        22208 (         0 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:         109 (         2 loads     )
      marks:        1048576 (     22208 unique    )
      atoms:           1952
Memory total:          7860 KiB
       pools:          2235 KiB
     objects:          5625 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =      90430
pack_report: pack_mmap_calls          =      46771
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =  340852700 /  340852700
---------------------------------------------------------------------

$ git shortlog -sn
   369  Bob Jones
   365  Joe Smith

Meer valt er eigenlijk niet over te vertellen. Alle Mercurial tags zijn geconverteerd naar Git tags, en Mercurial branches en boekleggers (bookmarks) zijn geconverteerd naar Git branches. Nu ben je klaar om de repository naar zijn nieuwe server-thuis te sturen:

$ git remote add origin git@my-git-server:myrepository.git
$ git push origin --all

==== Bazaar

Bazaar is een DVCS tool die veel op Git lijkt, en als gevolg is het redelijk probleemloos om een Bazaar repository in een van Git te converteren. Om dit voor elkaar te krijgen, moet je de bzr-fastimport-plugin importeren.

===== De bzr-fastimport plugin verkrijgen

De procedure om de fastimport plugin te installeren verschilt op een UNIX-achtige besturingssysteem en op Windows. In het eerste geval, is het het eenvoudigste om het bzr-fastimport-pakket te installeren, en dat zal alle benodigde afhankelijkheden installeren.

Bijvoorbeeld, met Debian en afgeleiden, zou je het volgende doen:

$ sudo apt-get install bzr-fastimport

Met RHEL, zou je het volgende doen:

$ sudo yum install bzr-fastimport

Met Fedora, vanaf release 22, is dnf de nieuwe package manager:

$ sudo dnf install bzr-fastimport

Als het pakket niet beschikbaar is, kan je het als een plugin installeren:

$ mkdir --parents ~/.bazaar/plugins     # maakt de benodigde folders voor de plugins
$ cd ~/.bazaar/plugins
$ bzr branch lp:bzr-fastimport fastimport   # iumporteert de fastimport plugin
$ cd fastimport
$ sudo python setup.py install --record=files.txt   # installeert de plugin

Om deze plugin te laten werken, heb je ook de fastimport Python module nodig. Je kunt controleren of deze aanwezig is of niet en het installeren met de volgende commando’s:

$ python -c "import fastimport"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: No module named fastimport
$ pip install fastimport

Als het niet beschikbaar is, kan je hte downloaden van het adres https://pypi.python.org/pypi/fastimport/.

In het tweede geval (op Windows), wordt bzr-fastimport automatisch geinstalleerd met de standalone versie en de standaard installatie (laat alle checkboxen aangevinkt). Dus in dit geval hoef je niets te doen.

Vanaf dat moment, is de manier waarop je een Bazaar repository afhankelijk van of je een enkele branch hebt, of dat je op een repository werkt die meerdere branches heeft.

===== Project met een enkele branch

Ga met cd in de directory die jouw Bazaar repository bevat en initialiseer de Git repository:

$ cd /path/to/the/bzr/repository
$ git init

Nu kan je eenvoudigweg je Bazaar repository exporteren en converteren in een Git repository met het volgende commando:

$ bzr fast-export --plain . | git fast-import

Afhankelijk van de grootte van het project, wordt jouw Git repository gebouwd in een periode varierend van een paar seconden tot een paar minuten.

===== Het geval van een project met een hoofd-branch en een werk-branch

Je kunt ook een Bazaar repository importeren dat branches bevat. Laten we aannemen dat je twee branches hebt: een vertegenwoordigt de hoofd-branch (myProject.trunk), de andere is de werk-branch (myProject.work).

$ ls
myProject.trunk myProject.work

Maak de Git repository en ga er met cd erheen:

$ git init git-repo
$ cd git-repo

Pull de master-branch in git:

$ bzr fast-export --export-marks=../marks.bzr ../myProject.trunk | \
git fast-import --export-marks=../marks.git

Pull de werk-branch in Git:

$ bzr fast-export --marks=../marks.bzr --git-branch=work ../myProject.work | \
git fast-import --import-marks=../marks.git --export-marks=../marks.git

git branch zal je nu de master-branch alsook de work-branch laten zien. Controleer de logs om je ervan te vergewissen dat ze volledig zijn en verwijder de marks.bzr en de marks.git bestanden.

===== De staging area synchroniseren

Onafhankelijk van het aantal branches die je had en de import-methode die je gebruikt hebt, is je staging area niet gesynchroniseerd met HEAD, en met het importeren van verschillende branches, is je werk-directory ook niet gesynchroniseerd. Deze situatie is eenvoudig op te lossen met het volgende commando:

$ git reset --hard HEAD

===== Het negeren van de bestanden die met .bzrignore werden genegeerd

Laten we nu eens kijken naar de te negeren bestanden. Het eerste wat we moeten doen is .bzrignore naar .gitignore hernoemen. Als het .bzrignore bestand een of meerdere regels bevat die beginnen met "!!" of "RE:", zal je deze moetten wijzigen en misschien verscheidene .gitignore-bestanden maken om precies dezelfde bestanden te negerern die Bazaar ook negeerde.

Uiteindelijk zal je een commit moeten maken die deze wijziging voor de migratie bevat:

$ git mv .bzrignore .gitignore
$ # modify .gitignore if needed
$ git commit -am 'Migration from Bazaar to Git'

===== Jouw repository naar de server sturen

We zijn er! Je kunt nu de repository naar zijn nieuwe thuis-server pushen:

$ git remote add origin git@my-git-server:mygitrepository.git
$ git push origin --all
$ git push origin --tags

Je Git repository is klaar voor gebruik.

==== Perforce

Het volgende systeem waar we naar gaan kijken is het importeren uit Perforce. Zoals hierboven besproken, zijn er twee manieren om Git en Perforce met elkaar te laten praten: git-p4 en Perforce Git Fusion.

===== Perforce Git Fusion

Met Git Fusion verloopt dit proces vrijwel pijnloos. Configureer alleen je project settings, user mappings en branches met behulp van een configuratie bestand (zoals behandeld in [_p4_git_fusion]), en clone de repository. Git Fusion geeft je iets wat eruit ziet als een echte Git repository, die dan klaar is om naar een reguliere Git host te pushen als je dat wilt. Je kunt zelfs Perforce als je Git host gebruiken als je dat wilt.

===== Git-p4

Git-p4 kan ook als een importeer gereedschap werken. Als voorbeeld zullen we het Jam project importeren van de Perforce Public Depot. Om je werkstation in te richten, moet je de P4PORT omgevingsvariabele exporteren zodat deze wijst naar het Perforce depot:

$ export P4PORT=public.perforce.com:1666
Note

Om dit mee te kunnen doen, moet je een Perforce depot hebben om mee te verbinden. Wij zullen het publieke depot op public.perforce.com gebruiken in onze voorbeelden, maar je kunt elk depot waar jetoegang toe hebt gebruiken.

Roep het git p4 clone commando aan om het Jam project van de Perforce server te importeren, waarbij je het pad van het depot en het project en het pad waar je het project in wilt importeren meegeeft:

$ git-p4 clone //guest/perforce_software/jam@all p4import
Importing from //guest/perforce_software/jam@all into p4import
Initialized empty Git repository in /private/tmp/p4import/.git/
Import destination: refs/remotes/p4/master
Importing revision 9957 (100%)

Dit specifieke project heeft maar een branch, maar als je branches hebt die geconfigureerd zijn met branch views (of alleen een set directories), kan je de --detect-branches vlag gebruiken bij git p4 clone om alle branches van het project ook te importeren. Zie [_git_p4_branches] voor wat meer diepgang op dit onderwerp.

Op dit punt ben je bijna klaar. Als je naar de p4import directory gaat en git log aanroept, kan je je geïmporteerde werk zien:

$ git log -2
commit e5da1c909e5db3036475419f6379f2c73710c4e6
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

    [git-p4: depot-paths = "//public/jam/src/": change = 8068]

commit aa21359a0a135dda85c50a7f7cf249e4f7b8fd98
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

    [git-p4: depot-paths = "//public/jam/src/": change = 7304]

Je kunt zien dat git-p4 een identificerend element heeft achtergelaten in elk commit-bericht. Je kunt dit element prima daar laten, in het geval dat je in de toekomst naar het Perforce change nummer moet refereren. Echter, als je dit element wilt weghalen, is dit het moment om het te doen - voordat je begint te werken met de nieuwe repository. Je kunt git filter-branch gebruiken om de element-tekenreeksen en masse te verwijderen:

$ git filter-branch --msg-filter 'sed -e "/^\[git-p4:/d"'
Rewrite e5da1c909e5db3036475419f6379f2c73710c4e6 (125/125)
Ref 'refs/heads/master' was rewritten

Als je git log aanroept, kan je zien dat alle SHA-1 checksums voor de commits zijn gewijzigd, maar de git-p4 tekenreeksen staan niet meer in de commit-berichten:

$ git log -2
commit b17341801ed838d97f7800a54a6f9b95750839b7
Author: giles <giles@giles@perforce.com>
Date:   Wed Feb 8 03:13:27 2012 -0800

    Correction to line 355; change </UL> to </OL>.

commit 3e68c2e26cd89cb983eb52c024ecdfba1d6b3fff
Author: kwirth <kwirth@perforce.com>
Date:   Tue Jul 7 01:35:51 2009 -0800

    Fix spelling error on Jam doc page (cummulative -> cumulative).

Je import is klaar om te worden gepusht naar je nieuwe Git server.

==== TFS

Als je team haar versiebeheer uit TFVC naar Git gaat converteren, wil je de hoogst-betrouwbare conversie gebruiken die je maar kunt krijgen. Dit houdt in dat, hoewel we zowel git-tfs als git-tf in de samenwerkings-paragraaf hebben behandeld, zullen we hier alleen git-tfs behandelen, omdat git-tfs branches ondersteunt, en deze beperking het vrijwel onmogelijk maakt om git-tf hiervoor te gebruiken.

Note

Dit is een eenrichtings conversie. De Git repository die hier wordt aangemaakt kan geen verbinding meer leggen met het oorspronkelijk TFVC project.

Het eerste om te doen is gebruikersnamen mappen. TFVC is nogal ruimdenkend met wat er in het auteur veld gaat voor changesets, maar Git wil een voor de mens leesbare naam en email adres hebben. Je kunt deze informatie als volgt van het tf commando-regel client krijgen:

PS> tf history $/myproject -recursive > AUTHORS_TMP

Dit pakt alle changesets in de geschiedenis van het project en zet dit in het AUTHORS_TMP bestand die we verder gaan verwerken om de gegevens uit het User kolom (de tweede) te halen. Open het bestand en bekijk op welke karakter de kolom begint en eindigt en vervang, in de volgende commando-regel, de parameters 11-20 van het cut commando met de waarden die jij gevonden hebt:

PS> cat AUTHORS_TMP | cut -b 11-20 | tail -n+3 | sort | uniq > AUTHORS

Het cut commando behoudt alleen de karakters tussen 11 en 20 van elke regel. Het tail commando slaat de eerste twee regels over, die kolom-koppen en ASCII-art onderstrepingen zijn. Het resultaat van dit alles wordt aan sort en uniq doorgegeven om duplicaten te verwijderen en bewaard in een bestand genaamd AUTHORS. De volgende stap is handmatig; om git-tfs van dit bestand gebruik te laten maken, moet elke regel in dit formaat staan:

DOMAIN\username = User Name <email@address.com>

Het gedeelte links is het “User” veld van TFVC, en het gedeelte rechts van het gelijk-teken is de gebruikersnaam die voor Git commits gaat worden gebruikt.

Als je dit bestand eenmaal hebt, is de volgende stap om te nemen een volledige kloon van het TFVC project waar je in bent geïnteresseerd te maken:

PS> git tfs clone --with-branches --authors=AUTHORS https://username.visualstudio.com/DefaultCollection $/project/Trunk project_git

Vervolgens wil je de git-tfs-id gedeeltes aan het eind van de commit-berichten opschonen. Het volgende commando gaat dit doen:

PS> git filter-branch -f --msg-filter 'sed "s/^git-tfs-id:.*$//g"' '--' --all

Dit gebruikt het sed commando van de Git-bash omgeving om elke regel die begint met “git-tfs-id:” met leegte te vervangen, dit Git vervolgens dan zal negeren.

Als dit eenmaal gedaan is, ben je klaar om een nieuwe remote toe te voegen, al je branches te pushen, en je team is klaar om met Git te gaan werken.

==== Importeren op maat

Als jouw systeem niet een van de bovenstaande is, moet je op het internet gaan zoeken naar een importeerder - goede importeerders zijn beschikbaar voor vele andere systemen, inclusief CVS, Clear Case, Visual Source Safe en zelfs een directory met archieven. Als geen van die tools voor jou geschikt zijn, je hebt een heel obscure tool, of je hebt om een andere reden een meer aangepaste importeer proces nodig, dan moet je git fast-import gebruiken. Dit commando leest eenvoudige instructies van stdin om specifieke Git gegevens te schrijven. Het is veel eenvoudiger om op deze manier Git objecten aan te maken dan de rauwe Git commando’s te gebruiken of om zelf de rauwe objecten te schrijven (zie [ch10-git-internals] voor meer informatie). Op deze manier kan je een import script schrijven die de benodigde informatie uit het te importeren systeem leest en op stdout eenvoudige instructies afdrukt. Je kunt dit programma dan aanroepen en de uitvoer doorgeven aan git fast-import met een pipe-instructie.

Om een snelle demonstratie te geven, ga je een eenvoudige importeerder schrijven. Stel dat je in current aan het werk bent, je maakt op gezette tijden een backup van je project door de directory naar een andere te kopieren met een datum-tijd indicatie genaamd back_YYYY_MM_DD, en je wilt dit importeren in Git. Je directory-structuur ziet er zo uit:

$ ls /opt/import_from
back_2014_01_02
back_2014_01_04
back_2014_01_14
back_2014_02_03
current

Om een Git directory te importeren, moet je weten hoe Git haar gegevens opslaat. Zoals je je zult herinneren, is Git in de basis een geschakelde lijst van commit objecten die verwijzen naar een snapshot van de inhoud. Alles wat je hoeft te doen is fast-import te vertellen wat de snapshots van de inhoud zijn, welke commit-gegevens hiernaar verwijzen en de volgorde waarin ze staan. Je strategie zal zijn om een voor een door de snapshots te gaan en commits te maken met de inhoud van elke directory, en elke commit terug te laten verwijzen naar de vorige.

Zoals we in [_an_example_git_enforced_policy] gedaan hebben, schrijven we dit in Ruby, omdat dit is waar we gewoonlijk mee werken en het redelijk eenvoudig te lezen is. Je kunt dit voorbeeld redelijk eenvoudig in iets schrijven waar je bekend mee bent - het moet gewoon de juiste informatie naar stdout uitvoeren. En als je op Windows draait, betekent dit dat je ervoor moet zorgen dat je geen carriage returns aan het eind van je regels gebruikt - git fast-import is erg secuur in het alleen accepteren van line feeds (LF) niet de carriage return line feeds (CFLF) die door Windows wordt gebruikt.

Om te beginnen, spring je in de doel directory en identificeert elke subdirectory, die elk een snapshot is van wat je wilt importeren als een commit. Je springt in elke subdirectory en drukt de commando’s af die nodig zijn om het te exporteren. Je hoofdlus ziet er zo uit:

last_mark = nil

# loop through the directories
Dir.chdir(ARGV[0]) do
  Dir.glob("*").each do |dir|
    next if File.file?(dir)

    # move into the target directory
    Dir.chdir(dir) do
      last_mark = print_export(dir, last_mark)
    end
  end
end

Je roept print_export in elke directory aan, die de inhoud en kenmerk (mark) van de vorige snapshot neemt en je de inhoud en kenmerk van deze teruggeeft; op die manier kan je ze goed koppelen.

“Mark” is de term die fast-import gebruikt voor een identificatie die je aan een commit geeft; tijdens het aanmaken van commit geef je elk een kenmerk die je kunt gebruiken om als koppeling vanaf andere commits. Dus, het eerste wat je moet doen in je print_export methode is een kenmerk genereren van de naam van de directory:

mark = convert_dir_to_mark(dir)

Je doet dit door een reeks (array) directories te maken en de indexwaarde als het kenmerk te gebruiken, omdat een kenmerk een integer dient te zijn. Je methode ziet er zo uit:

$marks = []
def convert_dir_to_mark(dir)
  if !$marks.include?(dir)
    $marks << dir
  end
  ($marks.index(dir) + 1).to_s
end

Nu je een integer representatie hebt van je commit, heb je een datum nodig voor de commit metadata. Omdat de datum in de naam van de directory zit, ga je het eruit halen. De volgende regel in je print_export bestand is:

date = convert_dir_to_date(dir)

where convert_dir_to_date is defined as:

def convert_dir_to_date(dir)
  if dir == 'current'
    return Time.now().to_i
  else
    dir = dir.gsub('back_', '')
    (year, month, day) = dir.split('_')
    return Time.local(year, month, day).to_i
  end
end

Dat retourneert een integerwaarde voor de datum van elke directory. Het laatste stukje meta-informatie dat je nodig hebt voor elke commit zijn de gegevens van de committer, die je hardgecodeerd hebt in een globale variabele:

$author = 'John Doe <john@example.com>'

Nu ben je klaar om de commit data af te drukken voor je importeerder. De initiële informatie geeft aan dat je een commit object definieert en op welke branch deze staat, gevolgd door het kenmerk die je hebt gegenereert, de informatie van de committer en het commit bericht, en dan de vorige commit als die er is. De code ziet er zo uit:

# print the import information
puts 'commit refs/heads/master'
puts 'mark :' + mark
puts "committer #{$author} #{date} -0700"
export_data('imported from ' + dir)
puts 'from :' + last_mark if last_mark

Je geeft de tijdzone (-0700) hardgecodeerd, omdat dit nu eenmaal makkelijk is. Als je vanuit een ander systeem importeert, moet je de tijdzone als een relatieve verschuiving (offset) weergeven. Het commit bericht moet worden uitgedrukt in een speciaal formaat:

data (size)\n(contents)

Het formaat bestaat uit het woord data, de grootte van de te lezen gegevens, een nieuwe regel en tot slot de gegevens. Omdat je hetzelfde formaat later nodig hebt om de bestandsinhoud te specificeren, maak je een hulpmethode, export_data:

def export_data(string)
  print "data #{string.size}\n#{string}"
end

Wat er nu nog overblijft is het specifieren van de bestandsinhoud voor elk snapshot. Dit is makkelijk, omdat je elk van deze in een directory hebt - je kunt het deleteall commando afdrukken gevolgd door de inhoud van elk bestand in de directory. Git zal dan elke snapshot op de juiste manier opslaan:

puts 'deleteall'
Dir.glob("**/*").each do |file|
  next if !File.file?(file)
  inline_data(file)
end

Let op: Omdat veel systemen hun revisies zien als wijzigingen van de ene commit naar de ander, kan fast-import ook commando’s accepteren waar bij elke commit kan worden aangegeven welke bestanden er zijn toegevoegd, verwijderd of gewijzigd en welke nieuwe elementen erbij zijn gekomen. Je kunt de verschillen tussen de snapshots berekenen en alleen deze gegevens meegeven, maar het is veel ingewikkelder om dit te doen - je kunt net zo makkelijk Git alle gegevens meegeven en het hem laten uitzoeken. Als dit beter past bij jouw gegevens, neem dan de fast-import man page door voor de details hoe je deze gegevens moet doorgeven in dat geval.

Het formaat om de nieuwe bestandsinhoud weer te geven of om een gewijzigd bestand te specificeren met de nieuwe inhoud is als volgt:

M 644 inline path/to/file
data (size)
(file contents)

Hier is 644 de mode (als je aanroepbare bestanden hebt, moet je dit detecteren en in plaats daarvan 755 opgeven), en in de code (inline) geef je de inhoud direct achter deze regel weer. Je inline_data methode ziet er zo uit:

def inline_data(file, code = 'M', mode = '644')
  content = File.read(file)
  puts "#{code} #{mode} inline #{file}"
  export_data(content)
end

Je hergebruikt de export_data methode die je eerder hebt gedefinieerd, omdat dit formaat hetzelfde is als waarmee je de commit bericht gegevens specificeert.

Het laatste wat je nog moet doen is om het huidige kenmerk te retourneren, zodat het kan worden doorgegeven in de volgende iteratie:

return mark
Note

Als je op Windows draait moet je je ervan verzekeren dat je een extra stap toevoegt. Zoals eerder opgemerkt, gebruikt Windows CRLF voor nieuwe regel tekens waar git fast-import alleen LF verwacht. Om dit probleem te verhelpen en git fast-import blij te maken, moet je ruby vertellen om LF te gebruiken in plaats van CRLF:

$stdout.binmode

Dat is 't. Hier is het script in zijn totaliteit:

#!/usr/bin/env ruby

$stdout.binmode
$author = "John Doe <john@example.com>"

$marks = []
def convert_dir_to_mark(dir)
    if !$marks.include?(dir)
        $marks << dir
    end
    ($marks.index(dir)+1).to_s
end

def convert_dir_to_date(dir)
    if dir == 'current'
        return Time.now().to_i
    else
        dir = dir.gsub('back_', '')
        (year, month, day) = dir.split('_')
        return Time.local(year, month, day).to_i
    end
end

def export_data(string)
    print "data #{string.size}\n#{string}"
end

def inline_data(file, code='M', mode='644')
    content = File.read(file)
    puts "#{code} #{mode} inline #{file}"
    export_data(content)
end

def print_export(dir, last_mark)
    date = convert_dir_to_date(dir)
    mark = convert_dir_to_mark(dir)

    puts 'commit refs/heads/master'
    puts "mark :#{mark}"
    puts "committer #{$author} #{date} -0700"
    export_data("imported from #{dir}")
    puts "from :#{last_mark}" if last_mark

    puts 'deleteall'
    Dir.glob("**/*").each do |file|
        next if !File.file?(file)
        inline_data(file)
    end
    mark
end

# Loop through the directories
last_mark = nil
Dir.chdir(ARGV[0]) do
    Dir.glob("*").each do |dir|
        next if File.file?(dir)

        # move into the target directory
        Dir.chdir(dir) do
            last_mark = print_export(dir, last_mark)
        end
    end
end

Als je dit script aanroept, krijg je inhoud dat er ongeveer zo uit ziet:

$ ruby import.rb /opt/import_from
commit refs/heads/master
mark :1
committer John Doe <john@example.com> 1388649600 -0700
data 29
imported from back_2014_01_02deleteall
M 644 inline README.md
data 28
# Hello

This is my readme.
commit refs/heads/master
mark :2
committer John Doe <john@example.com> 1388822400 -0700
data 29
imported from back_2014_01_04from :1
deleteall
M 644 inline main.rb
data 34
#!/bin/env ruby

puts "Hey there"
M 644 inline README.md
(...)

Om de importeerder te laten draaien, geeft je de uitvoer met een pipe door aan git fast-import terwjil je in de Git directory staat waar je naartoe wilt importeren. Je kunt een nieuwe directory aanmaken en daarna git init daarin aanroepen als een begin, en daarna je script aanroepen:

$ git init
Initialized empty Git repository in /opt/import_to/.git/
$ ruby import.rb /opt/import_from | git fast-import
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:           13 (         6 duplicates                  )
      blobs  :            5 (         4 duplicates          3 deltas of          5 attempts)
      trees  :            4 (         1 duplicates          0 deltas of          4 attempts)
      commits:            4 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           1 (         1 loads     )
      marks:           1024 (         5 unique    )
      atoms:              2
Memory total:          2344 KiB
       pools:          2110 KiB
     objects:           234 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize = 1073741824
pack_report: core.packedGitLimit      = 8589934592
pack_report: pack_used_ctr            =         10
pack_report: pack_mmap_calls          =          5
pack_report: pack_open_windows        =          2 /          2
pack_report: pack_mapped              =       1457 /       1457
---------------------------------------------------------------------

Zoals je kunt zien, als het succesvol eindigt, geeft het je een bergje statistieken wat het allemaal heeft bereikt. In dit geval, heb je in totaal 13 objecten geïmporteerd voor 4 commits in 1 branch. Je kunt nu git log aanroepen om je nieuwe historie te bekijken:

$ git log -2
commit 3caa046d4aac682a55867132ccdfbe0d3fdee498
Author: John Doe <john@example.com>
Date:   Tue Jul 29 19:39:04 2014 -0700

    imported from current

commit 4afc2b945d0d3c8cd00556fbe2e8224569dc9def
Author: John Doe <john@example.com>
Date:   Mon Feb 3 01:00:00 2014 -0700

    imported from back_2014_02_03

Kijk eens aan - een mooie, schone Git repository. Het is belangrijk om op te merken dat er niets is uitgecheckt - je hebt initieel nog geen enkel bestand in je werk directory. Om deze te krijgen, moet je je branch resetten tot waar master nu is:

$ ls
$ git reset --hard master
HEAD is now at 3caa046 imported from current
$ ls
README.md main.rb

Je kunt nog veel meer doen met het fast-import gereedschap - verschillende modes behandelen, binaire gegevens, meerdere branches en mergen, tags, voortgangs-indicators en meer. Een aantal voorbeelden van meer ingewikkelde scenarios zijn beschikbaar in de contrib/fast-import directory van de Git broncode.

=== Samenvatting

Je zou je op je gemak moeten voelen om Git als een client te gebruiken voor andere versiebeheer systemen, of bijna ieder bestaand repository te importeren in een Git versie zonder gegevens te verliezen. Het volgende hoofdstuk zal het ruwe binnenwerk van Git behandelen, zodat je iedere byte kunt bewerken, als dat nodig is.

== Git Binnenwerk

Je bent misschien meteen doorgegaan naar dit hoofdstuk vanuit een eerder hoofdstuk, of je bent misschien hier beland nadat je de rest van het boek gelezen hebt - hoe dan ook, dit is waar we werking onder de motorkap en de implemantatie van Git gaan behandelen. We zijn van mening dat het leren van deze informatie fundamenteel belangrijk was om te begrijpen hoe nuttig en krachtig Git is, maar anderen hebben ons duidelijk gemaakt dat het juist verwarrend kan zijn en onnodig complex voor beginners. Daarom hebben we deze behandeling als laatste hoofdstuk in het boek opgenomen zodat je het vroeg of laat in je leerproces kunt gaan lezen. We laten deze beslissing geheel aan jou over.

Maar nu je hier toch bent, laten we beginnen. Ten eerste, als het nog niet duidelijk mocht zijn, Git is in wezen een op inhoud-adresseerbaar bestandssysteem met een VCS (versiebeheer) gebruikers interface erbovenop geschreven. Je zult straks meer lezen over de betekenis hiervan.

In de begindagen van Git (vooral voor 1.5), was de gebruikers interface veel complexer omdat de nadruk lag op dit bestandssysteem en veel minder op een strak versiebeheer systeem. In de laatste paar jaren is de gebruikersinterface bijgeslepen totdat het net zo gemakkelijk te gebruiken was als ieder ander systeem; maar vaak hangt het stereotype nog rond van de vroege Git interface die zo complex was en moeilijk te leren.

Het op inhoud-adresseerbare bestandssysteemlaag is ongelofelijk gaaf, dus we zullen die eerst in dit hoofdstuk behandelen; daarna zullen je vertellen over de transport mechanismen en het taken voor onderhoud van de repository waar je op den duur mee te maken kunt krijgen.

=== Binnenwerk en koetswerk (plumbing and porcelain)

Dit boek behandelt primair hoe Git te gebruiken met ongeveer 30 werkwoorden als checkout, branch, remote, enzovoort. Maar omdat Git initieel een gereedschapkist was voor een versiebeheersysteem in plaats van een compleet gebruikersvriendelijke versibeheersysteem, heeft het een aantal werkwoorden die het grondwerk verzorgen en die ontworpen zijn om op een UNIX manier te worden gekoppeld of vanuit scripts te worden aangeroepen. Aan deze commando’s worden over het algemeen gerefereerd als “plumbing” (binnenwerk) commando’s, en de meer gebruikersvriendelijke commando’s worden “porcelain” (koetswerk) genoemd.

Je zult nu wel gemerkt hebben dat de eerste negen hoofdstukken van het boek houden zich vrijwel exclusief bezig met porcelain commando’s. Maar in dit hoofdstuk ga je meerendeels te maken krijgen met de plumbing commando’s op het diepere niveau, omdat deze je toegang geven tot het binnenwerk van Git, en om je te laten zien hoe en waarom Git doet wat het doet. Veel van deze commando’s zijn niet bedoeld voor handmatig gebruik op de commando-regel, maar meer bedoeld als gebruik als onderdeel voor nieuwe gereedschappen en eigengemaakte scripts.

Wanneer je git init aanroept in een nieuwe of bestaande directory, maakt Git de .git directory aan, waar vrijwel alles wat Git opslaat en bewerkt aanwezig is. Als je een backup wilt maken of je repository wilt klonen, geeft het kopieren van deze ene directory naar elders je vrijwel alles wat je nodig hebt. Deze hele hoofdstuk behandelt eigenlijk het spul dat je in deze directory kunt zien. Dit is hoe een nieuw-gemaakte .git-directory er normaalgesproken uitziet:

$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/

Afhankelijk van je versie van Git, zou je wat extra inhoud kunnen zien, maar dit is een versie git init repository — dit is wat je standaard ziet. Het description bestand wordt alleen gebruikt door het GitWeb programma, dus maak je er geen zorgen over. Het config bestand bevat jouw project-specifieke configuratie opties, en de info directory bevat een globale exclude bestand voor genegeerde patronen die je niet wilt tracken in een .gitignore bestand. De hooks directory bevat de hook-scripts voor de werkstation of server kant, waar we dieper op zijn ingegaan in [_git_hooks].

Resten vier belangrijke regels: de HEAD en (nog te maken) index bestanden, en de objects en refs directories. Dit zijn de kern-onderdelen van Git. De objects directory bevat alle inhoud voor jouw database, de refs directory bevat verwijzingen naar commit objecten in die gegevens (branches, tags, remotes en zo meer), het HEAD bestand verwijst naar de branch die je op dit moment uitgecheckt hebt, en het index bestand is waar Git de informatie over je staging gebied bewaart. We zullen hierna dieper ingaan op elk van deze onderdelen om te zien hoe Git werkt.

=== Git objecten

Git is een op inhoud-adresseerbaar bestandssysteem. Mooi. Wat betekent dat nu eigenlijk? Het betekent dat in het hart van Git een eenvoudig sleutel-waarde gegevens opslag zit. Je kunt elke vorm van inhoud erin stoppen, en het zal je een sleutel teruggeven die je kunt gebruiken om de inhoud op elk gewenst moment weer op te halen.

Om dit te laten zien, zal je het plumbing commando hash-object gebruiken, die gegevens aanneemt, dit opslaat in je .git directory en je de sleutel teruggeeft waarmee de gegevens zijn opgeslagen.

Eerst, moet je een nieuwe Git repository initialiseren en vaststellen dat er niets in de objects directory zit:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

Git heeft nu de objects directory geinitialiseerd en daarin pack en info subdirectories aangemaakt, maar er zijn geen reguliere bestanden. Nu sla je wat tekst op in je Git database:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

In het eenvoudigste vorm neemt het git hash-object de inhoud die je hem hebt gegeven en alleen maar de unieke sleutel teruggeven die zou worden gebuikt als je het zou opslaan in de Git database. De -w vertelt hash-object om niet simpelweg de sleutel terug te geven maar om het object op te slaan in de database. Tot slot vertelt de --stdin optie het commando om de te verwerken inhoud van stdin te lezen; als je dit niet aangeeft, verwacht het commando een bestands-argument aan het eind van het commando met daarin de inhoud die moet worden verwerkt.

De uitvoer van het commando is een 40-karakter checksum hash. Dit is de SHA-1 hash — een controlegetal van de inhoud die je opslaat plus een header, waar je iets later meer over gaat lezen. Nu kan je zien hoe Git je gegevens heeft opgeslagen:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Als je weer je objects directoy gaat bekijken, kan je zien dat het nu een bestand bevat voor die nieuwe inhoud. Dit is hoe Git de inhoud initieel opslaat — als een enkel bestand per stuk inhoud, met het SHA-1 controlegetal als naam die is berekend over de inhoud en de header. De subdirectory heeft de eerste 2 karakters van de SHA-1 als naam, en de bestandsnaam is de overige 38 karakters.

Als je inhoud in je object database hebt, kan je de inhoud bekijken met het git cat-file commando. Dit commando is een soort Zwitsers zakmes voor het inspecteren van Git objecten. Met het doorgeven van -p vertel je het cat-file commando om het type inhoud uit te zoeken en het netjes aan je te laten zien:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

Nu ben je in staat om inhoud aan Git toe te voegen en om het er weer uit te halen. Je kunt dit ook met de inhoud van bestanden doen. Bijvoorbeeld, je kunt een eenvoudig versiebeheer op een bestand doen. Eerst, maak een nieuw bestand aan en bewaar de inhoud in je database:

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

Daarna schrijf je wat nieuwe inhoud naar het bestand, en bewaart het opnieuw:

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

Je object database bevat beide versies van dit nieuwe bestand (zowel als de eerste inhoud die je daar bewaard hebt):

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

Op dit moment kan je je lokale kopie van dat test.txt bestand verwijderen, en dan Git gebruiken om uit de object database de eerste versie die je bewaard hebt:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

of de tweede versie op te halen:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

Maar de SHA-1 sleutel onthouden voor elke versie van je bestand is niet praktisch; plus je bewaart niet de bestandsnaam in je systeem — alleen de inhoud. Dit type object noemen we een blob. Je kunt Git je het objecttype laten vertellen van elk object in Git, gegeven de SHA-1 sleutel, met git cat-file -t:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

==== Boom objecten (tree objects)

Het volgende type waar we naar gaan kijken is de tree, wat het probleem oplost van het opslaan van bestandsnamen en je ook in staat stelt om een groep van bestanden bij elkaar op te slaan. Git slaat de inhoud op een manier die vergelijkbaar is met een UNIX bestandssysteem, maar wat versimpeld. Alle inhoud wordt opgeslagen als tree en blob objecten, waarbij trees overeenkomen met UNIX directory entries en blobs min of meer overeenkomen met inodes of bestandsinhoud. Een enkele tree object bevat een of meer tree entries, elk daarvan bevat een SHA-1 verwijzing naar een blob of subtree met de bijbehorende mode, type en bestandsnaam. Bijvoorbeeld, de meest recente tree in een project kan er ongeveer zo uitzien:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

De master^{tree} syntax geeft het tree object aan waarnaar wordt verwezen door de laatste commit op je master-branch. Merk op dat de lib subdirectory geen blob is, maar een verwijzing naar een andere tree:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb
Note

Afhankelijk van de shell die je gebruikt, kan je fouten krijgen als je de master^{tree} syntax gebruikt.

In CMD op Windows is het ^ karakter gebruikt voor escapen, dus je moet het verdubbelen om dit te voorkomen: git cat-file -p master^^{tree}. Als je PowerShell gebruikt, moeten parameters waarin de {} karakters worden gebruikt van quotes worden voorzien om te voorkomen dat ze verkeerd worden geinterpreteerd: git cat-file -p 'master^{tree}'.

Als je ZSH gebruikt, wordt het ^ karakter gebruikt voor globbing, dus je moet de hele expressie in quotes zetten: git cat-file -p "master^{tree}".

Conceptueel zijn de gegevens die Git opslaat ongeveer dit:

Eenvoudige versie van het Git datamodel.
Figure 146. Eenvoudige versie van het Git datamodel.

Je kunt redelijk eenvoudig je eigen boom maken. Git maakt normaalgesproken een tree door de staat van je staging gebied of index te nemen en daarvan een reeks tree objects te maken. Dus, om een tree object te maken, moet je eerst een index opzetten door wat bestanden te stagen. Om een index te maken met een enkele ingang — de eerste versie van je test.txt bestand — kan je het plumbing commando git update-index gebruiken. Je gebruikt dit commando om kunstmatig de eerdere versie van het bestand test.txt aan een nieuwe staging gebied toe te voegen. Je moet het de optie --add doorgeven omdat het bestand nog niet bestaat in je staging gebied (je hebt nog niet eens een staging gebied ingericht) en --cacheinfo omdat het bestand dat je toevoegt in in je directory zit maar in je database. Daarna geef je de mode, SHA-1 en bestandsnaam op:

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

In dit geval geef je een mode 100644 op, wat aangeeft dat het een normaal bestand is. Andere opties zijn 100755, wat aangeeft dat het een uitvoerbaar bestand is; en 120000, wat een symbolische link aangeeft. De modus is afgeleid van normale UNIX modi maar het is minder flexibel — deze drie modi zijn de enige die geldig zijn voor bestanden (blobs) in Git (alhoewel andere modi worden gebruikt voor directories en submodules).

Nu kan je het git write-tree commando gebruiken om het staging gebied te schrijven naar een tree object. Hier is geen -w optie nodig — het aanroepen van write-tree maakt automatisch een tree object aan van de staat van de index als die tree nog niet bestaat:

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

Je kunt ook verifiëren dat dit een tree object is met hetzelfde git cat-file commando die je eerder gezien hebt:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

Je gaat nu een nieuwe tree maken met de tweede versie van test.txt en ook nog een nieuw bestand:

$ echo 'new file' > new.txt
$ git update-index test.txt
$ git update-index --add new.txt

Je staging gebied heeft nu de nieuwe versie van test.txt alsook het nieuwe bestand new.txt. Schrijf dat deze tree weg (sla de staat van het staging gebied of index op in een tree object) en kijk hoe dit eruit ziet:

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Merk op dat deze tree beide bestands entries bevat en ook dat de test.txt SHA-1 gelijk aan de “versie 2” SHA-1 van eerder is (1f7a7a). Puur voor de lol, ga je de eerste tree als een subdirectory toevoegen in deze. Je kunt trees in je staging area lezen door git read-tree aan te roepen. In dit geval, kan je een bestaande tree als een subtree in je staging gebied lezen door de --prefix optie te gebruiken bij dit commando:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

Als je een werk directory van de nieuwe tree maakt die je zojuist geschreven hebt, zou je de twee bestanden op het hoogste niveau van de werk directory krijgen en een subdirectory met de naam bak die de eerste versie van het test.txt bestand zou bevatten. Je kunt de gegevens die Git bevat voor deze structuren als volgt weergeven:

De inhoudsstructuur van je huidige Git gegevens.
Figure 147. De inhoudsstructuur van je huidige Git gegevens.

==== Commit objecten

Je hebt drie trees die de verschillende snapshots weergeven van je project die je wilt volgen, maar het eerdere probleem blijft: je moet alle drie SHA-1 waarden onthouden om de snapshots te kunnen terughalen. Je hebt ook geen informatie over wie de snapshots heeft opgeslagen, wanneer ze zijn opgeslagen of waarom ze waren opgeslagen. Dit is de basis informatie die het commit object voor je opslaat.

Om een commit object te maken, roep je commit-tree aan en geef je een de SHA-1 van een enkele tree op en welke commit objecten, indien van toepassing, er direct aan vooraf gaan. Begin met de eerste tree die je geschreven hebt:

$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d

Je zult verschillende hash-waarden krijgen omdat er verschillen zijn in aanmaak tijd en auteur-gegevens. Vervang de commit en tag-hashwaarden verderop in dit hoofdstuk met je eigen checksums. Nu kan je naar je nieuwe commit object kijken met git cat-file:

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

first commit

Het formaat voor een commit object is eenvoudig: het geeft de tree op het hoogste niveau weer voor de snapshot van het project op dat punt; de auteur/committer informatie (welke je user.name en user.email configuratie instellingen gebruikt en een timestamp); een blanko regel en dan het commit bericht.

Vervolgens ga je de andere twee commit objects schrijven, die elk naar de commit refereren die er direct aan vooraf ging:

$ echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

Elk van de drie commit objecten verwijzen naar een van de drie snapshot trees die je gemaakt hebt. Gek genoeg, heb je nu een echte Git historie die je kunt bekijken met het git log commando, als je dit aanroept op de laatste SHA-1 commit:

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

	third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700

	second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

Verbluffend. Je hebt zojuist de diepere niveau operaties uitgevoerd die een Git historie hebben opgebouwd, zonder gebruik te maken van een van de hogere commando’s. In essentie is dit wat Git doet als je de git add en git commit commando’s gebuikt — het slaat blobs op voor de bestanden die zijn gewijzigd, werkt de index bij, schrijft de trees weg en schrijft commit objecten weg die verwijzen naar de trees op het hoogste niveau en de commits die er direct aan vooraf zijn gegaan. Deze drie hoofd Git objecten — de blob, de tree en de commit worden initieel opgeslagen als aparte bestanden in je .git/objects directory. Hier zijn alle huidige objecten in de voorbeeld directory, met als commentaar wat ze opslaan:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Als je al de interne verwijzingen volgt, zal je een objectgraaf krijgen die er ongeveer zo uitziet:

Alle bereikbare objects in je Git directory.
Figure 148. Alle bereikbare objects in je Git directory.

==== Object opslag

We hebben eerder gezegd dat er een header wordt opgeslagen bij de inhoud. Laten we nu de tijd nemen om een kijkje te nemen hoe Git haar objecten opslaat. Je gaat zien hoe een blob object interactief wordt opgeslagen — in dit geval, de zin “what is up, doc?” — in de Ruby scripttaal.

Je kunt de interactieve Ruby modus starten met het irb commando:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Git stelt een header samen die begint met het type van het object, in dit geval een blob. Aan dat eerste gedeelte van de header voegt Git een spatie toe gevolgd door de lengte in bytes van de inhoud en tot slot een null byte:

>> header = "blob #{content.length}\0"
=> "blob 16\u0000"

Git plakt de header en de oorspronkelijke inhoud samen en berekent het SHA-1 controlegetal van die nieuwe inhoud. Je kunt de SHA-1 waarde van een tekenreeks in Ruby berekenen door de SHA1 digest library te includen met het require commando en daarna Digest::SHA1.hexdigest() aan te roepen met de tekenreeks:

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

Laten we dit vergelijken met de uitvoer van git hash-object. Hier gebruiken we echo -n om te voorkomen dat er een newline aan de invoer wordt toegevoegd.

$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37

Git comprimeert de nieuwe inhoud met zlib, wat je in Ruby kunt doen met de zlib library. Eerst moet je de library requiren en daarna Zlib::Deflate.deflate() aanroepen op de inhoud:

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

Als laatste schrijf je je zlib-deflated inhoud naar een object op schijf. Je bepaalt het pad van het object die je wilt schrijven (de eerste twee karakters van de SHA-1 waarde als de naam van de subdirectory, en de overige 38 karakters zijnde de bestandsnaam binnen die directory). In Ruby kan je de FileUtils.mkdir_p() functie gebruiken om de subdirectory aan te maken als die nog niet bestaat. Daarna open je het bestand met File.open() en schrijf je de eerder zlib-gecomprimeerde inhoud naar het bestand met een write() aanroep op de filehandle die je krijgt:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32

Laten we de inhoud van het object controleren met git cat-file:

---
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?
---

Dat is alles - je hebt een valide Git blob object gemaakt. Alle Git objecten worden op dezelfde manier opgeslagen, alleen met andere types - in plaats van de tekenreeks blob, begint de header met commit of tree. Daarnaast, alhoewel de inhoud van een blob zo ongeveer alles kan zijn, is de inhoud van een commit en tree zeer specifiek geformatteerd.

=== Git Referenties

Als je geinteresseerd bent in zien van de geschiedenis van je repository vanaf commit, zeg maar, 1a410e, zou je iets als git log 1a410e kunnen aanroepen om die geschiedenis te bekijken, maar je moet nog steeds onthouden dat 1a410e de commit is die je als beginpunt wilt gebruiken voor die geschiedenis. Het in plaats daarvan handiger zijn als je een bestand zou hebben waarin je die SHA-1 waarde kunt opslaan onder een eenvoudiger naam zoudat je die eenvoudiger naam zou kunnen gebruiken in plaats van die kale SHA-1 waarde.

In Git worden deze simpele namen “referenties” of “refs” genoemd, en je kunt de bestanden die deze SHA-1 waarden bevatten vinden in de .git/refs directory. In het huidige project, bevat deze directory geen bestanden, maar wat er wel in zit is een simpele structuur:

$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f

Om een nieuwe referentie te maken die je gaat helpen onthouden waar je laatste commit is, kan je technisch gezien iets simpels als dit doen:

$ echo 1a410efbd13591db07496601ebc7a059dd55cfe9 > .git/refs/heads/master

Nu kan je de head-referentie die je zojuist gemaakt hebt in je Git commando’s gebruiken in plaats van de SHA-1 waarde:

$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Het wordt je niet aangeraden om de referentiebestanden direct te bewerken. Git voorziet in een veiliger commando genaamd update-ref als je een referentie wilt bijwerken:

$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

Dit is wat een branch in Git eigenlijk is: een eenvoudige verwijzing of referentie naar de head van een bepaalde werkomgeving. Om achteraf een branch te maken naar de tweede commit, kan je dit doen:

$ git update-ref refs/heads/test cac0ca

Je branch zal alleen werk bevatten van die commit en daarvoor:

$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Nu ziet je Git database er conceptueel zo ongeveer uit:

Git directory objecten inclusief branch head referenties.
Figure 149. Git directory objecten inclusief branch head referenties.

Als je commando’s aanroept zoals git branch <branchnaam>, roept Git feitelijk die update-ref commando aan om de SHA-1 van de laatste commit op de branch waar je op zit te plaatsten in de referentie die je op dat moment wilt aanmaken.

==== De HEAD

De vraag is nu, als je git branch <branchnaam> aanroept, hoe weet Git de SHA-1 van de laatste commit? Het antwoord is het HEAD bestand.

Het HEAD bestand is een symbolische referentie naar de branch waar je op dit moment op zit. Met symbolische referentie bedoelen we dat, in tegenstelling tot een normale referentie, het over het algemeen geen SHA-1 waarde bevat, maar een verwijzing naar een andere referentie. Als je naar het bestand kijkt, zie je normaalgesproken zoiets als dit:

$ cat .git/HEAD
ref: refs/heads/master

Als je git checkout test aanroept, werkt Git het bestand bij om er zo uit te zien:

$ cat .git/HEAD
ref: refs/heads/test

Als je git commit aanroept, wordt het commit object aangemaakt, waarbij de ouder van dat commit object wordt gespecificeerd door de SHA-1 waarde die staat in de referentie waar HEAD op dat moment naar verwijst.

Je kunt dit bestand ook handmatig bijwerken, maar alweer is er een veiliger comando om dit te doen: symbolic-ref. Je kunt de waarde van je HEAD met dit commando lezen:

$ git symbolic-ref HEAD
refs/heads/master

Je kunt ook de waarde van HEAD bepalen met het zelfde commando:

$ git symbolic-ref HEAD refs/heads/test
$ cat .git/HEAD
ref: refs/heads/test

Je kunt geen symbolische referentie waarde invullen die niet voldoet aan de de refs-stijl:

$ git symbolic-ref HEAD test
fatal: Refusing to point HEAD outside of refs/

==== Tags

We zijn zojuist geëindigt met het bespreken van de drie hoofd objecttypen van Git (blobs, trees en commits), maar er is nog een vierde. Het tag object lijkt erg op een commit object — het bevat een tagger, een datum, een bericht en een verwijzing. Het belangrijkste verschil is dat een tag object over het algemeen verwijst naar een commit in plaats van een boom. Het lijkt op een branch referentie, maar het zal nooit bewegen — het verwijst altijd naar dezelfde commit, maar geeft het een vriendelijkere naam.

Zoals besproken in Git Basics, zijn er twee soorten tags: geannoteerd en lichtgewicht. Je kunt een lichtgewicht tag aanmaken door iets als het volgende aan te roepen:

$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d

Dat is alles wat een lichtgewicht tag is — een referentie dat nooit zal bewegen. Een geannoteerde tag is echter veel complexer. Als je een geannoteerde tag aanmaakt, maakt Git een tag object en schrijft daarna een referentie die daarnaar verwijst, in plaats van direct te verwijzen naar de commit. Je kunt dit zien door een geannoteerde tag aan te maken (met de -a optie):

$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'

Hier is de SHA-1 waarde van het object die is aangemaakt:

$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2

En roep nu het git cat-file -p commando aan op die SHA-1 waarde;

$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700

test tag

Merk op dat vermelding van het object wijst naar de SHA-1 waarde van de commit die je hebt getagged. Merk ook op dat het niet perse hoeft te verwijzen naar een commit; je kunt elke Git object taggen. In de broncode van Git, bijvoorbeeld, heeft de onderhouder hun GPG publieke sleutel als een blob object toegevoegd en deze toen getagged. Je kunt de publieke sleutel bekijken door het volgende aan te roepen in een cloon van de Git repository:

$ git cat-file blob junio-gpg-pub

De Linux kernel repository heeft ook een tag die niet naar een commit wijst — de eerste tag die gemaakt is verwijst naar de initiële tree van de import van de broncode.

==== Remotes

Het derde type referentie die je zult zien is een remote referentie. Als je een remote toevoegt en ernaar pusht, slaat Git voor elke de waarde die je het laatst naar die remote hebt gepusht in de refs/remotes directory. Bijvoorbeeld, je kunt een remote genaamd origin toevoegen en daar je master-branch naar pushen:

$ git remote add origin git@github.com:schacon/simplegit-progit.git
$ git push origin master
Counting objects: 11, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 716 bytes, done.
Total 7 (delta 2), reused 4 (delta 1)
To git@github.com:schacon/simplegit-progit.git
  a11bef0..ca82a6d  master -> master

Je kunt zien wat de master-branch op de origin remote was op met moment dat je voor het laatst met de server hebt gecommuniceerd, door het refs/remotes/origin/master bestand te bekijken:

$ cat .git/refs/remotes/origin/master
ca82a6dff817ec66f44342007202690a93763949

Referenties van remotes verschillen van branches (refs/heads referenties) voornamelijk door het feit dat ze als alleen-lezen worden beschouwd. Je kunt naar een git checkout doen, maar Git zal HEAD nooit naar een laten verwijzen, dus je zult er nooit een kunnen bijwerken met een commit commando. Git gebruikt ze als boekleggers naar de laatst bekende staat van waar deze branches op stonden op die servers.

=== Packfiles

Als je alle instructies in het voorbeeld van de hiervoorgaande sectie hebt gevolgd, zou je nu een test Git repository moeten hebben met 11 objecten — vier blobs, drie trees, drie commits en een tag:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Git comprimeert de inhoud van deze bestanden met zlib, en je slaat niet veel op, dus al deze bestanden nemen samen maar 925 bytes in beslag. Je zult wat lijviger inhoud aan de repository toevoegen om een interessante mogelijkheid van Git te demonstreren. Om het te demonstreren zullen we het repo.rb bestand van de Grit library toevoegen — dit is een broncode bestand van ongeveer 22K.

$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
$ git checkout master
$ git add repo.rb
$ git commit -m 'added repo.rb'
[master 484a592] added repo.rb
 3 files changed, 709 insertions(+), 2 deletions(-)
 delete mode 100644 bak/test.txt
 create mode 100644 repo.rb
 rewrite test.txt (100%)

Als je naar de resulterende tree kijkt, zie je de SHA-1 waarde die je repo.rb bestand heeft gekregen voor het nieuwe blob object:

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

Je kunt daarna git cat-file gebruiken om te zien hoe groot dat object is:

$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044

Wijzig nu dat bestand een beetje, en kijk wat er gebeurt:

$ echo '# testing' >> repo.rb
$ git commit -am 'modified repo a bit'
[master 2431da6] modified repo.rb a bit
 1 file changed, 1 insertion(+)

Kijk naar de tree die door die commit gemaakt is, en je zult iets interessants zien:

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

De blob is nu een andere blob, wat inhoudt dat ondanks dat je maar een enkele regel aan het eind van een bestand van 400 regels hebt toegevoegd, Git de nieuwe inhoud als een volledig nieuw object heeft opgeslagen.

$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054

Je hebt nu twee vrijwel identieke objecten van 22K op je schijf staan (elk gecomprimeerd tot ongeveer 7K). Zou het niet aardig zijn als Git een van deze volledig zou opslaan maar dan het tweede object alleen als de delta tussen deze en de eerste?

Laat dit nu ook het geval zijn. Het initiële formaat waar Git objecten op schijf bewaard wordt een “loose” (los, ruimzittend) object formaat genoemd. Echter, van tijd tot tijd pakt Git een aantal van deze bestanden in een enkele binair bestand “packfile” genaamd in met als doel ruimte te besparen en meer efficiënt te zijn. Git doet dit als je teveel losse bestanden hebt, als je het git gc commando handmatig aanroept, of als je naar een remote server pusht. Om te zien wat er gebeurt, kan je Git handmatig vragen om de objecten in te pakken door het git gc commando aan te roepen:

$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)

Als je in je objecten directory kijkt, zal je zien dat de meeste van je objecten zijn verdwenen en dat er een aantal andere bestanden zijn verschenen:

$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack

De objecten die zijn overgebleven zijn de blobs waar geen enkele commit nog naar wijst — in dit geval de blobs van het “what is up, doc?”-voorbeeld en het “test content”-voorbeeld die je eerder hebt gemaakt. Omdat je ze nooit aan enige commit hebt toegevoegd, worden ze beschouwd als bungelend (dangling) en worden ze niet in je nieuwe packfile ingepakt.

De andere bestanden zijn je nieuwe packfile en een index. Het packfile bestand is een enkel bestand met daarin de inhoud van al de objecten die zijn verwijderd van je bestandssysteem. De index is een bestand die de relatieve beginpunten bevat in die packfile zodat je snel naar een specifiek object kunt zoeken. Wat cool is, is dat ondanks dat de bestanden op schijf voordat je het gc commando aanriep samen ongeveer 15K groot waren, het nieuwe packfile bestand maar 7K is. Je hebt schijfruimte-gebruik tot de helft verminderd door je objecten in te pakken.

Hoe doet Git dit? Als Git objecten inpakt, kijkt het naar bestanden die vergelijkbare namen en grootte hebben, en slaat daarvan alleen de delta’s tussen de ene versie van het bestand en de andere op. Je kunt in het packfile bestand kijken en zien wat Git gedaan heeft om schijfruimte te besparen. Het git verify-pack binnenwerk commando stelt je in staat om te zien wat er ingepakt is:

$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag    130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree   136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree   36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree   136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree   6 17 1314 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree   8 19 1331 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree   71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob   10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob   9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob   22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   9 20 7262 1 \
  b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob   10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok

Hier verwijst de 033b4 blob die, als je het je nog kunt herinneren, de eerste versie van je repo.rb bestand is, naar de b042a blob, wat de tweede versie van het bestand was. De derde kolom in de uitvoer is de grootte van het object in het packfile bestand, dus je kunt zien dat b042a 22K van het bestand inneemt, aar dat 033b4 maar 9 bytes in beslag neemt. Wat ook interessant is, is dat de tweede versie van het bestaand degene is die intact opgeslagen is, terwijl de orginele versie is opgeslagen als een delta — de reden hiervoor is dat het waarschijnlijker is dat je snellere toegang nodig hebt naar de meest recente versie van het bestand.

Het echte leuke van dit is, is dat het op elk gewenste moment weer opnieuw kan worden ingepakt. Git zal van op gezette tijden je database automatisch opnieuw inpakken, altijd met het doel nog meer ruimte te besparen, maar je kunt ook handmatig opnieuw laten inpakken door git gc zelf aan te roepen.

=== De Refspec

In dit hele boek hebben we eenvoudige verbanden (mappings) gebruikt tussen remote branches naar lokale referenties, maar ze kunnen veel complexer zijn. Stel even dat je met de paar laatste secties hebt meegedaan en een kleine lokale Git repository gemaakt hebt, en nu een remote eraan zou willen toevoegen:

$ git remote add origin https://github.com/schacon/simplegit-progit

Dit commando voegt een sectie toe aan je .git/config bestand, waarbij de naam van de remote (origin) wordt opgegeven, de URL van de remote repository en de refspec die gebruikt moet worden voor het fetchen:

[remote "origin"]
	url = https://github.com/schacon/simplegit-progit
	fetch = +refs/heads/*:refs/remotes/origin/*

Het formaat van de refspec is, eerst, een optionele +, gevolgd door <src>:<dst>, waar `<src> het patroon is voor referenties aan de remote kant, en <dst> de plaats is waar deze referenties lokaal zullen worden getrackt. De + draagt Git op om de referenties bij te werken zelfs als het geen fast-forward is.

In het standaard geval dat automatisch wordt geschreven door een git remote add commando, zal Git alle referenties onder refs/heads op de server fetchen en ze lokaal naar refs/remotes/origin schrijven. Dus, als er een master-branch op de server is, kan je de log van deze branch op deze manieren benaderen:

$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master

Dit is allemaal gelijkwaardig, omdat Git elk van deze uitwerkt tot refs/remotes/origin/master.

Als je in plaats hiervan Git alleen de master-branch wilt laten pullen, en niet elke andere branch op de remote server, kan je de fetch regel wijzigen zodat het alleen naar die branch wijst:

fetch = +refs/heads/master:refs/remotes/origin/master

Dit is gewoon de standaard refspec voor git fetch naar die remote. Als je een eenmalige fetch wilt doen, kan je de refspec ook op de command regel opgeven. Om de master-branch te pullen van de remote naar een lokale origin/mymaster, kan je dit aanroepen

$ git fetch origin master:refs/remotes/origin/mymaster

Je kunt ook meerdere refspecs opgeven. Op de commando regel kan je meerdere branches als volgt pullen:

$ git fetch origin master:refs/remotes/origin/mymaster \
	 topic:refs/remotes/origin/topic
From git@github.com:schacon/simplegit
 ! [rejected]        master     -> origin/mymaster  (non fast forward)
 * [new branch]      topic      -> origin/topic

In dit geval wordt de pull van de master-branch geweigerd omdat het niet als een fast-forward referentie was opgegeven. Je kunt dat overschrijven door een + voor de refspec op te geven.

Je kunt ook meerdere respecs voor fetchen aangeven in je configuratie bestand. Als je altijd de master en experiment-branches wilt fetchen, voeg je twee regels toe:

[remote "origin"]
	url = https://github.com/schacon/simplegit-progit
	fetch = +refs/heads/master:refs/remotes/origin/master
	fetch = +refs/heads/experiment:refs/remotes/origin/experiment

Je kunt niet gedeeltelijke globs in het patroon opgeven, dus dit zou ongeldig zijn:

fetch = +refs/heads/qa*:refs/remotes/origin/qa*

Echter, je kunt naamsruimten (namespaces) of directories opgeven om zoiets voor elkaar te krijgen. Als je een QA team hebt die een reeks van branches pusht, en je wilt de master branch verkrijgen en alle branches van het QA team maar niets anders, kan je een configuratie instelling als deze gebruiken:

[remote "origin"]
	url = https://github.com/schacon/simplegit-progit
	fetch = +refs/heads/master:refs/remotes/origin/master
	fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*

Als je een complexe workflow proces hebt waarbij een QA-team branches pusht, waarbij ontwikkelaar branches pushen en integratie teams die pushen naar en samenwerken op remote branches, kan je ze op deze manier eenvoudig namespaces toewijzen.

==== Refspecs pushen

Het is leuk dat je namespaced referenties op deze manier kunt fetchen, maar hoe krijgt het QA-team om te beginnen hun branches in een qa/ namespace? Je krijgt dit voor elkaar door refspecs te gebruiken om te pushen.

Als het QA-team hun master-branch naar qa/master op de remote server wil pushen, kunnen ze dit aanroepen:

$ git push origin master:refs/heads/qa/master

Als ze willen dat Git dit elke keer automatisch doet als ze git push origin aanroepen, kunnen ze een push waarde aan hun configuratie bestand toevoegen:

[remote "origin"]
	url = https://github.com/schacon/simplegit-progit
	fetch = +refs/heads/*:refs/remotes/origin/*
	push = refs/heads/master:refs/heads/qa/master

Nogmaals, dit zal ervoor zorgen dat een git push origin de lokale master-branch standaard naar de remote qa/master-branch pusht.

==== References verwijderen

Je kunt de refspec ook gebruiken om referenties te verwijderen van de remote server door iets als dit aan te roepen:

$ git push origin :topic

Omdat de refspec <src>:<dst> is zegt dit, door het weglaten van het <src> gedeelte, eigenlijk dat de topic branch van de remote niets moet worden, waarmee het wordt verwijderd.

Of je kunt de nieuwere syntax gebruiken (beschikbaar vanaf Git v1.7.0):

$ git push origin --delete topic

=== Uitwisseling protocollen

Git kan op twee belangrijke manieren gegevens uitwisselen tussen twee repositories: het “domme” (dumb) protocol en het “slimme” (smart) protocol. In dit gedeelte zal de manier van werken van beide snel worden besproken.

==== Het domme protocol

ALs je een repository opzet die alleen gelezen hoeft te worden via HTTP, is het domme protocol het meest waarschijnlijke zijn die zal worden gebruikt. Dit protocol wordt “dom” genoemd omdat het geen Git-specifieke code nodig heeft aan de kant van de server tijdens het uitwisselingsproces; het fetch proces is een reeks van HTTP GET requests, waar het werkstation aannames mag doen over de inrichting van de Git repository op de server.

Note

Het domme protocol wordt tegenwoordig nog maar sporadisch gebruikt. Het is moeilijk te beveiligen of af te schermen, dus de meeste Git hosts (zowel in de cloud als op locatie) zullen het weigeren te gebruiken. Het wordt over het algemeen aangeraden om het slimme protocol te gebruiken, die we iets verderop zullen bespreken.

Laten we het http-fetch proces voor de simplegit library eens volgen:

$ git clone http://server/simplegit-progit.git

Het eerste wat dit commando doet is het info/refs bestand pullen. Dit bestand wordt geschreven door het update-server-info commando, wat de reden is waarom je dit als post-receive hook moet activeren om het uitwisselen via HTTP goed te laten werken:

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

Nu heb je een lijst met de remote referenties en SHA-1 waarden. Vervolgens ga je op zoek naar de HEAD referentie zodat je weet wat je moet uitchecken als je klaar bent:

=> GET HEAD
ref: refs/heads/master

Je moet de master-branch uitchecken als je het proces hebt voltooid. Op dit punt ben je gereed om het proces te doorlopen. Omdat je vertrekpunt het ca82a6 commit object is die je in het info/refs bestand zag, begin je met die te fetchen:

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

Je krijgt een object terug - dat object is in het losse (loose) formaat op de server, en je hebt het met een statische HTTP GET request gefetcht. Je kunt het nu met zlib-uncompress uitpakken, de header ervan afhalen, en naar de commit inhoud kijken:

$ git cat-file -p ca82a6dff817ec66f44342007202690a93763949
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700

changed the version number

Vervolgens heb je nog twee objecten op te halen - cfda3b, wat de boom met inhoud is waar de commit die we zojuist opgehaald hebben naar wijst, en 085bb3, wat de ouder-commit is:

=> GET objects/08/5bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
(179 bytes of data)

En daarmee krijg je je volgende commit object. Haal het boom-object op:

=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)

Oeps, het ziet er naar uit dat het boom-object niet in het losse formaat op de server is, dus je krijgt een 404 antwoord. Hier zijn een aantal mogelijke oorzaken voor - het object kan in een andere repository zitten, of het zou in een packfile in deze repository kunnen zitten. Git controleert eerst of er alternatieven zijn opgegeven:

=> GET objects/info/http-alternates
(empty file)

Dit geeft een lijst met alternatieve URLs terug, Git controleert daar op losse bestanden en packfiles - dit is een aardig mechanisme voor projecten die forks zijn van elkaar zijn om objecten te delen op schijf. Echter, omdat er in dit geval geen alternatieven worden gegeven, moet het object in een packfile zitten. Om te zien welke packfiles er beschikbaar zijn op deze server, moet je het objects/info/packs bestand te pakken krijgen, waar een opsomming hiervan in staat (wat ook door de update-server-info wordt gegenereerd):

=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

Er is maar één packfile op de server, dus jouw object moet daar wel in zitten, maar je gaat het index bestand toch controleren om er zeker van te zijn. Dit is ook handig als je meerdere packfiles op de server hebt, omdat je dan kan zien welk packfile het object wat je nodig hebt bevat:

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)

Nu je de packfile index hebt, kan je kijken of jouw object daar in zit - omdat de index de SHA-1 waarden van de objecten bevat die in de packfile zitten en de relatieve afstand (offset) naar deze objecten. Jouw object is er, dus ga je verder en haalt de hele packfile op:

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack
(13k of binary data)

Je hebt je boom object, dus je gaat verder met je commits af te lopen. Die zitten ook allemaal in de packfile die je zojuist hebt gedownload, dus je hoeft geen verzoeken meer te doen naar de server. Git checkt een werk-kopie van de master-branch uit waarnaar werd verwezen door de HEAD referentie die je aan het begin hebt gedownload.

==== Het slimme protocol

Het domme protocol is eenvoudig, maar een beetje inefficiënt, het kan geen gegevens aan die van het werkstation naar de server moet worden geschreven. Het slimme protocol is een meer gebruikelijke methode van gegevens uit te wisselen, maar het heeft een proces aan de remote kant nodig die op de hoogte is van Git - het moet lokale gegevens kunnen lezen, uitvinden wat het werkstation al heeft en nog nodig heeft, en een op maat gemaakte packfile hiervoor maken. Er zijn twee groepen van processen voor het uitwisselen van gegevens: een paar voor het uploaden van gegevens en een paar voor het downloaden van gegevens.

===== Het uploaden van gegevens

Om gegevens te uploaden naar een remote proces, gebruikt Git de send-pack en receive-pack processen. Het send-pack proces loopt op het werkstation en maakt verbinding met een receive-pack proces aan de remote kant.

====== SSH

Bijvoorbeeld, stel dat je git push origin master in jouw project aanroept en origin is als een URL gedefiniëerd die het SSH protocol gebruikt. Git start het send-pack proces op, die over SSH een verbinding initiëert naar je server. Het probeert een commando op de remote server aan te roepen via een SSH call die er ongeveer zo uitziet:

$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master□report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000

Het git-receive-pack commando antwoordt meteen met een regel voor elke referentie die het op dit moment heeft - in dit geval alleen de master-branch en zijn SHA-1. De eerste regel heeft ook een lijst met de mogelijkheden van de server (hier, report-status, delete-refs, en een aantal andere, inclusief de identificatie van de client).

Elke regel begint met een hex waarde van 4 tekens die aangeeft hoe lang de rest van de regel is. Je eerste regel begint met 00a5, wat hexadecimaal is voor 165, wat weer inhoudt dat er nog 165 bytes tot die regel behoren. De volgende regel is 0000, wat betekent dat de server klaar is met het uitlijsten van de referenties.

Nu dat het de status van de server kent, bepaalt je send-pack proces welke commits het heeft die de server niet heeft. Voor elke referentie die deze push gaat bijwerken, vertelt het send-pack proces het receive-pack proces deze informatie. Bijvoorbeeld, als je de master-branch aan het bijwerken bent en een experiment-branch toevoegt, zal het antwoord van send-pack er ongeveer zo uitzien:

0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
	refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
	refs/heads/experiment
0000

Git stuurt een regel voor elke referentie die je bijwerkt met de lengte van de regel, de oude SHA-1, de nieuwe SHA-1 en de referentie die wordt geüpdate. De eerste regel bevat ook de mogelijkheden van het werkstation. De SHA-1 waarde van allemaal '0’en houdt in dat er niets daarvoor was - omdat je de referentie van het experiment aan het toevoegen bent. Als je een referentie aan het verwijderen zou zijn, zou je het omgekeerde zien: alle '0’en aan de rechterkant.

Daarna stuurt het werkstation een packfile van alle objecten die de server nog niet heeft. Als laatste antwoordt de server met een indicatie van succes (of falen):

000eunpack ok

====== HTTP(S)

Dit proces is voor het grootste gedeelte hetzelfde als over HTTP, al is de handshaking iets anders. De verbinding wordt begonnen met dit verzoek:

=> GET http://server/simplegit-progit.git/info/refs?service=git-receive-pack
001f# service=git-receive-pack
00ab6c5f0e45abd7832bf23074a333f739977c9e8188 refs/heads/master□report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1~vmg-bitmaps-bugaloo-608-g116744e
0000

Dat is het einde van de eerste werkstation-server uitwisseling. Het werkstation stuurt daarna een ander verzoek, dit keer een POST, met de gegevens die worden geleverd door send-pack.

=> POST http://server/simplegit-progit.git/git-receive-pack

Het POST verzoek bevat de uitvoer van send-pack en de packfile als zijn bagage (payload). De server geeft daarna aan of het succesvol of niet heeft verwerkt in zijn HTTP antwoord.

===== Gegevens downloaden

Als je gegevens download, zijn de fetch-pack en 'upload-pack` processen erbij betrokken. Het werkstation begint een fetch-pack proces die verbinding maakt met een upload-pack proces op de remote kant om te onderhandelen welke gegevens er naar het werkstation zullen worden gestuurd.

====== SSH

Als je de fetch via SSH doet, zal fetch-pack ongeveer dit aanroepen:

$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"

Nadat fetch-pack verbinding heeft gemaakt, stuurt upload-pack zoiets als dit terug:

00dfca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000

Dit lijkt op waar receive-pack mee antwoordt, maar de mogelijkheden zijn anders. Daarenboven stuurt het terug waar HEAD op dit moment naar wijst (symref=HEAD:refs/heads/master), zodat het werkstation weet wat het uit moet checken als dit een kloon is.

Op dit punt kijkt het fetch-pack-proces naar welke objecten het heeft en antwoordt met de objecten dat het nodig heeft door “want” te sturen en dan de SHA-1 die het wil hebben. Het stuurt alle objecten die het al heeft met “have” en daarna de SHA-1. En aan het einde van deze lijst, schrijft het “done” om het upload-pack proces aan te zetten tot het beginnen met sturen van de packfile met de gegevens die het nodig heeft:

003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta
0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
0009done
0000

====== HTTP(S)

De handshake voor een fetch operatie vergt twee HTTP verzoeken. Het eerste is een GET naar hetzelfde adres als gebruikt in het domme protocol:

=> GET $GIT_URL/info/refs?service=git-upload-pack
001e# service=git-upload-pack
00e7ca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed no-done symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fca82a6dff817ec66f44342007202690a93763949 refs/heads/master
0000

Dit lijkt erg op het aanroepen van git-upload-pack over een SSH verbinding, maar de tweede uitwisseling wordt uitgevoerd als een separaat verzoek:

=> POST $GIT_URL/git-upload-pack HTTP/1.0
0032want 0a53e9ddeaddad63ad106860237bbf53411d11a7
0032have 441b40d833fdfa93eb2908e52742248faf0ee993
0000

Wederom, dit volgt hetzelfde patroon als hierboven. Het antwoord op dit verzoek geeft succes of falen aan, en bevat de packfile.

==== Protocolen samenvatting

Deze paragraaf bevat een zeer basale overzicht van de uitwisselings-protocollen. Het protocol omvat vele andere mogelijkheden, zoals multi_ack of side-band mogelijkheden, maar de behandeling hiervan valt buiten het bestek van dit boek. We hebben geprobeerd je een idee te geven van het globale over-en-weer tussen werkstation en server; als je hierover meer wilt weten, dan kan je bijvoorbeeld een kijkje nemen in de Git broncode.

=== Onderhoud en gegevensherstel

Bij tijd en wijle, moet je wat opschonen - een repository iets compacter maken, een geïmporteerde repository opschonen of verloren gegane werk herstellen. Deze paragraaf zal een aantal van deze scenarios behandelen.

==== Onderhoud

Op gezette tijden roept Git automatisch een commando genaamd “auto gc” aan. Meestal zal dit commando niets doen. Echter, als er teveel losse objecten zijn (objecten die niet in een packfile zitten) of teveel packfiles, roept Git een volwaardige git gc commando aan. Het “gc” staat voor vuil ophalen (garbage collect), en het commando voert een aantal dingen uit: het verzamelt alle losse objecten en zet ze in packfiles, het consolideert packfiles in een grote packfile, en het verwijdert objecten die onbereikbaar zijn vanaf enig commit en een aantal maanden oud zijn.

Je kunt auto gc ook handmatig aanroepen als volgt:

$ git gc --auto

Nogmaals, over het algemeen doet dit niets. Je moet ongeveer 7.000 losse objecten hebben of meer dan 50 packfiles om Git een echte gc commando te laten aanroepen. Je kunt deze grenswaarden aanpassen met respectievelijk het gc.auto en gc.autopacklimit configuratie waarden.

Het andere wat gc zal doen is je referenties in een enkel bestand stoppen. Stel dat je repository de volgende branches en tags bevat:

$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1

Als je git gc aanroept, zal je deze bestanden niet langer in de refs directory hebben staan. Git verplaatst ze allemaal in het kader van efficiëntie naar een bestand met de naam .git/packed-refs die er als volgt uit ziet:

$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9

Als je een referentie bijwerkt, werkt Git dit bestand niet bij, maar schrijft in plaats daarvan een nieuw bestand naar refs/heads. Om de juiste SHA-1 voor een gegeven refentie te pakken te krijgen, zoekt Git de referentie eerst in de refs directory en daarna het packed-refs bestand als achtervang. Dus, als je een referentie niet kunt vinden in de refs directory, staat deze waarschijnlijk in je packed-refs bestand.

Let even op de laatste regel van het bestand, die begint met een ^. Dit geeft aan dat de tag directory erboven een geannoteerde tag is, en deze regel de commit is waar de geannoteerde tag naar verwijst.

==== Gegevensherstel

Op een bepaald punt in je Git-reis, kan je per ongeluk een commit kwijt raken. Meestal gebeurt dit omdat je een branch force-delete waar werk op zat, en je komt erachter dat je die branch toch nog nodig had; of je hebt een branch ge-hard-reset, en daarmee commits laat vallen waar je toch nog iets van wilde gebruiken. Aangenomen dat dit gebeurd is, hoe kan je die commits nog terughalen?

Hier is een voorbeeld die de master branch hard-reset in je test repository naar een oudere commit en daarna de verloren commits herstelt. Laten we eerst eens zien hoe je repository er op dit moment uitziet:

$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Nu gaan we de master-branch terugzetten naar de middelste commit:

$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Je bent effectief de bovenste twee commits kwijtgeraakt - je hebt geen branch waar deze commits vanaf bereikt kunnen worden. Je moet de SHA-1 van de laatste commit zien te vinden en dan een branch toevoegen die daarnaar wijst. De truuk is het vinden van de SHA-1 van die laatste commit - we mogen aannemen dat je deze niet uit je hoofd hebt geleerd, toch?

Vaak is de snelste manier om een instrument genaamd git reflog te gebruiken. Als je aan het werk bent, houdt Git stilletjes bij wat je HEAD was elke keer als je het verandert. Elke keer als je commit, of branches wijzigt, wordt de reflog bijgewerkt. De reflog wordt ook bijgewerkt door het git update-ref commando, wat nog een reden is om dit te gebruiken in plaats van alleen de SHA-1 waarden naar je ref bestanden te schrijven, zoals we besproken hebben in [_git_refs]. Je kunt zien waar je op enig moment geweest bent door git reflog aan te roepen:

$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rb

Hier kunnen we de twee commits zien die we uitgechecked hebben gehad, maar hier is ook niet veel informatie te zien. Om dezelfde informatie op een veel nuttiger manier te zien, kunnen we git log -g aanroepen, die je een normale log uitvoer voor je reflog laat zien.

$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:22:37 2009 -0700

		third commit

commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

       modified repo.rb a bit

Het lijkt erop dat de laatste commit degene is die je kwijt was geraakt, dus je kunt deze terughalen door een nieuwe branch te maken op die commit. Bijvoorbeeld, je kunt een branch genaamd recover-branch beginnen op die commit (ab1afef):

$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

Toppie - nu heb je een branch genaamd recover-branch die staat waar je master-branch heeft gestaan, en de eerste twee commits worden weer bereikbaar. Okay, nu stel dat je verlies om wat voor reden dan ook niet meer in de reflog zichtbaar is - je kunt dat naspelen door recover-branch weg te halen en de reflog weg te gooien. Nu kan je op geen enkele manier meer bij die eerste twee commits:

$ git branch -D recover-branch
$ rm -Rf .git/logs/

Omdat de reflog gegevens worden bewaard in de .git/logs/ directory, heb je effectief geen reflog. Hoe kan je nu die commit herstellen? Een manier is om het git fsck instrument te gebuiken die je database op integriteit controleert. Als je het aanroept met de --full optie, zal het je alle objecten laten zien waar geen enkele andere object naar verwijst:

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

In dit geval kan je de ontbrekende commit zien achter de tekst “dangling commit”. Je kunt het op dezelfde manier herstellen, door een branch toe te voegen die wijst naar die SHA-1.

==== Objecten verwijderen

Er zijn ontzettend veel goede dingen met Git, maar een eigenschap die problemen kan veroorzaken is het feit dat een git clone de hele geschiedenis van het project download, inclusief elke versie van elk bestand. Dit is prima als het hele spul broncode is, omdat Git optimaal is ingericht om die gegevens efficiënt te comprimeren. Echter, als iemand op enig moment in de geschiedenis van je project een enorm groot bestand heeft toegevoegd, zal elke kloon voor altijd gedwongen zijn om dat grote bestand te downloaden, zelfs als het in de volgende commit uit het project zou zijn verwijderd. Omdat het vanuit de historie bereikt kan worden, zal het altijd aanwezig zijn.

Dit kan een groot probleem zijn als je Subversion of Perforce repositories naar Git aan het converteren bent. Omdat je in deze systemen niet de hele historie downloadt, heeft dit soort toevoegingen veel minder gevolgen. Als je een import vanuit een ander systeem gedaan hebt, of om een andere reden vindt dat je repository veel groter is dan het zou moeten zijn, volgt hier een manier om uit te vinden hoe je grote objecten kunt vinden en verwijderen.

Wees gewaarschuwd: deze techniek is destructief voor je commit historie. Het herschrijft elke commit object sinds de eerste boom die je moet wijzigen om een referentie van een groot bestand te verwijderen. Als je dit direct na een import doet, voordat iemand is begonnen om werk op de commit te baseren zit je nog goed - anders zal je alle bijdragers moeten vertellen dat ze hun werk moeten rebasen op je nieuwe commits.

Om dit te demonstreren, ga je een groot bestand in je test repository toevoegen, deze in de volgende commit verwijderen, het opzoeken en het definitief uit de repository verwijderen. Eerst: voeg een groot object toe aan je historie:

$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
[master 7b30847] add git tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 git.tgz

Oeps - je wilde niet een enorme tarball aan je project toevoegen. Laten we 'm maar snel weggooien:

$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'
[master dadf725] oops - removed large tarball
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 git.tgz

Nu ga je gc aanroepen op je database en kijken hoeveel ruimte je nu gebruikt:

$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)

Je kunt het count-objects commando gebruiken om snel te zien hoeveel ruimte je gebruikt:

$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0

De size-pack regel is de grootte van je packfiles in kilobytes, dus je gebruikt bijna 5MB. Voor de laatste commit gebruikte je iets van 2K - het is duidelijk, het verwijderen van het bestand van de vorige commit heeft het niet uit je historie verwijderd. Elke keer als iemand deze repository kloont, zullen ze alle 5MB moeten klonen alleen om dit kleine project te pakken te krijgen, omdat je per ongeluk een groot bestand hebt toegevoegd. Laten we er vanaf komen.

Eerst zal je het moeten vinden. In dit geval, weet je al welk bestand het is. Maar stel dat je het niet zou weten; hoe zou je uitvinden welk bestand of bestanden er zoveel ruimte in beslag nemen? Als je git gc aanroept, komen alle bestanden in een packfile terecht; je kunt de grote objecten vinden door een ander binnenwerk commando git verify-pack aan te roepen en de uitvoer te sorteren op het derde veld in de uitvoer, wat de bestandsgrootte is. Je kunt het ook door het tail commando pipen, omdat je alleen geinteresseerd bent in de laatste paar grootste bestanden:

$ git verify-pack -v .git/objects/pack/pack-29...69.idx \
  | sort -k 3 -n \
  | tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob   4975916 4976258 1438

Het grote object staat onderaan: 5MB. Om uit te vinden welk bestand dit is, ga je het rev-list commando gebruiken, die je al eventjes gebruikt hebt in [_enforcing_commit_message_format]. Als je --objects doorgeeft aan rev-list, laat het alle SHA-1s zien van commits en ook de blob SHA-1s met het bestandspad die ermee verbonden is. Je kunt dit gebruiken om de naam van jouw blob te vinden:

$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz

Nu moet je dit bestand uit alle bomen in je verleden verwijderen. Je kunt eenvoudig zien welke commits dit bestand hebben gewijzigd:

$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball

Je moet alle commits herschrijven die stroomafwaarts van 7b30847 liggen om dit bestand volledig uit je Git historie te verwijderen. Om dit te doen, gebruik je filter-branch, die je gebruikt hebt in Geschiedenis herschrijven:

$ git filter-branch --index-filter \
  'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten

De --index-filter optie is gelijk aan de --tree-filter optie zoals gebruikt in Geschiedenis herschrijven, behalve dat in plaats van een commando door te geven die bestanden wijzigt die op schijf zijn uitgecheckt, je elke keer het staging gebied of index wijzigt.

In plaats van een specifiek bestand te verwijderen met iets als rm file, moet je het verwijderen met git rm --cached - je moet het van de index verwijderen, niet van schijf. De reden hierachter is snelheid - omdat Git niet elke revisie hoeft uit te checken naar schijf voordat het je filter aanroept kan het proces veel, veel sneller werken. Je kunt dezelfde resultaat met --tree-filter bereiken als je wilt. De --ignore-unmatch optie bij git rm vertelt het geen fout te genereren als het patroon die je probeert te vinden niet aanwezig is. Als laatste, vraag je filter-branch om je historie te alleen herschrijven vanaf de 7b30847 en later, omdat je weet dat dit de plaats is waar het probleem begon. Anders zou het vanaf het begin starten en onnodig langer zou duren.

Je historie bevat niet langer meer een referentie naar dat bestand. Echter, je reflog en een nieuwe set van refs die Git toegevoegd heeft toen je het filter-branch gebruikte bestaan nog steeds onder .git/refs/original nog steeds wel, dus je zult deze moeten verwijderen en dan de database opnieuw inpakken. Je moet afraken van alles wat maar een verwijzing heeft naar die oude commits voordat je opnieuw inpakt.

$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)

Laten we eens kijken hoeveel ruimte je gewonnen hebt.

$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0

De grootte van de ingepakte repository is gekrompen naar 8K, wat veel beter is dan 5MB. Je kunt aan de waarde van de grootte zien dat het grote object nog steeds in je losse objecten zit, dus het is nog niet weg; maar het wordt niet meer uitgewisseld met een push of een toekomstige kloon, en daar gaat het uiteindelijk om. Als je het echt wilt, zou je het object volledig kunnen verwijderen door git prune aan te roepen met de --expire optie:

$ git prune --expire now
$ git count-objects -v
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0

=== Omgevingsvariabelen

Git draait altijd in een bash shell, en gebruikt een aantal shell omgevingsvariabelen om te bepalen hoe het zich moet gedragen. Af en toe is het handig om te weten welke deze zijn, en hoe ze kunnen worden gebruikt om Git zich te laten gedragen op de manier zoals jij dat wilt. Dit is geen uitputtende lijst van alle omgevingsvariabelen waar Git naar kijkt, maar we zullen de meest nuttige behandelen.

==== Globaal gedrag

Een bepaald deel van het algemene gedrag van Git als computer programma is afhankelijk van omgevingsvariabelen.

GIT_EXEC_PATH bepaalt waar Git zijn sub-programma’s zoekt (zoals git-commit, git-diff en andere). Je kunt de huidige waarde zien door git --exec-path aan te roepen.

HOME wordt over het algemeen niet beschouwd als aanpasbaar (te veel andere zaken zijn hiervan afhankelijk), maar dit is waar Git op zoek gaat naar het globale configuratie bestand. Als je een echte overdraagbare Git installatie wilt, compleet met globale configuratie, kan je HOME overschrijven in het shell profiel van de draagbare Git.

PREFIX is hiermee vergelijkbaar, maar voor de systeem-brede configuratie. Git kijkt naar dit bestand op $PREFIX/etc/gitconfig.

GIT_CONFIG_NOSYSTEM, indien gezet, schakelt dit het gebruik van het systeem-brede configuratie bestand uit. Dit kan nuttig zijn als je systeem configuratie je commando’s in de weg zit, maar je hebt hier geen toegang toe om het te wijzigen of te verwijderen.

GIT_PAGER bepaalt welk programma er gebruikt wordt om uitvoer van meerdere pagina’s op de commando regel te laten zien. Als deze waarde niet gezet is, wordt PAGER gebruikt als achtervang.

GIT_EDITOR is de editor die Git zal aanroepen als de gebruiker tekst moet wijzigen (een commit bericht, bijvoorbeeld). Als deze waarde niet is gezet, wordt EDITOR gebruikt.

==== Locaties van de repository

Git gebruikt een aantal omgevingsvariabelen om te bepalen hoe het met de huidige repository samenwerkt.

GIT_DIR is de locatie van de .git folder. Als dit niet is opgegeven, loopt Git de directory structuur omhoog tot het op ~ of / uitkomt, bij elke stap opzoek naar een .git directory.

GIT_CEILING_DIRECTORIES bepaalt het gedrag bij het zoeken naar een .git directory. Als je directories in gaat die langzaam zijn (zoals die op een tape drive, of over een langzame netwerkverbinding), zou je ervoor kunnen kiezen om Git te eerder laten stoppen met proberen dan het anders zou doen, zeker als Git wordt aangeroepen als je shell prompt wordt opgebouwd.

GIT_WORK_TREE is de locatie van de root van de werkdirectory voor een non-bare repository. Als --git-dir of GIT-DIR wordt gespecificeerd maar geen van --work-tree, GIT_WORK_TREE of core.worktree is gegeven wordt de huidige werk-directory beschouwd als het hoogste niveau van je werk-tree.

GIT_INDEX_FILE is het pad naar het index bestand (alleen bij non-bare repositories).

GIT_OBJECT_DIRECTORY kan worden gebruikt om de locatie van de directory aan te geven die normaalgesproken onder .git/objects te vinden is.

GIT_ALTERNATE_OBJECT_DIRECTORIES is een met dubbele punt gescheiden lijst (geformatteerd als /dir/een:/dir/twee:…) die vertelt waar Git moet zoeken naar objecten als ze niet in GIT_OBJECT_DIRECTORY te vinden zijn. Als je toevallig veel projecten hebt met grote bestanden die precies dezelfde inhoud hebben, kan dit worden gebruiktom te voorkomen dat er teveel kopieën hiervan worden opgeslagen.

==== Pathspecs

Een “pathspec” refereert aan hoe je paden opgeeft bij zaken in Git, inclusief het gebruik van wildcards. Deze worden gebruikt in het .gitignore bestand, maar ook op de commando-regel (git add *.c).

GIT_GLOB_PATHSPECS en GIT_NOGLOB_PATHSPECS bepalen het standaard gedrag van wildcards in pathspecs. Als GIT_GLOB_PATHSPECS de waarde 1 heeft, gedragen wildcard karakters als wildcards (wat het standaard gedrag is); als GIT_NOGLOB_PATHSPECS de waarde 1 heeft, passen wildcard karakters alleen op zichzelf, wat inhoudt dat iets als *c alleen een bestand met de naam “*.c” zou passen, in plaats van elk bestand waarvan de naam eindigt op .c. Je kunt dit gedrag in specifieke gevallen overschrijven door de pathspecs te laten beginnen met :(glob) of :(literal), als in :(glob)*.c.

GIT_LITERAL_PATHSPECS schakelt beide bovenstaande gedragingen uit; geen enkele wildcard karakter zal werken, en de overschrijvende prefixes worden ook uitgeschakeld.

GIT_ICASE_PATHSPECS zet alle pathspecs in voor ongevoelige werkwijze voor hoofdletters en kleine letters.

==== Committen

De uiteindelijke aanmaak van een Git commit object wordt normaalgesproken gedaan door git-commit-tree, die de omgevingsvariabelen gebruikt als zijn primaire bron van informatie, waarbij wordt teruggevallen op configuratiewaarden alleen als deze niet aanwezig zijn.

GIT_AUTHOR_NAME is de mens-leesbare naam in het “author” veld.

GIT_AUTHOR_EMAIL is de email voor het “author” veld.

GIT_AUTHOR_DATE is de datum/tijd waarde die voor het “author” veld wordt gebruikt.

GIT_COMMITTER_NAME bepaalt de mens-leesbare naam voor het “committer” veld.

GIT_COMMITTER_EMAIL is het email adres voor het “committer” veld.

GIT_COMMITTER_DATE wordt gebruikt voor de datum/tijd waarde in het “committer” veld.

EMAIL is de terugvalwaarde voor het email adres in geval de configuratie waarde in user.email niet is ingevuld. Als deze niet is ingevuld, valt Git terug op de systeemgebruiker en host naam.

==== Network communicatie

Git gebruikt de curl library om netwerk operaties over HTTP te doen, dus GIT_CURL_VERBOSE vertelt Git om alle berichten uit te sturen die worden gegenereerd door deze library. Dit is gelijk aan het intypen van curl -v op de commandoregel.

GIT_SSL_NO_VERIFY vertelt Git om de SSL certificaten niet te controleren. Dit kan soms nodig zijn als je een door jezelf ondertekende certificaat gebruikt om Git repositories te bedienen via HTTPS, of je bent bezig met het opzetten van een Git server maar je hebt nog geen volwaardig certificaat geïnstalleerd.

Als de gegevenssnelheid van een HTTP operatie lager is dan GIT_HTTP_LOW_SPEED_LIMIT bytes per seconde voor langer dan GIT_HTTP_LOW_SPEED_TIME seconden, zal Git die operatie afbreken. Deze waarden overschrijven de http.lowSpeedLimit en http.lowSpeedTime configuratie waarden.

GIT_HTTP_USER_AGENT zet de user-agent tekenreeks die Git gebruikt wanneer het communiceert via HTTP. De standaard waarde is iets als git/2.0.0.

==== Diffen en Mergen

GIT_DIFF_OPTS is een beetje een misnomer. De enige geldige waarden zijn -u<n> of --unified=<n>, wat het aantal contextregels bepaalt die worden getoond met een git diff commando.

GIT_EXTERNAL_DIFF wordt gebruikt als een overschrijving voor de diff.external configuratie waarde. Als het een waarde heeft, zal Git dit programma gebruiken als git diff wordt aangeroepen.

GIT_DIFF_PATH_COUNTER en GIT_DIFF_PATH_TOTAL zijn nuttig binnen het programma die wordt gespecificeerd door GIT_EXTERNAL_DIFF of diff.external. Het eerste geeft aan welk bestand in een reeks van bestanden wordt gedifft (beginnend met 1), en de laatste is het totaal aantal bestanden in de reeks.

GIT_MERGE_VERBOSITY bepaalt de uitvoer voor de recursieve merge strategie. De toegestane waarden zijn als volgt:

  • 0 voert niets uit, behalve mogelijk een enkele foutboodschap.

  • 1 laat alleen conflicten zien.

  • 2 laat ook bestandswijzigingen zien.

  • 3 geeft uitvoer als bestanden worden overgeslagen omdat ze niet zijn gewijzigd.

  • 4 laat alle paden zien als ze worden verwerkt.

  • 5 en hoger laten gedetailleerd debug informatie zien.

De standaardwaarde is 2.

==== Debuggen

Wil je echt zien waar Git zoal mee bezig is? Git heeft een redelijk volledige set van traces ingebouwd, en alles wat je hoeft te doen is ze aan te zetten. De mogelijke waarden van deze variabelen zijn als volgt:

  • “true”, “1”, of “2” - de trace categorie wordt naar stderr geschreven.

  • Een absoluut pad beginnend met / - de trace uitvoer wordt naar dat bestand geschreven.

GIT_TRACE bepaalt traces over het algemeen die niet in een andere specifieke categorie valt. Dit is inclusief de expansie van aliassen, en het delegeren naar andere sub-programma’s.

$ GIT_TRACE=true git lga
20:12:49.877982 git.c:554               trace: exec: 'git-lga'
20:12:49.878369 run-command.c:341       trace: run_command: 'git-lga'
20:12:49.879529 git.c:282               trace: alias expansion: lga => 'log' '--graph' '--pretty=oneline' '--abbrev-commit' '--decorate' '--all'
20:12:49.879885 git.c:349               trace: built-in: git 'log' '--graph' '--pretty=oneline' '--abbrev-commit' '--decorate' '--all'
20:12:49.899217 run-command.c:341       trace: run_command: 'less'
20:12:49.899675 run-command.c:192       trace: exec: 'less'

GIT_TRACE_PACK_ACCESS bepaalt het traceren van packfile toegang. Het eerste veld is de packfile die wordt benaderd, het tweede is de relatieve afstand (offset) binnen dat bestand:

$ GIT_TRACE_PACK_ACCESS=true git status
20:10:12.081397 sha1_file.c:2088        .git/objects/pack/pack-c3fa...291e.pack 12
20:10:12.081886 sha1_file.c:2088        .git/objects/pack/pack-c3fa...291e.pack 34662
20:10:12.082115 sha1_file.c:2088        .git/objects/pack/pack-c3fa...291e.pack 35175
# […]
20:10:12.087398 sha1_file.c:2088        .git/objects/pack/pack-e80e...e3d2.pack 56914983
20:10:12.087419 sha1_file.c:2088        .git/objects/pack/pack-e80e...e3d2.pack 14303666
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

GIT_TRACE_PACKET zet traceren op pakket-niveau voor netwerk operaties aan.

$ GIT_TRACE_PACKET=true git ls-remote origin
20:15:14.867043 pkt-line.c:46           packet:          git< # service=git-upload-pack
20:15:14.867071 pkt-line.c:46           packet:          git< 0000
20:15:14.867079 pkt-line.c:46           packet:          git< 97b8860c071898d9e162678ea1035a8ced2f8b1f HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/2.0.4
20:15:14.867088 pkt-line.c:46           packet:          git< 0f20ae29889d61f2e93ae00fd34f1cdb53285702 refs/heads/ab/add-interactive-show-diff-func-name
20:15:14.867094 pkt-line.c:46           packet:          git< 36dc827bc9d17f80ed4f326de21247a5d1341fbc refs/heads/ah/doc-gitk-config
# […]

GIT_TRACE_PERFORMANCE bepaalt het loggen van performance gegevens aan. De uitvoer laat zien hoe lang elk specifieke git aanroep duurt.

$ GIT_TRACE_PERFORMANCE=true git gc
20:18:19.499676 trace.c:414             performance: 0.374835000 s: git command: 'git' 'pack-refs' '--all' '--prune'
20:18:19.845585 trace.c:414             performance: 0.343020000 s: git command: 'git' 'reflog' 'expire' '--all'
Counting objects: 170994, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (43413/43413), done.
Writing objects: 100% (170994/170994), done.
Total 170994 (delta 126176), reused 170524 (delta 125706)
20:18:23.567927 trace.c:414             performance: 3.715349000 s: git command: 'git' 'pack-objects' '--keep-true-parents' '--honor-pack-keep' '--non-empty' '--all' '--reflog' '--unpack-unreachable=2.weeks.ago' '--local' '--delta-base-offset' '.git/objects/pack/.tmp-49190-pack'
20:18:23.584728 trace.c:414             performance: 0.000910000 s: git command: 'git' 'prune-packed'
20:18:23.605218 trace.c:414             performance: 0.017972000 s: git command: 'git' 'update-server-info'
20:18:23.606342 trace.c:414             performance: 3.756312000 s: git command: 'git' 'repack' '-d' '-l' '-A' '--unpack-unreachable=2.weeks.ago'
Checking connectivity: 170994, done.
20:18:25.225424 trace.c:414             performance: 1.616423000 s: git command: 'git' 'prune' '--expire' '2.weeks.ago'
20:18:25.232403 trace.c:414             performance: 0.001051000 s: git command: 'git' 'rerere' 'gc'
20:18:25.233159 trace.c:414             performance: 6.112217000 s: git command: 'git' 'gc'

GIT_TRACE_SETUP laat informatie zien over wat Git ontdekt over de repository en de omgeving waar het mee samenwerkt.

$ GIT_TRACE_SETUP=true git status
20:19:47.086765 trace.c:315             setup: git_dir: .git
20:19:47.087184 trace.c:316             setup: worktree: /Users/ben/src/git
20:19:47.087191 trace.c:317             setup: cwd: /Users/ben/src/git
20:19:47.087194 trace.c:318             setup: prefix: (null)
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean

==== Diversen

GIT_SSH, indien meegegeven, is een programma dat wordt aangeroepen inplaats van ssh als Git probeert verbinding te leggen naar een SSH host. Het wordt aangeroepen als $GIT_SSH [gebruikersnaam@]host [-p <poort>] <commando>. Merk op dat dit niet de makkelijkste manier is om de manier waarop ssh wordt aangeroepen aan te passen; het zal extra commando-regel parameters niet ondersteunen, dus je zult een wrapper script moeten schrijven en GIT_SSH hiernaar moeten laten wijzen. Het is waarschijnlijk makkelijker om gewoon het ~/.ssh/config bestand hiervoor te gebruiken.

GIT_ASKPASS overschrijft de waarde van de core.askpass configuratie waarde. Dit is het programma dat wordt aangeroepen elke keer als Git de gebruiker moet vragen om de aanloggegevens, die een tekst prompt mag verwachten als een commando-regel argument, en het antwoord moet teruggeven op stdout. (Zie [_credential_caching] voor meer over dit subsysteem.)

GIT_NAMESPACE bepaalt de toegang tot refs in een namespace, en is gelijjkwaardig aan de --namespace vlag. Dit wordt is voornamelijk nuttig aan de kant van de server, waar je misschien meerdere forks wilt opslaan van een enkele repository, waarbij alleen de refs apart wordt gehouden.

GIT_FLUSH kan gebruikt worden om Git te dwingen om niet gebufferde I/O te gebruiken als het incrementeel naar stdout schrijft. Een waarde van 1 heeft als gevolg dat Git vaker wegschrijft, een waarde van 0 heeft tot gevolg dat alle uitvoer eerst wordt gebufferd. De standaard waarde (als deze waarde niet wordt opgegeven) is om een toepasselijke bufferschema te kiezen afhankelijk van de aktiviteit en de uitvoer-modus.

GIT_REFLOG_ACTION* laat je de omschrijvende tekst bepalen die naar de reflog wordt geschreven. Hier is een voorbeeld:

$ GIT_REFLOG_ACTION="my action" git commit --allow-empty -m 'my message'
[master 9e3d55a] my message
$ git reflog -1
9e3d55a HEAD@{0}: my action: my message

=== Samenvatting

Je zou nu een redelijk goed begrip moeten hebben wat Git in de achtergrond uitvoert en, tot op zekere hoogte, hoe het is geïmplementeerd. Dit hoofdstuk heeft een aantal binnenwerk (plumbing) commando’s - commando’s die een niveau lager en simpeler zijn dan de deftige (porcelain) commando’s waarover je in de rest van het boek gelezen hebt. Het begrip over hoe Git op een dieper niveau werkt zou het makkelijker moeten maken om te begrijpen waarom het doet wat het doet en helpen bij het schrijven van je eigen gereedschappen en scripts om jouw specifieke werkwijze te ondersteunen.

Git als een op inhoud-adresseerbaar bestandssysteem is een zeer krachtig gereedschap die je eenvoudig kunt gebruiken voor meer dan alleen een versiebeheersysteem. We hopen dat je je pasverworven kennis over het binnenwerk van Git kunt gebruiken om je eigen gave toepassing van deze technologie te implementeren en je meer op je gemak te voelen bij het gebruik van Git op meer geavanceerde manieren.

== Git in andere omgevingen

Als je het hele boek gelezen hebt, zal je erg veel geleerd hebben over het gebruik van Git op de commando regel. Je kunt met lokale bestanden werken, je repository verbinden met andere via een netwerk, en op een effectieve manier werken met anderen. Maar daarmee houdt het verhaal niet op; Git wordt normaalgesproken gebruikt als onderdeel van een groter ecosysteem, en het werkstation is niet altijd de beste manier om ermee te werken. We zullen nu een kijkje nemen naar andere soorten omgevingen waar Git nuttig kan zijn, en hoe andere applicaties (inclusief de jouwe) zij aan zij samenwerken met Git.

=== Grafische interfaces

De natuurlijke omgeving van Git is het werkstation. Nieuwe mogelijkheden verschijnen daar het eerst, en alleen op de commandoregel is de volle kracht van Git volledig tot je beschikking. Maar platte tekst is niet de beste keuze voor alle taken; soms heb je gewoon een meer visuele representatie nodig, en sommige gebruikers voelen zich veel meer op hun gemak bij een point-and-click interface.

Het is belangrijk op te merken dat verschillende interfaces zijn toegesneden op andere workflows. Sommige clients laten slechts een verzameling zorgvuldig uitgekozen onderdelen van Git zien, dit om een specifieke manier van werken te ondersteunen die de auteurs ervan beschouwen als effectief. Vanuit dat oogpunt gezien zijn geen van deze gereedschappen “beter” te noemen dan een ander, ze zijn gewoonweg beter in staat om hun specifieke doel te dienen. Merk ook dat er niets is wat deze grafische clients kunnen doen wat niet vanaf de commando-regel te doen zou zijn; de commando-regel is nog steeds de plaats waar je de meeste mogelijkheden en controle hebt als je met je repositories werkt.

==== gitk en git-gui

Als je Git installeert, krijg je de visuele gereedschappen erbij, zijnde gitk en git-gui.

gitk is een gereedschap waarmee je de geschiedenis grafisch kan bekijken. Zie het als een krachtige grafische schil over git log en git grep. Dit is het gereedschap die je gebruikt als je iets probeert te vinden wat in het verleden heeft plaatsgevinden, of de historie van je project probeert te laten zien.

Gitk is het eenvoudigste aan te roepen vanaf de commando-regel. Gewoon cd gebruiken om een Git repository in te gaan en dan dit typen:

$ gitk [git log options]

Gitk accepteert veel opties van de commando-regel, de meeste daarvan worden doorgegeven aan de onderliggende git log actie. Een van de meest nuttige hiervan is de --all vlag, die gitk vertelt om commits te laten zien die vanuit elke ref bereikbaar zijn, niet alleen HEAD. De interface van Gitk ziet er zo uit:

De `gitk` historie viewer.
Figure 150. De gitk historie viewer.

Bovenaan is iets wat eruit ziet als de uitvoer van git log --graph; elke stip staat voor een commit, de lijntjes stellen ouderrelaties voor, en refs worden getoond als gekleurde vierkantjes. De gele stip stelt HEAD voor, en de rode stippen stellen wijzigingen voor die nog een commit moeten worden. Onderaan is een voorstelling van de geselecteerde commit; de commentaren en patch staan links en een samenvatting staat rechts. Hiertussen staat een verzameling van mogelijkheden om in de geschiedenis te zoeken.

git-gui daarentegen is primair een gereedschap om commits samen te stellen. Ook dit is het eenvoudigste om aan te roepen van de commando-regel:

$ git gui

En het ziet er ongeveer zo uit:

Het `git-gui` commit gereedschap.
Figure 151. Het git-gui commit gereedschap.

Links staat de index; wijzigingen die nog niet zijn gestaged staan bovenaan, wijzigingen die zijn gestaged staan onderaan. Je kunt volledige bestanden tussen deze twee stadia verplaatsen door op hun icoontjes te klikken, of je selecteert een bestand voor bekijken door op de naam te klikken.

Rechtsboven wordt de diff getoond, hier worden de veranderingen getoond voor het bestand dat op dat moment is geselecteerd. Je kunt individuele hunks stagen (of individuele regels) door in dit gebied rechts te klikken.

Rechtsonder is het gedeelte waar het bericht en acties kunnen worden ingetypt. Tik je bericht in de tekst-box en klik op “Commit” om iets vergelijkbaars te doen als git commit. Je kunt er ook voor kiezen om de laatste commit te amenderen door de “Amend” radio knop te klikken, wat het “Staged Changes” gedeelte vult met de inhoud van de laatste commit. Daarna kan je eenvoudigweg bepaalde wijzigingen kunt stagen of unstagen, het commit bericht wijzigen en weer “Commit” klikken om de oude commit te vervangen met een nieuwe.

gitk en git-gui zijn voorbeelden van taak-georiënteerde gereedschappen. Elk van deze is toegesneden op een specifieke doel (respectievelijk geschiedenis bekijken en commits maken), en zaken die niet nodig zijn voor die taak zijn weggelaten.

==== GitHub voor Mac en Windows

GitHub heeft twee workflow-georiënteerde clients gemaakt: een voor Windows en een voor Mac. Deze clients zijn goede voorbeelden van workflow-georiënteerde gereedschappen - inplaats van alle functionaliteit van Git beschikbaar te stellen, hebben ze zich gericht op een beperkte set van algemeen gebruikte functies die goed samenwerken. Ze zien er zo uit:

GitHub voor Mac.
Figure 152. GitHub voor Mac.
GitHub voor Windows.
Figure 153. GitHub voor Windows.

Ze zijn ontworpen om zoveel mogelijk op dezelfde manier er uit te zien en te werken, dus we zullen ze in dit hoofdstuk als een enkel product behandelen. We zullen hier geen gedetailleerde behandeling geven van deze gereedschappen (ze hebben hun eigen documentatie), maar een snel overzicht van de “changes” view (waar je het meeste van je tijd zult besteden) is op zijn plaats.

  • Links staat de lijst van repositories die de client volgt (trackt); je kunt een repository toevoegen (hetzij door het te klonen of door deze lokaal toe te voegen) door het “+” icoon te klikken die boven dit gebied staat.

  • In het midden is een commit-invoer gebied, dit stelt je in staat om een commit bericht in te voeren, en de bestanden die hierin mee moeten te selecteren. (In Windows is de commit historie hier direct onder getoond; op Mac is het een aparte tab.)

  • Rechts is een diff view, waar wordt getoond wat er in je werk directory is gewijzigd, of welke wijzigingen er in de geselecteerde commit zitten.

  • Het laatste waar je op kunt letten is de “Sync” knop rechtsboven, wat de belangrijkste manier is waarmee je over het netwerk communiceert.

Note

Je hebt geen GitHub account nodig om deze middelen te gebruiken. Alhoewel ze zijn ontworpen om de service van GitHub en aangeraden workflows in de aandacht te brengen, werken ze prima met elke willekeurige ander repository, en zullen netwerk operaties met alle Git hosts uitvoeren.

===== Installatie

GitHub voor Windows kan worden gedownload van https://windows.github.com, en GitHub voor Mac van https://mac.github.com. Als de applicatie voor het eerst wordt aangeroepen, voeren ze je door alle instellingen die nodig zijn om Git te laten werken, zoals het invoeren van je naam en email adres, en beide stellen verstandige standaard instellingen voor, voor veel gebruikte configuratie opties, zoals caches voor inloggegevens en CRLF gedrag.

Beide zijn “overwinteraars” (“evergreen”) – updates worden gedownload en geïnstalleerd terwijl de applicaties in gebruik zijn. Bij deze installatie zit ook een versie van Git ingesloten, wat betekent dat je je waarschijnlijk geen zorgen hoeft te maken om het ooit handmatig te hoeven updaten. Op Windows heeft de client een shortcut om Powershell aan te roepen met Posh-git, waar we later in dit hoofdstuk meer over zullen vertellen.

De volgende stap is om de applicatie een aantal repositories te geven om mee te werken. De client toont je een lijst met repositories waar je toegang toe hebt op GitHub, en je kunt ze in één stap klonen. ALs je al een lokale repository hebt, kan je de directory hiervan vanuit de Finder of Windows Explorer in de GitHub client slepen, en het zal in de lijst van repositories aan de linkerkant worden opgenomen.

===== Aangeraden workflow

Als het eenmaal is geïnstalleerd en geconfigureerd, kan je de GitHub client voor veel reguliere Git taken gebruiken. De tool is gemaakt met de workflow die “GitHub Flow” heet in gedachten. We behandelen dit in meer detail in De GitHub flow, maar de achterliggende gedachte is dat (a) je naar een branch gaat committen, en (b) dat je vrij regelmatig met een remote repository zult gaan synchroniseren.

Branch beheer is een van de gebieden waar deze twee tools afwijken. Op Mac, is er een knop bovenaan in de window voor het maken van een nieuwe branch:

``Create Branch'' knop op Mac.
Figure 154. “Create Branch” knop op Mac.

In Windows wordt dit gedaan door de naam van de nieuwe branch in de branch-switching widget in te typen:

Een branch aanmaken in Windows.
Figure 155. Een branch aanmaken in Windows.

Als je branch eenmaal is aangemaakt, is het maken van nieuwe commits min of meer rechttoe-rechtaan. Maak een aantal wijzigingen in je werk directory, en als je naar de GitHub client window gaat, zal het je laten zien welke bestanden zijn veranderd. Vul een commit bericht in, selecteer de bestanden die je erin wilt plaatsen, en klik op de “Commit” knop (ctrl-enter of ⌘-enter).

De belangrijkste manier waarmee je met andere repositories samenwerkt via het netwerk is via de “Synch” functie. Git heeft intern verschillende operaties voor pushen, fetchen, mergen en rebasen, maar de GitHub clients vatten die allemaal samen in een multi-stap functie. Hier is wat er gebeurt als je de Sync-knop klikt:

  1. git pull --rebase. Als dit mislukt omdat er een merge conflict is, val dan terug op git pull --no-rebase.

  2. git push.

Dit is de meeste gebruikelijke volgorde van netwerk commando’s als je op deze manier werkt, dus als je ze in een commando samenvoegt bespaart het je veel tijd.

===== Samenvatting

Deze gereedschappen zijn zeer geschikt voor de workflows waarvoor ze zijn ontworpen. Ontwikkelaars zowel als niet-ontwikkelaars kunnen op deze manier binnen een paar minuten met elkaar samenwerken op een project, en veel van de best practices voor dit soort van workflows zijn in de gereedschappen ingebakken. Echter, als je workflow hiervan afwijkt, of als je meer controle wilt over hoe en welke netwerk operaties er gedaan worden, raden we je een andere client of de commando-regel aan.

==== Andere GUIs

Er zijn een aantal andere grafische Git clients, en ze variëren van gespecialiseerde gereedschappen gemaakt voor één specifiek doel tot aan applicaties die probereen alles wat Git kan beschikbaar te stellen. De officiële Git website heeft een onderhouden lijst van de meest populaire clients op http://git-scm.com/downloads/guis. Een uitgebreidere lijst is beschikbaar op de Git wiki, op https://git.wiki.kernel.org/index.php/Interfaces,_frontends,_and_tools#Graphical_Interfaces.

=== Git in Visual Studio

Vanaf Visual Studio 2013 Update 1, hebben Visual Studio gebruikers een direct in hun IDE ingebouwde Git client. Visual Studio heeft al een hele tijd functionaliteit om te integreren met versie beheer, maar deze was gericht op gecentraliseerde systemen die bestanden blokkeren, en Git pastte niet goed in deze werkwijze. De Git ondersteuning van Visual Studio 2013 is gescheiden opgezet van deze oudere functionaliteit, en het resultaat is een veel betere aansluiting tussen Studio en Git.

Om de functionaliteit te starten, open je een project die wordt beheerd met Git (of type gewoon git init in een bestaand project), en selecteer View > Team Explorere in het menu. Je ziet dan de "Connect" view, die er ongeveer zo uit ziet:

Verbinding maken met een Git repository vanuit Team Explorer.
Figure 156. Verbinding maken met een Git repository vanuit Team Explorer.

Visual Studio onthoudt alle door Git beheerde projecten die je geopend hebt, en ze zijn beschikbaar in de lijst onderop. Als je het gewenste project daar niet ziet, klik dan op de "Add" link en type het pad naar de werk directory. Dubbel klikken op een van de lokale Git repositories resulteert in de Home view, die eruit ziet als De "Home" view voor een Git repository in Visual Studio.. Dit is een centrale plaats voor het uitvoeren van Git acties; als je code aan het schrijven bent, zal je waarschijnlijk de meeste tijd doorbrengen in de "Changes" view, maar wanneer het tijd wordt om wijzigingen die je teamgenoten hebben gemaakt te pullen, zal je de "Unsynched Commits" en "Branches" views gebruiken.

De Home view voor een Git repository in Visual Studio.
Figure 157. De "Home" view voor een Git repository in Visual Studio.

Visual Studio heeft nu een krachtige op taak georiënteerde gebruikers interface voor Git. Het bevat een lineaire historie view, een diff viewer, remote commando’s en vele andere mogelijkheden. Voor de volledige documentatie van alle mogelijkheden hiervan (die hier niet past), verwijzen we je naar http://msdn.microsoft.com/en-us/library/hh850437.aspx.

=== Git in Eclipse

Eclipse wordt geleverd met een plugin met de naam Egit, die een redelijk volledige interface verzorgt naar Git operaties. Je komt er door om te schakelen naar de Git Perspective (Window > Open Perspective > Other…, en selecteer "Git").

EGit omgeving van Eclipse.
Figure 158. EGit omgeving van Eclipse.

Bij EGit wordt veel goede documentatie geleverd, die je kunt vinden door naar Help > Help Contents te gaan, en de "EGit Documentation" knoop te kiezen in de inhoudsopgave.

=== Git in Bash

Als je een Bash gebruiker bent, kan je gebruik maken van een aantal van de mogelijkheden van je shell om je ervaringen met Git een stuk prettiger te maken. Git wordt namelijk met plugins voor een aantal shells geleverd, maar het staat standaard niet aan.

Allereerst, moet je een kopie van het bestand contrib/completion/git-completion.bash uit de Git broncode halen. Kopieer het naar een handige plaats, zoals je home directory, en voeg dit toe aan je .bashrc:

. ~/git-completion.bash

Als dat eenmaal gedaan is, wijzig je directory naar een git repository en type:

$ git chec<tab>

…en Bash zal automatisch het commando aanvullen naar git checkout. Dit werkt met alle subcommando’s van Git, commando-regel parameters en namen van remotes en refs waar van toepassing.

Het is ook nuttig om je prompt te wijzigen om informatie te laten zien over de Git repository in de huidige directory. Dit kan zo eenvoudig of complex worden als je zelf wilt, maar er zijn over het algemeen een paar stukken belangrijke informatie die de meeste mensen willen, zoals de huidige branch en de status van de werk directory. Om dit aan je prompt toe te voegen, kopiëer je simpelweg het contrib/completion/git-prompt.sh bestand van de Git broncode repository naar je home directory, en zet zoiets als dit in je .bashrc:

. ~/git-prompt.sh
export GIT_PS1_SHOWDIRTYSTATE=1
export PS1='\w$(__git_ps1 " (%s)")\$ '

De \w' betekent het afdrukken van de huidige working directory, de `\$ drukt het $ gedeelte van de prompt af, en __git_psl " (%s)" roept de functie aan die uitgevoerd wordt door git-prompt.sh met een formatterings argument. Nu zal je bash prompt er zo uit zien als je ergens in een door Git beheerd project zit:

Aangepaste `bash` prompt.
Figure 159. Aangepaste bash prompt.

Al deze scripts hebben behulpzame documentatie; neem een kijkje in de documentatie van git-completion.bash en git-prompt.sh voor meer informatie.

=== Git in Zsh

Git wordt ook geleverd met een library voor het voltooien van commando’s met tab voor Zsh. Om het te gebruiken, roep je gewoon autoload -Uz compinit && compinit aan in je .zshrc. De interface van zsh is een stukje krachtiger dan die van Bash:

$ git che<tab>
check-attr        -- laat gitattributes informatie zien
check-ref-format  -- controleer dat een referentie naam goed is samengesteld
checkout          -- checkout branch of pad naar de working tree
checkout-index    -- kopieer bestanden van de index naar working directory
cherry            -- vind commits die nog niet stroomopwaards zijn gemerged
cherry-pick       -- pas wijzigingen toe die door enkele bestaande commits zijn geintroduceerd

Tab-voltooingen die ambigue zijn worden niet alleen getoond; ze hebben behulpzame omschrijvingen, en je kunt de lijst grafisch navigeren door herhaaldelijk tab in te drukken. Dit werkt met Git commando’s, hun argumenten, en namen van zaken die zich in de repository bevinden (zoals refs en remotes), zowel als bestandsnamen en alle andere zaken waarvan Zsh weet hoe deze met de tab te voltooien.

Zsh wordt geleverd met een framework om informatie op te halen van een versie beheer systeem, genaamd vcs_info. Om de branchnaam rechts te tonen, voeg je deze regels toe aan je ~/.zshrc bestand:

autoload -Uz vcs_info
precmd_vcs_info() { vcs_info }
precmd_functions+=( precmd_vcs_info )
setopt prompt_subst
RPROMPT=\$vcs_info_msg_0_
# PROMPT=\$vcs_info_msg_0_'%# '
zstyle ':vcs_info:git:*' formats '%b'

Dit resulteert in het tonen van de huidige branch aan de rechterkant van de terminal, zodra je shell in een Git repository staat. (De linkerkant wordt ook ondersteund, vanzelfsprekend; gewoon de toewijzing naar PROMPT ontcommentarieren.) Het ziet er ongeveer zo uit:

Aangepaste `zsh` prompt.
Figure 160. Aangepaste zsh prompt.

Voor meer informatie over vcs_info, kan je de documentatie bekijken in de zshcontrib(1) man-page, of online op http://zsh.sourceforge.net/Doc/Release/User-Contributions.html#Version-Control-Information.

In plaats van vcs_info, heb je misschien voorkeur voor de prompt-aanpassingsscript dat met Git wordt geleverd, deze heet git-prompt.sh; zie https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh voor details. git-prompt.sh is compatible met Bash en Zsh.

Zsh is krachtig genoeg dat er complete frameworks aan zijn gewijd om het beter te maken. Een van deze heet "oh-my-zsh", en deze staat op https://github.com/robbyrussell/oh-my-zsh. In het plugin systeem van oh-my-zsh zit een krachtige git tab-voltooing, en het heeft een rijke verzameling prompt "themes", en vele daarvan tonen versie-beheer gegevens. Een voorbeeld van een oh-my-zsh thema. is maar een voorbeeld van wat gedaan kan worden met dit systeem.

Een voorbeeld van een oh-my-zsh thema.
Figure 161. Een voorbeeld van een oh-my-zsh thema.

=== Git in Powershell

De standaard commando regel terminal in Windows (cmd.exe) is niet echt in staat om een aangepaste Git beleving te ondersteunen, maar als je Powershell gebruikt heb je geluk. Dit werkt ook als je PowerShell op een niet-Windows platform zoals Debian werkt. Een pakket met de naam Posh-Git (https://github.com/dahlbyk/posh-git) levert krachtige tab-voltooings functionaliteit, zowel als een uitgebreide prompt om je te helpen bij het zeer nauwlettend volgen van je repository status. Het ziet er zo uit:

Powershell met Posh-git.
Figure 162. Powershell met Posh-git.

==== Installatie ===== Voorwaarden (alleen Windows) Voordat je PowerShell scripts op je machine kunt draaien, moet je je locale ExecutionPolicy op RemoteSigned zetten (Eigenlijk alles behalve Undefined en Restricted). Als je AllSigned kiest in plaats van RemoteSigned, moeten ook lokale scripts (je eigen) digitaal ontertekend worden om te kunnen worden uitgevoerd. Met RemoteSigned moeten alleen Scripts waarvan de "ZoneIdentifier" op Internetgezet is (gedownload vanaf het net) getekend worden, andere niet. Als je een adminstrator bent, en dit voor alle gebruikers op die machine installen, gebruik dan "-Scope LocalMachine". Als je een normale gebruiker bent, zonder administratieve rechten, kan je "-Scope CurrentUser" gebruiken om het alleen voor jou in te stellen. Meer over PowerShell Scopes: (https://technet.microsoft.com/de-de/library/hh847849.aspx) Meer over PowerShell ExecutionPolicy: (https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy)

> Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned -Force

===== PowerShell Gallery Als je minstens PowerShell 5 of PowerShell 4 met PackageManagement hebt geinstalleerd, kan je de package manager gebruiken om Posh-Git voor je op te halen. Meer informatie over de benodigdheden: (https://docs.microsoft.com/en-us/powershell/gallery/psget/get_psget_module)

> Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
> Update-Module PowerShellGet -Force
> Install-Module Posh-Git -Scope AllUsers

Als je Posh-Git alleen voor de huidige gebruiker wilt installeren en niet voor iedereen, gebruik dan "-Scope CurrentUser". Als het tweede commando faalt met een fout als Modules 'PowerShellGet' was not installed by using Install-Module, moet je eerst een ander commando aanroepen:

> Install-Module PowerShellGet -Force -SkipPublisherCheck

Dan kan je weer teruggaan en nog een keer proberen. Dit gebeurt, omdat de modules die met Windows PowerShell worden verscheept tegekend zijn met een ander publicatie certificaat.

===== Update PowerShell Prompt Om git informatie in je prompt te laten zien, moet posh-git worden geimporteerd. Om dit automatisch te laten gebeuren moet het import statement in je $profile script worden opgenomen. Dit script wordt elke keer als je een nieuw PowerShell prompt opent aangeroepen. Onthoud, dat er meerdere $profile scripts zijn. Bijv. een voor het console en een aparte voor de ISE.

> New-Item -Name $(Split-Path -Path $profile) -ItemType Directory -Force
> 'Import-Module Posh-Git' | Out-File -Append -Encoding default -FilePath $profile

===== Van broncode Gewoon een Poshh-Git versie downloaden van (https://github.com/dahlbyk/posh-git), en het uitpakken in de WindowsPowerShell directory. Dan een Powershell prompt openen als administrator, en dit uitvoeren:

> cd ~\Documents\WindowsPowerShell\Module\posh-git
> .\install.ps1

Dit zal de juiste regel toevoegen aan je profile.ps1 bestand en posh-git wordt actief de volgende keer dat je je prompt opent.

=== Samenvatting

Je hebt gezien hoe de kracht van Git kan worden aangewend vanuit de gereedschappen die je in je dagelijkse werkzaamheden gebruikt, en ook hoe je de Git repositories kunt benaderen vanuit je eigen programmatuur.

== Git in je applicaties inbouwen

Als je applicatie bestemd is voor ontwikkelaars, is er een grote kans dat het kan profiteren van integratie met versiebeheer. Zelfs applicaties voor niet-ontwikkelaars, zoals document editors, kunnen potentieel profiteren van versiebeheer functionaliteit, en het model van Git werkt erg goed voor vele verschillende scenarios.

Als je Git moet integreren met je applicatie, heb je eigenlijk twee opties: een shell openen en de Git commando-regel tool gebruiken, of Git als library inbedden in je applicatie.

=== Commando-regel Git

Een optie is om een shell proces op te starten (spawning) en de Git commando-regel tool gebruiken om het werk te doen. Dit heeft het voordeel van canoniek zijn, en alle mogelijkheden van Git worden ondersteund. Het is toevallig ook nog eens redelijk eenvoudig, omdat de meeste omgevingen waarin het programma draait een relatief eenvoudige manier hebben om een proces met commando-regel argumenten te aan te roepen. Echter, deze aanpak heeft ook een aantal nadelen.

Een ervan is dat alle uitvoer in platte tekst is. Dit houdt in dat je de soms gewijzigde uitvoer van Git moet parsen om de voortgang en resultaat informatie te lezen, wat inefficiënt en foutgevoelig kan zijn.

Een ander is een gebrekkige fout herstel. Als een repository op de een of andere manier corrupt is geraakt, of de gebruiker heeft een configuratiewaarde misvormd, zal Git simpelweg weigeren om een groot aantal operaties uit te voeren.

Weer een ander is proces beheer. Git eist dat je een shell omgeving bijhoudt in een separaat proces, wat ongewilde complexiteit kan toevoegen. Het coördineren van veel van dit soort processen (vooral als de kans bestaat dat dezelfde repository door een aantal processen wordt benaderd) kan een nogal grote uitdaging zijn.

=== Libgit2

Een andere optie die je hebt is om Libgit2 te gebruiken. Libgit2 is een implementatie van Git die nergens van afhankelijk is, met een focus op het behouden van een goede API om vanuit andere programma’s te gebruiken. Je kunt het vinden op http://libgit2.github.com.

Laten we allereerst eens kijken hoe de C API eruit ziet. Hier is een razendsnel overzicht:

// Open a repository
git_repository *repo;
int error = git_repository_open(&repo, "/path/to/repository");

// Dereference HEAD to a commit
git_object *head_commit;
error = git_revparse_single(&head_commit, repo, "HEAD^{commit}");
git_commit *commit = (git_commit*)head_commit;

// Print some of the commit's properties
printf("%s", git_commit_message(commit));
const git_signature *author = git_commit_author(commit);
printf("%s <%s>\n", author->name, author->email);
const git_oid *tree_id = git_commit_tree_id(commit);

// Cleanup
git_commit_free(commit);
git_repository_free(repo);

De eerste paar regels openen een Git repository. Het git_repository type vertegenwoordigt een handvat (handle) naar een repository met een buffer in het geheugen. Dit is de eenvoudigste manier, voor als je het exacte pad naar de werk directory of .git folder weet. Er is ook git_repository_open_ext die opties voor zoeken in zich heeft, git_clone en z’n vriendjes voor het maken van een lokale kloon van een remote repository, en git_repository_init voor het maken van een hele nieuwe repository.

Het tweede stuk code gebruikt rev-parse syntax (zie Branch referenties voor meer details) om de commit te krijgen waar HEAD uiteindelijk naar wijst. Het type dat je terugkrijgt is een git_object pointer, wat staat voor iets dat bestaat in de Git object database voor een repository. git_object is eigenlijk en “parent” type voor een aantal verschillende soorten objecten; de geheugenindeling voor elk van de “child” typen is hetzelfde als voor git_object, dus je kunt het veilig naar de juiste omzetten (casten). In dit geval, zou git_object_type(commit) GIT_OBJ_COMMIT teruggeven, dus het is veilig te casten naar een git_commit pointer.

Het volgende stuk laat zien hoe de kenmerken van een commit kunnen worden benaderd. De laatste regel hier gebruikt een git_oid type; dit is hoe Libgit2 een SHA-1 hash representeert.

Uit dit voorbeeld beginnen een aantal patronen naar boven te komen:

  • Als je een pointer declareert en een referentie hieraan doorgeeft met een Libgit2 aanroep, zal die aanroep waarschijnlijk een integer foutcode teruggeven. Een 0 waarde geeft een succes aan; al het andere is een fout.

  • Als Libgit2 een pointer voor je vult, ben jij verantwoordelijk deze weer vrij te geven.

  • Als Libgit2 een const pointer teruggeeft van een aanroep, hoef je deze niet vrij te geven, maar het wordt ongeldig als het object waar het toe behoort vrij wordt gegeven.

  • In C schrijven is nogal uitdagend.

Dat laatste houdt in dat het niet erg waarschijnlijk is dat je in C gaat schrijven als je Libgit2 gebruikt. Gelukkig is er een aantal taal-specifieke "bindings" beschikbaar die het redelijk eenvoudig maken om vanuit jouw specifieke taal en omgeving met Git repositories te werken. Laten we naar het bovenstaande voorbeeld kijken die we in Ruby schrijven met gebruikmaking van de bindings voor deze taal die Rugged heet, en gevonden kan worden op https://github.com/libgit2/rugged.

repo = Rugged::Repository.new('path/to/repository')
commit = repo.head.target
puts commit.message
puts "#{commit.author[:name]} <#{commit.author[:email]}>"
tree = commit.tree

Zoals je kunt zien, is de code een stuk schoner. Ten eerste, Rugged gebruikt exceptions; het kan dingen gooien als ConfigError of `ObjectError om fout situaties aan te geven. Ten tweede, er is geen expliciete vrij geven van middelen, omdat Ruby een garbage-collector kent. Laten we een kijkje nemen naar een iets ingewikkelder voorbeeld: vanaf het begin een commit samenstellen

blob_id = repo.write("Blob contents", :blob) (1)

index = repo.index
index.read_tree(repo.head.target.tree)
index.add(:path => 'newfile.txt', :oid => blob_id) (2)

sig = {
    :email => "bob@example.com",
    :name => "Bob User",
    :time => Time.now,
}

commit_id = Rugged::Commit.create(repo,
    :tree => index.write_tree(repo), (3)
    :author => sig,
    :committer => sig, (4)
    :message => "Add newfile.txt", (5)
    :parents => repo.empty? ? [] : [ repo.head.target ].compact, (6)
    :update_ref => 'HEAD', (7)
)
commit = repo.lookup(commit_id) (8)
  1. Maak een nieuwe blob, die de inhoud van een nieuw bestand bevat.

  2. Vul de index met tree van de commit van de head, en voeg het nieuwe bestand toe aan het pad nexfile.txt.

  3. Dit maakt een nieuwe tree aan in de ODB, en gebruikt deze voor de nieuwe commit.

  4. We gebruiken hetzelfde kenmerk voor de velden van zowel de auteur en de committer.

  5. Het commit bericht.

  6. Als je een commit maakt, moet je de ouders van de nieuwe commit opgeven. Dit gebruikt de punt van HEAD voor de enige ouder.

  7. Rugged (en Libgit2) kan optioneel een referentie updaten als er een commit wordt gemaakt.

  8. De retourwaarde is de SHA-1 has van een nieuw commit object, die je dan weer kan gebruiken om een Commit object te krijgen.

De code in Ruby is mooi en schoon, maar omdat Libgit2 het zware werk doet, zal deze code ook redelijk snel draaien. Als je niet zo’n rubyist bent, we behandelen nog een aantal andere bindings in [_libgit2_bindings].

==== Functionaliteit voor gevorderden

Libgit2 heeft een aantal mogelijkheden die buiten het bereik liggen van de kern van Git. Een voorbeeld is de inhaakmogelijkheid (pluggability): Libgit2 geeft je de mogelijkheid om eigengemaakte “backends” voor een aantal type operaties te verzorgen, zodat je dingen anders kunt opslaan dan Git standaard doet. Libgit2 staat, onder andere, eigengemaakte backends toe voor configuratie, opslag van refs en de object database.

Laten we eens kijken hoe dit in elkaar zit. De onderstaande code is geleend van de set van backend voorbeelden die door het Libgit2 team wordt geleverd (deze kunnen worden gevonden op https://github.com/libgit2/libgit2-backends). Hier zie je hoe een eigengemaakte backend voor de objectdatabase wordt opgezet:

git_odb *odb;
int error = git_odb_new(&odb); (1)

git_odb_backend *my_backend;
error = git_odb_backend_mine(&my_backend, /*…*/); (2)

error = git_odb_add_backend(odb, my_backend, 1); (3)

git_repository *repo;
error = git_repository_open(&repo, "some-path");
error = git_repository_set_odb(odb); (4)

(Merk op dat fouten worden opgevangen, maar niet afgehandeld. We hopen dat jouw code beter is dan de onze.)

  1. Initialiseer een lege object database (ODB) “frontend”, die als een bevatter dient voor de “backends” die het echte werk zullen uitvoeren.

  2. Initialiseer een eigengemaakte ODB backend.

  3. Voeg de backend toe aan de frontend.

  4. Open een repository, en stel het in om onze ODB te gebruiken om objecten op te zoeken.

Maar wat is dat git_odb_backend_mine voor een ding? Nou, dat is de constructor voor je eigen ODB implementatie, en je kunt daarin doen wat je wilt, zolang als je de git_odb_backend structuur maar juist vult. Hier is hoe het eruit zou kunnen zien:

typedef struct {
    git_odb_backend parent;

    // Some other stuff
    void *custom_context;
} my_backend_struct;

int git_odb_backend_mine(git_odb_backend **backend_out, /*…*/)
{
    my_backend_struct *backend;

    backend = calloc(1, sizeof (my_backend_struct));

    backend->custom_context = …;

    backend->parent.read = &my_backend__read;
    backend->parent.read_prefix = &my_backend__read_prefix;
    backend->parent.read_header = &my_backend__read_header;
    // …

    *backend_out = (git_odb_backend *) backend;

    return GIT_SUCCESS;
}

Een heel subtiele beperking hier is dat de eerste member van my_backend_struct een git_odb_backend structure moet zijn; dit zorgt ervoor dat de geheugenindeling dat is wat de Libgit2 code ervan verwacht. De rest is vrij in te vullen; deze structuur kan zo groot of zo klein zijn als je het nodig vindt.

De initialisatie functie alloceert wat geheugen voor de structure, richt de eigen context in en vult daarna de de members van de parent structure die het ondersteunt. Neem een kijkje in het include/git2/sys/odb_backend.h bestand in de Libgit2 broncode voor een volledige set van aanroep-signatures; jou specifieke use-case helpt je bepalen welke van deze je wilt ondersteunen.

==== Andere bindings

Libgit2 heeft bindings voor vele talen. Hier laten we je een klein voorbeeld waarbij we een aantal van de meer complete binding pakketen op het moment van schrijven; er bestaan libraries voor vele andere talen, waaronder C++, Go, Node.js, Erlang, en de JVM, alle in verschillende stadia van volwassenheid. De officiele verzameling van bindings kan je vinden door in de repositories te zoeken op https://github.com/libgit2. De code die we zullen schrijven gaat het commit bericht teruggeven van de commit waar de HEAD uiteindelijk naar toe wijst (vergelijkbaar met git log -l).

===== LibGit2Sharp

Als je een applicatie in .NET of Mono schrijft, is LigGit2Sharp (https://github.com/libgit2/libgit2sharp) waar je naar op zoek bent. De bindings zijn geschreven in C#, and er is veel zorg besteed aan het inpakken van de ruwe Libgit2 calls met CLR APIs die natuurlijk aanvoelen. Dit is hoe ons voorbeeld programma eruit ziet:

new Repository(@"C:\path\to\repo").Head.Tip.Message;

Voor Windows werkblad-applicaties is er zelfs een NuGet pakket dat je helpt snel op gang te komen.

===== objective-git

Als je applicatie op een Apple platform draait, gebruik je waarschijnlijk Objective-C als je implementatie taal. Objective-Git (https://github.com/libgit2/objective-git) is de naam van de Libgit2 bindings voor die omgeving. Ons voorbeeld programma ziet er zo uit:

GTRepository *repo =
    [[GTRepository alloc] initWithURL:[NSURL fileURLWithPath: @"/path/to/repo"] error:NULL];
NSString *msg = [[[repo headReferenceWithError:NULL] resolvedTarget] message];

Objective-git kan naadloos samenwerken met Swift, dus je hoeft er niet bang voor te zijn als je Objective-C hebt verlaten.

===== pygit2

De bindings voor Libgit2 in Python heten Pygit2, en kunnen worden gevonden op http://www.pygit2.org/. Ons voorbeeld programma:

pygit2.Repository("/path/to/repo") # open repository
    .head                          # get the current branch
    .peel(pygit2.Commit)           # walk down to the commit
    .message                       # read the message

==== Meer lezen

Een volledige behandeling van de mogelijkheden van Libgit2 ligt natuurlijk buiten het bestek van dit boek. Als je meer informatie wilt over Libgit2 zelf, kan je de API documentatie vinden op https://libgit2.github.com/libgit2, en een aantal handboeken op https://libgit2.github.com/docs. Voor de andere bindings, kan je de meegeleverde README en testen bekijken; je kunt er vaak kleine tutorials en hints naar meer informatie vinden.

=== JGit

Als je Git wilt gebruiken vanuit een Java programma, is er een Git library, Git geheten, die de volledige Git functionaliteit ondersteunt. JGit is een functioneel relatief volledige implementatie van Git geschreven in zuiver Java, en het wordt veel gebruikt in de Java gemeenschap. Het JGit project valt onder de Eclipse paraplu, en het adres waar het kan worden gevonden is http://www.eclipse.org/jgit.

==== De boel opzetten

Er zijn een aantal manieren om verbinding te maken met je project via JGit, en om er tegenaan te programmeren. Waarschijnlijk is de eenvoudigste manier om Maven te gebruiken - de integratie wordt bereikt door het volgende fragment aan de <dependencies> tag in je pom.xml bestand toe te voegen:

<dependency>
    <groupId>org.eclipse.jgit</groupId>
    <artifactId>org.eclipse.jgit</artifactId>
    <version>3.5.0.201409260305-r</version>
</dependency>

De versie zal hoogstwaarschijnlijk al vooruit zijn gegaan tegen de tijd dat je dit leest; kijk even op http://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit voor bijgewerkte repository informatie. Als deze stap eenmaal is voltooid, zal Maven automatisch de JGit libraries ophalen en die gebruiken die je nodig zult hebben.

Als je je binaire libraries liever zelf onderhoudt, zijn kant en klaar gebouwde JGit binairies beschikbaar op http://www.eclipse.org/jgit/download. Je kunt ze in je project inbouwen door een commando als deze aan te roepen:

javac -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App.java
java -cp .:org.eclipse.jgit-3.5.0.201409260305-r.jar App

==== Binnenwerk

JGit heeft twee basale niveaus van API: binnenwerk en koetswerk (plumbing en porcelain) De terminologie hiervoor komt van Git zelf, en JGit is ingedeeld in grofweg dezelfde soorten van gebieden: porcelain API’s zijn een vriendelijke voorkant voor reguliere gebruikers acties (het soort van dingen die een gewone gebruiker een commando regel voor zou gebruiken), terwijl de plumbing API’s er zijn voor interacties met repository objecten die zich diep in het systeem bevinden.

Het vertrekpunt voor de meeste JGit sessies is de Repository klasse, en het eerste wat je zult willen doen is om er een instantie van te maken. Voor een repository die gebaseerd is op een bestandssysteem (ja, JGit ondersteunt ook andere opslagmodellen), wordt dit gedaan door middel van FileRepositoryBuilder:

// Create a new repository
Repository newlyCreatedRepo = FileRepositoryBuilder.create(
    new File("/tmp/new_repo/.git"));
newlyCreatedRepo.create();

// Open an existing repository
Repository existingRepo = new FileRepositoryBuilder()
    .setGitDir(new File("my_repo/.git"))
    .build();

Deze builder heeft een vloeiende API waarmee alles kan worden voorzien die het nodig heeft om een Git repository te vinden, of je programma de preciese locatie weet of niet. Het kan de omgevingsvariabelen (.readEnvironment()) gebruiken, ergens van een plaats in de werk directory beginnen te zoeken (.setWorkTree(...).findGitDir()), of gewoon een bekende .git directory openen zoals hierboven.

Als je eenmaal een Repository instantie hebt, kan je er van allerlei dingen mee doen. Hier is een kleine bloemlezing:

// Get a reference
Ref master = repo.getRef("master");

// Get the object the reference points to
ObjectId masterTip = master.getObjectId();

// Rev-parse
ObjectId obj = repo.resolve("HEAD^{tree}");

// Load raw object contents
ObjectLoader loader = repo.open(masterTip);
loader.copyTo(System.out);

// Create a branch
RefUpdate createBranch1 = repo.updateRef("refs/heads/branch1");
createBranch1.setNewObjectId(masterTip);
createBranch1.update();

// Delete a branch
RefUpdate deleteBranch1 = repo.updateRef("refs/heads/branch1");
deleteBranch1.setForceUpdate(true);
deleteBranch1.delete();

// Config
Config cfg = repo.getConfig();
String name = cfg.getString("user", null, "name");

Er gebeurt hier eigenlijk best wel veel, dus laten we het stukje bij beetje doornemen.

De eerste regel haalt een pointer naar de master-referentie op. JGit pakt automatisch de echte master ref op, die gedefinieerd is op refs/heads/master, en geeft een object terug waarmee je de informatie over de referentie kunt ophalen. Je kunt de naam te pakken krijgen (.getName()), en het doel object van een directe referentie (.getObjectId()) of de referentie waar een symbolische ref naar wijst (.getTarget()). Ref objecten worden ook gebruikt om tag refs en objecten te vertegenwoordigen, dus je kunt vragen of de tag is “geschild” (“peeled”), wat inhoud dat het wijst naar het uiteindelijke doel van een (potentieel lange) tekenreeks van tag objecten.

De tweede regel haalt het doel van de master-referentie op, die wordt teruggegeven als een ObjectId instantie. ObjectId vertegenwoordigt de SHA-1 hash van een object, die al dan niet kan bestaan in de object database van Git. De derde regel is vergelijkbaar, maar laat zien hoe JGit omgaat met de rev-parse syntax (meer hierover in Branch referenties); je kunt er elke object specificatie aan doorgeven die Git begrijpt, en JGit geeft een geldige ObjectId terug voor dat object, of null.

De volgende twee regels laten zien hoe de rauwe inhoud van een object wordt ingeladen. In dit voorbeeld, roepen we ObjectLoader.copyTo() aan om de inhoud van het object direct naar stdout doorgeven, maar ObjectLoader heeft ook methoden om het type, de groote van het object te lezen, zowel als om de inhoud als een byte array terug te geven. Voor grote objeten (waar .isLarge() de waarde true teruggeeft), kan je .openStream() aanroepen om een InputStream-achtig object terug te krijgen die de data van de rauwe kan lezen zonder het eerst in z’n geheel in geheugen te lezen.

De volgende paar regels laten zien wat er voor nodig is om een nieuwe branch te maken. We maken een RefUpdate instantie, configureren een paar parameters, en roepen .update() aan om de wijziging af te trappen. Direct hierna is de code om dezelfde branch te verwijderen. Merk op dat .setForceUpdate(true) nodig is om dit te laten werken; anders zal de aanroep naar .delete() de waarde REJECTED teruggeven, en er gebeurt helemaal niets.

Het laatste voorbeeld laat zien hoe je de waarde user.name uit de Git configuratie bestanden kan ophalen. Deze instantie van Config gebruikt de repository die we eerder geopend hebben voor de lokale configuratie, maar pakt automatisch de wijzigingen op in de globale en systeem configuratie bestanden en leest daar ook waarden uit.

Dit is maar een klein voorproefje van de volledige binnenwerk API; er zijn nog veel meer methoden en klassen beschikbaar. Wat we hier ook niet hebben laten zien is de manier waarop JGit fouten behandelt, namelijk door middel van exceptions. De API’s van JGit gooien soms standaard Java excepties (zoals IOException), maar er is een heel scala aan JGit-specifieke exceptie typen die ook beschikbaar zijn (zoals NoRemoteRepositoryException, CorruptObjectException, en NoMergeBaseException).

==== Porcelein

De binnenwerk API’s zijn nogal compleet, maar het kan nogal bewerkelijk zijn om ze aan elkaar te knopen om een regulier doel te bereiken, zoals het toevoegen van een bestand aan de index, of een nieuwe commit maken. JGit levert een aantal API’s op een hoger niveau om je hiermee te helpen, en het beginpunt van deze API’s is de Git-klasse:

Repository repo;
// construct repo...
Git git = new Git(repo);

De Git klasse heeft een leuke verzameling hoog-niveau methoden in de builder-stijl die kunnen worden gebruikt om een redelijk ingewikkeld gedrag samen te stellen. Laten we een kijkje nemen naar een voorbeeld - iets doen als git ls-remote:

CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "p4ssw0rd");
Collection<Ref> remoteRefs = git.lsRemote()
    .setCredentialsProvider(cp)
    .setRemote("origin")
    .setTags(true)
    .setHeads(false)
    .call();
for (Ref ref : remoteRefs) {
    System.out.println(ref.getName() + " -> " + ref.getObjectId().name());
}

Dit is een normaal patroon met de Git klasse; de methode geeft een commando object terug waarmee je methode aanroepen aaneen kunt rijgen om parameters te zetten, die worden uitgevoerd als je .call() aanroept. In dit geval, vragen we de origin remote om tags, maar niet om heads. Merk ook het gebruik van een CredentialsProvider object op, voor autenticatie.

Vele andere commando’s zijn beschikbaar via de Git klasse, inclusief (maar niet beperkt tot) add, blame, commit, clean, push, rebase, revert en reset.

==== Meer lezen

Dit is maar een kleine selectie uit de volledige mogelijkheden van JGit. Als je geïnteresseerd bent en meer wilt lezen, kan je hier kijken voor meer informatie en inspiratie:

=== go-git

In het geval dat je Git in een service die in Golang is geschreven wilt integreren, is er ook een zuivere Go library implementatie. Deze implementatie heeft geen enkele afhankelijkheden met het onderliggende systeem en is dus niet onderheving aan handmatige geheugenbeheer fouten. Het is ook nog eens transparant voor de standaard Golang prestatie-analyse gereedschappen zoals CPU, Geheugen profilers, race detectors, etc.

go-git is gefocust op uitbreidbaarheid, compatibiliteit en ondersteund de meeste plumbing APIs, wat is beschreven op https://github.com/src-d/go-git/blob/master/COMPATIBILITY.md.

Hier is een eenvoudig voorbeeld van het gebruik van Go APIs:

import 	"gopkg.in/src-d/go-git.v4"

r, err := git.PlainClone("/tmp/foo", false, &git.CloneOptions{
    URL:      "https://github.com/src-d/go-git",
    Progress: os.Stdout,
})

Op het moment dat je een Repository instantie hebt, kan je de informatie verkrijgen en wijzigingen erop uitvoeren:

// retrieves the branch pointed by HEAD
ref, err := r.Head()

// get the commit object, pointed by ref
commit, err := r.CommitObject(ref.Hash())

// retrieves the commit history
history, err := commit.History()

// iterates over the commits and print each
for _, c := range history {
    fmt.Println(c)
}

==== Gevorderde functionaliteit

go-git heeft een aantal opmerkelijke mogelijkheden, een ervan is een uitbreidbaar opslag systeem, die vergelijkbaar is met Libgit2 backends. De standaard implementatie is in-memory opslag, wat heel snel is.

r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
    URL: "https://github.com/src-d/go-git",
})

Uitbreidbare opslag biedt veel interessante opties. Bijvoorbeeld, https://github.com/src-d/go-git/tree/master/_examples/storage geeft je d emogelijkheid om referenties, objecten en configuratie in een Aerospike database op te slaan.

Een andere mogelijkheid is een abstractie van een flexibel bestandssysteem. Met https://godoc.org/github.com/src-d/go-billy#Filesystem is het eenvoudig om alle bestanden op een andere manier op te slaan, bijv. door ze allemaal in een enkele archief op te slaan of door ze allemaal in het geheugen te houden.

Een andere geavanceerd gebruiksscenario is een erg optimaliseerbare HTTP client, zoals een die gevonden kan worden op https://github.com/src-d/go-git/blob/master/_examples/custom_http/main.go.

customClient := &http.Client{
	Transport: &http.Transport{ // accept any certificate (might be useful for testing)
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	},
	Timeout: 15 * time.Second,  // 15 second timeout
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse // don't follow redirect
	},
}

// Override http(s) default protocol to use our custom client
client.InstallProtocol("https", githttp.NewClient(customClient))

// Clone repository using the new client if the protocol is https://
r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{URL: url})

==== Meer lezen

Een volledige behandeling van de mogelijkheden van go-git ligt buiten het bestek van dit boek. Als je meer informatie wilt hebben over go-git, dan is API documentatie beschikbaar op https://godoc.org/gopkg.in/src-d/go-git.v4, en een aantal gebruiksvoorbeelden op https://github.com/src-d/go-git/tree/master/_examples.

== Git Commando’s

Overal in dit boek hebben we tientallen Git commando’s geïntroduceerd en we hebben ons best gedaan om ze te introduceren met een verhaaltje, en langzaamaan meer commando’s toe te voegen. Echter, hiermee zijn we geeindigd met een situatie waarbij de voorbeelden van het gebruik van commando’s nogal versnipperd zijn geraakt over het hele boek.

In deze appendix zullen we alle Git commando’s die we hebben behandeld in dit boek nogmaals doornemen, grofweg gegroepeerd op het gebruik ervan. We zullen globaal bespreken wat elk commando doet en verwijzen naar de plaats in het boek waar je kunt vinden waar we het hebben gebruikt.

=== Setup en configuratie

Er zijn twee commando’s die heel vaak gebruikt worden, van de eerste aanroepen van Git tot normale dagelijks gebruikte bijstellingen en referenties, de config en help commando’s.

==== git config

Git heeft een standaard manier om vele honderden dingen te doen. Voor veel van deze dingen, kan je Git vertellen om deze dingen standaard anders uit te voeren, of je voorkeuren instellen. Dit kan alles omvatten van Git vertellen wat je naam is, tot voorkeuren voor specifieke werkstation kleuren of welke editor je gebruikt. Er zijn verscheidene bestanden die door dit commando gelezen en geschreven worden zodat je deze waarden globaal kunt instellen of specifiek voor specifieke repositories.

Het git config commando is in ongeveer elk hoofdstuk van dit boek gebruikt.

In Git klaarmaken voor eerste gebruik hebben we het gebruikt om onze naam, email adres en editor voorkeuren aan te geven voordat we zelfs waren begonnen met Git te gebruiken.

In Git aliassen hebben we laten zien hoe je het kon gebruiken om commando-afkortingen te maken die lange optie-reeksen vervingen zodat je ze niet elke keer hoefde in te typen.

In Rebasen hebben we het gebruikt om --rebase de standaard manier te bepalen voor de aanroep van git pull.

In [_credential_caching] hebben we het gebruikt om een standaard bewaarplaats in te richten voor je HTTP wachtwoorden.

In [_keyword_expansion] hebben we laten zien hoe besmeur en opschoon filters op te zetten voor gegevens die Git in en uit gaan.

Tot slot is heel [_git_config] toegewijd aan dit commando.

==== git help

Het git help commando wordt gebruikt om je alle documentatie over elk commando te laten zien die geleverd wordt met Git. Hoewel we een grof overzicht van de meer populaire commando’s laten zien in deze appendix, kan je voor een volledige opsomming van alle mogelijke opties en vlaggen voor elk commando altijd git help <commando> aanroepen.

We hebben het git help commando in Hulp krijgen geïntroduceerd, en je laten zien hoe het te gebruiken om meer informatie te verkrijgen over de git shell in De server opzetten.

=== Projecten ophalen en maken

Er zijn twee manieren om een Git repository op te halen. Een manier is om het te kopiëren van een bestaande repository op het netwerk of ergens anders en de andere is om een nieuwe te maken in een bestaande directory.

==== git init

Om een directory in een nieuwe Git repository te veranderen zodat je kunt beginnen met versiebeheer, kan je simpelweg git init aanroepen.

We hebben dit voor het eerst behandeld in Een Git repository verkrijgen, waar we laten zien hoe een gloednieuwe repository gemaakt wordt om in te werken.

We hebben kort besproken hoe je de standaard branch van “master” kunt wijzigen in Branches op afstand (Remote branches).

We gebruiken dit commando om een lege bare repository te maken voor een server in De kale repository op een server zetten.

Tot slot, behandelen we een aantal details over wat er achter de schermen gebeurt in [_plumbing_porcelain].

==== git clone

Het git clone commando is eigenlijk een soort wrapper om een aantal andere commando’s. Het maakt een nieuwe directory, gaat daarin en roept git init aan om het een lege Git repository te maken, voegt een remote toe (git remote add) naar de URL die je het door hebt gegeven (standaard origin genaamd), roept een git fetch aan van die remote repository en checkt daarna de laatste commit uit naar je werk directory met git checkout.

Het git clone commando wordt in tientallen plaatsen in het boek gebruikt, maar we zullen een paar interessante plaatsen opnoemen.

Het wordt kort geïntroduceerd en uitgelegd in Een bestaande repository klonen, waar we een aantal voorbeelden behandelen.

In Git op een server krijgen bekijken we het gebruik van de --bare optie om een kopie van een Git repository te maken zonder een werk directory.

In [_bundling] gebruiken we het om een gebundelde Git repository te ontbundelen.

Tenslotte, in [_cloning_submodules] hebben we de --recurse-submodules optie laten zien waarmee het clonen van een repository met submodules iets te vereenvoudigen.

Alhoewel het op vele andere plaatsen in dit boek wordt gebruikt, zijn dit de toepassingen die nogal uniek zijn, of waar het op manieren wordt gebruikt die een beetje afwijkend zijn.

=== Basic Snapshotten

Voor de gewone workflow van het stagen van inhoud en dit committen naar je historie, zijn er maar een paar basis commando’s.

==== git add

Het git add commando voegt inhoud van de werk directory toe aan de staging area (of “index”) voor de volgende commit. Als het git commit commando wordt aangeroepen, kijkt het standaard alleen naar deze staging area, dus git add wordt gebruikt om de samenstelling van de volgende commit precies naar jouw eigen wens te bepalen.

Dit commando is een ongelofelijk belangrijk commando in Git en het wordt tientallen keren genoemd of gebruikt in dit boek. We zullen heel kort een aantal van de meer incourante gebruiken benoemen die kunnen worden gevonden.

We hebben git add voor het eerst geïntroduceerd en in detail uitgelegd in Nieuwe bestanden volgen (tracking).

We beschrijven hoe we het moeten gebruiken om merge conflicten op te lossen in Eenvoudige merge conflicten.

We behandelen het gebruik ervan om interactief alleen specifieke delen van een gewijzigd bestand te stagen in Interactief stagen.

Tot slot spelen we het op een laag niveau na in [_tree_objects], zodat je een idee krijgt van wat het achter de schermen doet.

==== git status

Het git status commando laat je de verschillende stadia zien van bestanden in je werk directory en staging area. Welke bestanden er zijn gewijzigd en nog niet gestaged en welke er zijn gestaged maar nog niet gecommit. In zijn reguliere vorm, laat het je ook een aantal hints zien over hoe bestanden tussen deze stadia te verplaatsen.

We behandelen status voor het eerst in De status van je bestanden controleren, zowel in zijn basis en versimpelde vormen. Alhoewel we het door het hele boek heen gebruiken, wordt vrijwel alles wat je met het git status commando kunt doen daar behandeld.

==== git diff

Het git diff commando wordt gebruikt als je de verschillen wilt zien tussen elke twee trees. Dit kan het verschil zijn tussen je werk omgeving en je staging area (git diff op zichzelf), tussen je staging area en je laatste commit (git diff --staged), of tussen twee commits (git diff master branchB).

We kijken eerst naar de basis gebruiken van git diff in Je staged en unstaged wijzigingen bekijken, waar we laten zien hoe je kunt zien welke wijzigingen er zijn gestaged en welke nog niet.

We gebruiken het om te kijken of er mogelijke witruimte-problemen zijn voor we committen met de --check optie in [commit_guidelines].

We zien hoe we de verschillen tussen branches efficiënter kunnen controleren met de git diff A...B syntax in Bepalen wat geïntroduceerd is geworden.

We gebruiken het om de witruimte verschillen weg te filteren met -b en hoe de verschillende stadia van conflicterende bestanden te vergelijken met --theirs, --ours en --base in Mergen voor gevorderden.

En tot slot, gebruiken we het om efficiënt submodule wijzigingen te vergelijjken met --submodule in [_starting_submodules].

==== git difftool

Het git difftool commando roept simpelweg een externe tool aan om je de verschillen te tonen tussen twee trees in het geval dat je iets anders wilt gebruiken dan het ingebouwde git diff commando.

We vermelden dit slechts kort in Je staged en unstaged wijzigingen bekijken.

==== git commit

Het git commit commando neemt alle bestandsinhoud die zijn gestaged met git add en slaat een nieuwe permanente snapshot in de database op en verplaatst erna de branch verwijzer op de huidige branch daar naar toe.

We behandelen eerst de basis van committen in Je wijzigingen committen. Daar laten we ook zien hoe je de -a vlag kunt gebruiken om de git add stap over te slaan in de dagelijkse workflows en hoe je de -m vlag kunt gebruiken om een commit bericht uit de commando regel kunt doorgeven in plaats van een editor te laten opstarten.

In Dingen ongedaan maken behandelen we het gebruik van de --amend optie om de meest recente commit over te doen.

In Branches in vogelvlucht gaan we met veel meer detail in op wat git commit doet en waarom het doet wat het doet.

We keken naar hoe commits cryptografisch te tekenen met de -S vlag in Commits tekenen.

Tot slot, nemen we een kijkje naar wat het git commit commando op de achtergrond doet en hoe het echt is geïmplementeerd in [_git_commit_objects].

==== git reset

Het git reset commando is voornamelijk gebruikt om zaken terug te draaien, zoals je waarschijnlijk al kunt zien aan het werkwoord. Het verplaatst de HEAD verwijzing en verandert optioneel de index of staging area en kan optioneel ook de werk directory wijzigen als je --hard gebruikt. Deze laatste optie maakt het mogelijk met dit commando je werk te verliezen als je het niet juist gebruikt, dus verzeker je ervan dat je alles goed begrijpt voordat je het gebruikt.

We hebben feitelijk het eenvoudigste gebruik van git reset in Een gestaged bestand unstagen behandeld, waar we het hebben gebruikt om een bestand te unstagen waar we git add op hadden gebruikt.

We behandelen het daarna in behoorlijk meer detail in Reset ontrafeld, die volledig is gewijd aan de uitleg van dit commando.

We gebruiken git reset --hard om een merge af te breken in Een merge afbreken, waar we ook git merge --abort gebruiken, wat een vorm van een wrapper is voor het git reset commando.

==== git rm

Het git rm commando wordt gebruikt om bestanden te verwijderen van de staging area en de werk directory voor Git. Het lijkt op git add in die zin dat het de verwijdering van een bestand staged voor de volgende commit.

We behandelen het git rm commando in enige detail in Bestanden verwijderen, inclusief het recursief verwijderen van bestanden en alleen bestanden verwijderen van de staging area maar ze in de werk directory ongemoeid te laten met --cached.

Het enige andere alternatieve gebruik van git rm in het boek is in [_removing_objects] waar we het even gebruiken en de --ignore-unmatch uitleggen als we git filter-branch aanroepen, wat ervoor zorgt dat we niet met een fout eindigen als het bestand dat we proberen te verwijderen niet bestaat. Dit kan nuttig zijn bij het maken van scripts.

==== git mv

Het git mv commando is een kleine gemaks-commando om een bestand te verplaatsen en dan git add aan te roepen op het nieuwe bestand en git rm op het oude bestand.

We noemen dit commando heel kort in Bestanden verplaatsen.

==== git clean

Het git clean commando wordt gebruikt om ongewenste bestanden te verwijderen uit je werk directory. Dit zou het verwijderen van tijdelijke bouw-artefacten kunnen inhouden of merge conflict bestanden.

We hebben veel van de opties en scenario’s besproken waar je het clean commando zou kunnen gebruiken in Je werk directory opschonen.

=== Branchen en mergen

Er zijn maar een handjevol commando’s die de meeste van de branch en merge functionaliteit in Git implementeren.

==== git branch

Het git branch commando is eigenlijk een soort branch beheer gereedschap. Het kan de branches die je hebt uitlijsten, een nieuwe branch aanmaken, branches verwijderen en hernoemen.

Het grootste gedeelte van Branchen in Git is gewijd aan het branch commando en het wordt door het gehele hoofdstuk gebruikt. We introduceren het eerst in Een nieuwe branch maken en we behandelen de meeste van de andere mogelijkheden (uitlijsten en verwijderen) in Branch-beheer.

In Tracking branches gebruiken we de git branch -u optie om een tracking branch op te zetten.

Tot slot, behandelen we een aantal dingen die het op de achtergrond doet in [_git_refs].

==== git checkout

Het git checkout commando wordt gebruikt om branches te wisselen en inhoud uit te cheken in je werk directory.

We komen het commando voor het eerst tegen in Tussen branches schakelen (switching) samen met de git branch commando.

We zien hoe het te gebruiken om tracking branches te starten met de --track vlag in Tracking branches.

We gebruiken het om bestandsconflicten te herintroduceren met --conflict=diff3 in Conflicten beter bekijken.

We behandelen het in nog meer detail in verband met haar relatie met git reset in Reset ontrafeld.

Tot slot, behandelen we een aantal implementatie details in [ref_the_ref].

==== git merge

Het git merge tool wordt gebruikt om een of meerdere branches te mergen naar de branch die je hebt uitgechecked. Het zal daarna de huidige branch voortbewegen naar het resultaat van de merge.

Het git merge commando werd voor het eerst geïntroduceerd in Eenvoudig branchen. En hoewel het op diverse plaatsen in het boek wordt gebruikt, zijn er over het algemeen erg weinig variaties op het merge commando, normaalgesproken alleen git merge <branch> met de naam van die ene branch die je wilt inmergen.

We hebben behandeld hoe een squashed merge te doen (waar Git het werk merged maar doet alsof het niet meer dan een nieuwe commit is zonder de historie van de branch die je in merged op te slaan) aan het einde van Gevorkt openbaar project.

We hebben veel behandeld over het merge proces en commando, inclusief het -Xignore-space-change commando en de --abort vlag om een problematische merge af te breken in Mergen voor gevorderden.

We hebben gezien hoe handtekeningen te verifiëren als je project GPG tekenen gebruikt in Commits tekenen.

Tot slot, hebben we Subtree mergen behandeld in Het mergen van subtrees.

==== git mergetool

Het git mergetool commando roept simpelweg een externe merge hulp aan in het geval dat je problemen hebt met een merge in Git.

We hebben het kort genoemd in Eenvoudige merge conflicten en behandelen met detail hoe je je eigen externe merge tool kunt implementeren in [_external_merge_tools].

==== git log

Het git log commando wordt gebruikt om de bereikbare opgeslagen historie van een project te laten zien vanaf de meeste recente commit snapshot en verder terug. Standaard laat het alleen de historie zien van de branch waar je op dat moment op werkt, maar het kan een andere of zelfs meerdere heads of branches worden gegeven vanwaar het door de geschiedenis zal gaan traceren. Het wordt ook vaak gebruikt om verschillen te laten zien tussen twee of meer branches op het commit niveau.

Dit commando wordt in vrijwel elk hoofdstuk van het boek gebruikt om de historie van het project te tonen.

We introduceren het commando en behandelen het met nogal wat detail in De commit geschiedenis bekijken. Daar nemen we een kijkje naar de -p en --stat opties om een indruk te krijgen van wat er was geïntroduceerd in elke commit en de --pretty en --oneline opties om de historie meer beknopt te bekijken, samen met wat eenvoudige datum en auteur filter opties.

In Een nieuwe branch maken gebruiken we het met de --decorate optie om eenvoudig te laten zien waar onze branch verwijzingen zijn en we gebruiken ook de --graph optie om te zien hoe uiteenlopende histories eruit zien.

In Besloten klein team en Commit reeksen behandelen we hoe de branchA..branchB syntax toe te passen om het git log commando te gebruiken om te bekijken welke commits er uniek zijn voor een branch in vergelijking met een andere branch. In Commit reeksen behandelen we dit redelijk uitgebreid.

In Merge log en Drievoudige punt behandelen we het gebruik van het branchA...branchB formaat en de --left-right syntax om te zien wat er in een branch zit of de andere, maar niet in beide. In Merge log kijken we ook naar hoe de --merge optie te gebruiken als hulp om een merge conflict te debuggen en ook het gebruik van de --cc optie om naar merge conflicten te kijken in je historie.

In RefLog verkorte namen gebruiken we de -g optie om de Git reflog te bekijken via dit gereedschap in plaats van branch navigatie te doen.

In Zoeken kijken we naar het gebruik van de -S en -L opties om een redelijk geraffineerde zoekopdrachten uit te voeren naar iets dat historisch heeft plaatsgevonden in de code, zoals het bekijken van de geschiedenis van een functie.

In Commits tekenen zien we hoe --show-signature te gebruiken om een validatie tekenreeks toe te voegen aan elke commit in de git log uitvoer afhankelijk van of het valide is getekend of niet.

==== git stash

Het git stash commando wordt gebruikt om tijdelijk niet-gecommit werk op te slaan, zodat je werk directory opgeschoond wordt; hierdoor hoef je geen onvolledig werk te committen naar een branch.

Dit wordt eigenlijk in zijn geheel behandeld in Stashen en opschonen.

==== git tag

Het git tag commando wordt gebruikt om een blijvende boekwijzer te geven aan een specifiek punt in de code historie. Over het algemeen wordt dit gebruikt voor zaken zoals releases.

Dit commando wordt geïntroduceerd en in detail behandeld in Taggen (Labelen) en we gebruiken het in de praktijk in Je releases taggen.

We behandelen ook hoe een GPG getekende tag te maken met de -s vlag en verifiëren er een met de -v vlag in Je werk tekenen.

=== Projecten delen en bijwerken

Er zijn niet veel commando’s in Git die het netwerk benaderen, bijna alle commando’s werken op de lokale database. Als je er klaar voor bent om je werk te delen of wijzigingen binnen te halen (pull) van elders, zijn er een handjevol commando’s die te maken hebben met remote repositories.

==== git fetch

Het git fetch commando communiceert met een remote repository en haalt alle informatie binnen die in die repository zit die nog niet in je huidige zit en bewaart dit in je lokale database.

We nemen voor het eerst een kijkje naar dit commando in Van je remotes fetchen en pullen en we vervolgen met het kijken naar voorbeelden van het gebruik in Branches op afstand (Remote branches).

We gebruiken het ook in diverse voorbeelden in Bijdragen aan een project.

We gebruiken het om een enkele specifieke refentie op te halen die buiten de standaard ruimte is in Pull Request Refs (Pull Request referenties) en we zien hoe we iets uit een bundel kunnen halen in [_bundling].

We richten een aantal zeer eigen refspecs in om git fetch iets op een net andere manier te laten doen dan standaard in [_refspec].

==== git pull

Het git pull commando is feitelijk een combinatie van de git fetch en git merge commando’s, waar Git van de remote die je opgeeft gaat ophalen en daarna direct probeert het opgehaalde te mergen in de branch waar je op dat moment op zit.

We stellen je het kort voor in Van je remotes fetchen en pullen en laten zien hoe je kunt bekijken wat het zal gaan mergen als je het aanroept in Een remote inspecteren.

We laten ook zien hoe het te gebruiken als hulp met bij problemen met rebasen in Rebaset spullen rebasen.

We laten zien hoe het te gebruiken met een URL om wijzigingen op een eenmalige manier te pullen in Remote branches uitchecken.

Tot slot, vermelden we heel snel dat je de --verify-signature optie kunt gebruiken om te verifiëren dat commits die je binnenhaalt met GPG getekend zijn in Commits tekenen.

==== git push

Het git push commando wordt gebruikt om te communiceren met een andere repository, berekenen wat je lokale database heeft en de remote niet, en daarna de verschillen te pushen naar de andere repository. Het vereist wel dat je schrijfrechten hebt op de andere repository en normaalgesproken is dit dus op de een of andere manier geautenticeerd.

We nemen een eerste kijkje naar het git push commando in Naar je remotes pushen. Hier behandelen we de basisprincipes van het pushen van een branch naar een remote repository. In Pushen gaan we iets dieper in op het pushen van specifieke branches en in Tracking branches zien we hoe tracking branches worden opgezet om automatisch naar te pushen. In Remote branches verwijderen gebruiken we de --delete vlag om een branch te verwijderen op de server met git push.

Door heel Bijdragen aan een project heen zien we een aantal voorbeelden van het gebruik van git push om werk op branches te delen middels meerdere remotes.

We zien hoe we het gebruiken om tags te delen die je gemaakt hebt met de --tags optie in Tags delen.

In [_publishing_submodules] gebruiken we de --recurse-submodules optie om te controleren dat al onze submodule werk is gepubliceerd voordat we het superproject pushen, wat zeer behulpzaam kan zijn als je submodules gebruikt.

In [_other_client_hooks] bespreken we kort de pre-push hook, wat een script is die we kunnen opzetten om te draaien voordat een push voltooid is, om te verifiëren dat pushen hiervan toegestaan is.

Tot slot, in [_pushing_refspecs] kijken we naar pushen met een volledige refspect in plaats van de generieke afkortingen die we normaalgesproken gebruiken. Dit kan je helpen om heel specifiek te zijn over welk werk je wilt delen.

==== git remote

Het git remote commando is een beheertool voor jouw administratie van remote repositories. Het stelt je in staat om lange URLs op te slaan als afkortingen, zoals “origin” zodat je ze niet elke keer helemaal hoeft in te typen. Je kunt er een aantal van hebben en het git remote commando wordt gebruikt om ze toe te voegen, te wijzigen en te verwijderen.

Dit commando wordt in detail behandeld Werken met remotes, inclusief het uitlijsten, toevoegen, verwijderen en het hernoemen.

Het wordt daarnaast in bijna elk daaropvolgend hoofdstuk van het boek gebruikt, maar altijd in de standaard git remote add <naam> <url> formaat.

==== git archive

Het git archive commando wordt gebruikt om een archiefbestand te maken van een specifieke snapshot van het project.

We gebruiken git archive om een tarball te maken van een project om het te delen in Een release voorbereiden.

==== git submodule

Het git submodule commando wordt gebruikt om externe repositories te beheren binnen normale repositories. Dit kan zijn voor libraries of andere tyepen gedeelde hulpmiddelen. Het submodule commando heeft een aantal sub-commando’s (add, update, sync, etc) om deze hulpmiddelen te beheren.

Dit commando wordt alleen benoemd en in zijn volledigheid behandeld in [_git_submodules].

=== Inspectie en vergelijking

==== git show

Het git show commando kan een Git object in een eenvoudige en voor een mens leesbare manier laten zien. Normaalgesproken gebruik je dit om de informatie over een tag of een commit te laten zien.

We gebruiken het eerst om informatie van een geannoteeerde tag te laten zien in Annotated tags.

Later gebruiken we het redelijk vaak in Revisie Selectie om de commits te tonen waar verscheidene van onze revisie selecties naar toe worden vertaald.

Een van de meer interessante dingen die we met git show doen is in Handmatig bestanden opnieuw mergen waar we specifieke bestandsinhoud ophalen uit verschillende stadia gedurende een merge conflict.

==== git shortlog

Het git shortlog commando wordt gebruikt om de uitvoer van git log samen te vatten. Het accepteert veel van dezelfde opties als het git log commando maar zal, in plaats van alle commits uit te lijsten, een samenvatting presenteren van de commits gegroepeerd per auteur.

We hebben laten zien hoe het te gebruiken om een mooie changelog te maken in De shortlog.

==== git describe

Het git describe commando wordt gebruikt om alles te pakken wat naar een commit kan leiden en produceert een tekenreeks die redelijk mens leesbaar is en niet zal veranderen. Het is een manier om een omschrijving van een commit te krijgen die net zo eenduidig is als een SHA-1, maar meer begrijpelijk.

We gebruiken git describe in Een bouw nummer genereren en Een release voorbereiden om een tekenreeks te verkrijgen om onze release bestand naar te vernoemen.

=== Debuggen

Git heeft een aantal commando’s die als hulp worden gebruikt bij het debuggen van een probleem in je code. Dit varieert van uitknobbelen waar iets is geïntroduceert tot wie het heeft geïntroduceerd.

==== git bisect

Het git bisect gereedschap is een ongelofelijk behulpzaam debugging tool die gebruikt wordt om uit te vinden welke specifieke commit de eerste was om een bug of probleem te bevatten door middel van een automatische binaire zoekactie.

Het wordt volledig behandeld in [_binary_search] en wordt alleen in dat gedeelte genoemd.

==== git blame

Het git blame commando markeert de regels van elk bestand met welke commit de laatste was die een wijziging invoerde bij elke regel in het bestand en welke persoon de auteur was van die commit. Dit is handig bij het uitzoeken van de persoon zodat er meer informatie kan worden gevraagd over een specifiek gedeelte van je code.

Dit wordt behandeld in [_file_annotation] en wordt alleen in dat gedeelte genoemd.

==== git grep

Het git grep commando kan je helpen elke willekeurige tekenreeks of regular expressie te vinden in elk bestand in je broncode, zelfs oudere versies van je project.

Het wordt behandeld in Git Grep en wordt alleen in dat gedeelte genoemd.

=== Patchen

Een aantal commando’s in Git draaien om het concept van het zien van commits in termen van de wijzigingen die ze introduceren, alsof de commit reeks een reeks van patches is. Deze commando’s helpen je je branches op deze manier te beheren.

==== git cherry-pick

Het git cherry-pick commando wordt gebruikt om de wijziging die in een enkele Git commit zit te pakken en deze te herintroduceren als een nieuwe commit op de branch waar je op dat moment op zit. Dit kan behulpzaam zijn bij het selectief een of twee commits te nemen van een branch in plaats van de hele branch in te mergen, waarbij alle wijzigingen worden genomen.

Cherry picking wordt beschreven en gedemonstreerd in Rebasende en cherry pick workflows.

==== git rebase

Het git rebase commando is eigenlijk een geautomatiseerde cherry-pick. Het bepaalt een reeks van commits en gaat deze dan een voor een in dezelfde volgorde elders cherry-picken.

Rebasen wordt in detail behandeld in Rebasen, inclusief het behandelen van de samenwerkings-problematiek waar je mee te maken krijgt als je al publieke branches gaat rebasen.

We gebruiken het in de praktijk tijdens een voorbeeld van het splitsen van je historie in twee aparte repositories in [_replace], waarbij we ook de --onto vlag gebruiken.

We behandelen het in een merge conflict geraken tijdens rebasen in Rerere.

We gebruiken het ook in een interactieve script modus met de -i optie in Meerdere commit berichten wijzigen.

==== git revert

Het git revert commando is feitelijk een omgekeerde git cherry-pick. Het maakt een nieuwe commit die de exacte tegenhanger van de wijziging die in de commit zit die je aanwijst toepast, en deze effectief ontdoet of terugdraait.

We gebruiken het in De commit terugdraaien om een merge commit terug te draaien.

=== Email

Veel Git projecten, inclusief Git zelf, worden volledig onderhouden via mail-lijsten. Git heeft een aantal gereedschappen ingebouwd gekregen die helpen dit proces eenvoudiger te maken, van het genereren van patches die je eenvoudig kunt mailen tot het toepassen van deze patches vanuit een email-box.

==== git apply

Het git apply commando past een patch toe die met het git diff of zelfs met het GNU diff commando is gemaakt. Dit is vergelijkbaar met wat het patch commando zou kunnen doen met een paar kleine verschillen.

We laten het gebruik zien en de omstandigheden waarin je het zou kunnen toepassen in Patches uit e-mail toepassen.

==== git am

Het git am commando wordt gebruikt om patches toe te passen vanuit een email inbox, en specifiek een die volgens mbox is geformatteerd. Dit is handig voor het ontvangen van patches via email en deze eenvoudig op je project toe te passen.

We hebben de workflow en gebruik rond git am behandeld in Een patch met am toepassen inclusief het gebruik van de --resolved, -i en -3 opties.

Er zijn ook een aantal hooks die je kunt gebruiken als hulp in de workflow rond git am en ze worden allemaal behandeld in [_email_hooks].

We hebben het ook gebruikt om GitHub Pull Request wijzigingen die als patch zijn geformatteerd toe te passen in Email berichten.

==== git format-patch

Het git format-patch commando wordt gebruikt om een reeks van patches te generen in mbox formaat die je kunt gebruiken om ze correct geformatteerd naar een mail lijst te sturen.

We behandelen een voorbeeld van een bijdrage leveren aan een project met gebruik van het git format-patch tool in Openbaar project per e-mail.

==== git imap-send

Het git imap-send commando zendt een mailbox gegenereerd met git format-patch naar een IMAP drafts folder.

We behandelen een voorbeeld van het bijdragen aan een project door het sturen van patches met de git imap-send tool in Openbaar project per e-mail.

==== git send-email

Het git send-email commando wordt gebruikt om patches via email te sturen die zijn gegenereerd met git format-patch.

We behandelen een voorbeeld van het bijdragen aan een project door het sturen van patches met het git send-email tool in Openbaar project per e-mail.

==== git request-pull

Het git request-pull commando wordt eenvoudigweg gebruikt om een voorbeeld bericht te genereren om naar iemand te mailen. Als je een branch hebt op een publieke server en iemand wilt laten weten hoe deze wijzigingen kunnen worden geïntegreerd zonder de patches via email te versturen, kan je dit commando aanroepen en de uitvoer sturen naar de persoon die je de wijzigingen wilt laten pullen.

We laten het gebruik van git request-pull om een pull message te laten genereren in Gevorkt openbaar project.

=== Externe systemen

Git wordt geleverd met een aantal commando’s om te integreren met andere versiebeheer systemen.

==== git svn

Het git svn commando wordt gebruikt om als client te communiceren met het Subversion versiebeheer systeem. Dit houdt in dat je Git kunt gebruiken om checkouts en commits te doen naar en van een Subversion server.

Dit commando wordt gedetailleerd behandeld in [_git_svn].

==== git fast-import

Voor andere versiebeheer systemen of het impoteren van bijna elk formaat, kan je git fast-import gebruiken om snel het andere formaat te mappen op iets wat Git eenvoudig kan vastleggen.

Dit commando wordt gedetailleerd behandeld in [_custom_importer].

=== Beheer

Als je een Git repository beheert of iets ingrijpend moet repareren, geeft Git je een aantal beheer commando’s om je hierbij te helpen.

==== git gc

Het git gc commando roept “vuilnis ophalen” (“garbage collection”) aan op je repository, waarbij onnodige bestanden in je database worden verwijderd en de overgebleven bestanden op een meer efficiënte manier worden opgeslagen.

Dit commando wordt normaalgesproken op de achtergrond voor jou aangeroepen, maar je kunt het handmatig aanroepen als je dat wilt. We behandelen een paar voorbeelden in [_git_gc].

==== git fsck

Het git fsck commando wordt gebruikt om de interne database te controleren op problemen of inconsistenties.

We gebruiken het slechts een keer heel kort in [_data_recovery] om te zoeken naar loshangende (dangling) objecten.

==== git reflog

Het git reflog commando neemt een log door waar in staat waar alle heads van al je branches hebben gestaan als je op zoek bent naar commits die je misschien bent verloren bij het herschrijven van histories.

We behandelen dit commando voornamelijk in RefLog verkorte namen, waar we het normale gebruik laten zien en hoe git log -g te gebruiken om dezelfde informatie te laten zien als in de git log uitvoer.

We behandelen ook een praktisch voorbeeld van het herstellen van zo’n verloren branch in [_data_recovery].

==== git filter-branch

Het git filter-branch commando wordt gebruikt om grote hoeveelheden commits te herschrijven volgens een bepaald patroon, zoals het overal verwijderen van een bestand of een hele repository terug te brengen tot een enkele subdirectory voor het extraheren van een project.

In Een bestand uit iedere commit verwijderen leggen we het commando uit en verkennen een aantal verschillende opties zoals --commit-filter, --subdirectory-filter en --tree-filter.

In [_git_p4] en [_git_tfs] gebruiken we dit om geïmporteerde externe repositories te op te schonen.

=== Binnenwerk commando’s (plumbing commando’s)

Er zijn ook nog een behoorlijk aantal meer technische binnenwerk commando’s die we zijn tegengekomen in het boek.

De eerste die we tegenkomen is ls-remote in Pull Request Refs (Pull Request referenties) waar we het gebruiken om te kijken naar de kale referenties op de server.

We gebruiken ls-files in Handmatig bestanden opnieuw mergen, Rerere en De index om een meer technische kijk te nemen op hoe je staging gebied er eigenlijk uitziet.

We gebruiken rev-parse in Branch referenties om zo ongeveer elke tekenreeks te nemen en het in een object SHA-1 te veranderen.

Echter, de meeste van de technische binnenwerk commando’s die we behandelen staan in [ch10-git-internals], wat meteen het belangrijkste onderwerp is waar dit hoofdstuk zich op richt. We hebben geprobeerd het gebruik van deze commando’s in de rest van het boek te vermijden.