Git
Chapters ▾ 2nd Edition

9.1 Git en andere systemen - Git als een client

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 zou 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 instrumenten 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. Ze zullen hier degenen behandelen die je het meest waarschijnlijk in het wild 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 zou gebruiken. 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. Er zijn best wel een paar commando’s nodig, 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 laten raken. 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 mijn test repository. Om dat eenvoudig te doen, kan je een tool svnsync genaamd gebruiken die bij Subversion wordt geleverd. Voor deze tests hebben we een nieuwe Subversion repository op Google Code gemaakt die een gedeeltelijke kopie was van het protobuf project, wat een tool is die gestructureerde gegevens encodeert voor versturen over een netwerk.

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://progit-example.googlecode.com/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. Het test project heeft maar ongeveer 75 commits 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 merged. 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 pulled die je nog niet hebt, en rebased als 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 merged 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...

Het is belangrijk om te onthouden, omdat de uitkomt 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 cloont, 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), is dit geen normale Git merge commit. 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 gepushed 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 daarna 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 aan 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; en ook de commits die mensen in de tussentijd naar de Subversion server hebben gemaakt niet. 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

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

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 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 dat universum, elk met hun eigen aanpak om hoe juist 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, als je het gedrag van Git aan de kant van het werkstation de voorkeurt geeft, maar als je werkt met een project wiens broncode wordt beheerd met Mercurial, is 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 http://mercurial.selenic.com/ 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 beging 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 hebben verwacht:

$ 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 op 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 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. Niettemin, de mogelijkheden en beperkingen zijn zeer goed afgestemd 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 146. 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 clonen. Hier clonen 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 gemapped 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 email lijst van je personeel 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 clonen 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 werkelijkheid 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 147. 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 hoge-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 krijgt 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 gepushed 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 (virtual machine) 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 omgevings variabelen moeten inrichten:

$ export P4PORT=10.0.1.254:1666
$ export P4USER=john
Op gang komen

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

$ 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 rebased, 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 die git-p4 behulpzaam heeft toegevoegd. Git-p4 probeert jouw Git instellingen en die van Perforce elk te bevredigen 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.

Pas 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 clonen, maar alle changesets die ooit in aanraking zijn geweest met deze paden. Dit zit dichter bij het Git-concept van een clone, 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 voro 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 gebranched 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 gepushed.

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 uiteenlopende 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 de 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 bewaren:

  • 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 uiteenlopende 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 zoals het ontworpen is, 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 148. 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.