Git 🌙
Chapters ▾ 2nd Edition

9.1 Git und andere VCS-Systeme - Git als Client

Die Welt ist nicht perfekt. Normalerweise kannst du nicht jedes Projekt, mit dem du arbeitest, sofort auf Git umstellen. Manchmal steckt man in einem Projekt mit einem anderen VCS fest und wünscht sich, man könnte zu Git wechseln. Wir werden im ersten Teil dieses Kapitels die Möglichkeiten kennenlernen, Git als Client zu verwenden, falls das Projekt, an dem du gerade arbeitest, ein anderes System nutzt.

Irgendwann wirst du vielleicht dein bestehendes Projekt nach Git migrieren wollen. Der zweite Teil dieses Kapitels behandelt die Migration deines Projekts zu Git aus verschiedenen Systemen sowie eine funktionierende Methode, wenn kein vorgefertigtes Import-Tool vorhanden ist.

Git als Client

Git bietet Entwicklern eine so reizvolle Umgebung, dass viele Anwender schon herausgefunden haben, wie man es auf den Arbeitsplätzen nutzen kann, auch wenn der Rest des Teams ein völlig anderes VCS einsetzt. Es gibt eine Vielzahl dieser Schnittstellen, die sogenannten „Brücken“. Hier werden wir die vorstellen, denen du am ehesten in der „freien Wildbahn“ begegnen wirst.

Git und Subversion

Ein großer Teil der Open-Source-Entwicklungsprojekte und eine ganze Reihe von Unternehmensprojekten nutzen Subversion zur Verwaltung ihres Quellcodes. Es gibt Subversion seit mehr als einem Jahrzehnt, und die meiste Zeit war es erste Wahl für ein VCS im Bereich Open-Source-Projekte. Es ist auch in vielen Aspekten sehr ähnlich zu CVS, das vorher der wichtigste Vertreter der Versionsverwaltung war.

Eines der herausragenden Merkmale von Git ist die bidirektionale Brücke zu Subversion, genannt git svn. Mit diesem Tool kannst du Git als geeigneter Client für einen Subversion-Server verwenden, so dass du alle lokalen Funktionen von Git nutzen und dann auf einen Subversion-Server pushen kannst, als ob du Subversion lokal einsetzen würdest. Das bedeutet, dass du lokale Branching- und Merging-Aktivitäten vornimmst, die Staging-Area nutzt, Rebasing- und Cherry-Picking-Aktivitäten durchführen kannst, während deine Mitstreiter weiterhin in ihrer dunklen und altertümlichen SVN-Umgebung tätig sind. Es ist eine gute Möglichkeit, Git in die Unternehmensumgebung einzuschleusen, deine Entwicklerkollegen zu helfen, effizienter zu werden und gleichzeitig die Infrastruktur so zu ändern, dass Git vollständig unterstützt wird. Die Subversion-Brücke ist das Portal zur D(istributed)-VCS-Welt.

git svn

Der Hauptbefehl in Git für sämtliches Subversion-Bridging ist git svn. Es sind ziemlich wenige Befehle erforderlich, so dass wir die gängigsten aufzeigen und dabei einige einfache Workflows durchgehen werden.

Es ist wichtig zu beachten, dass du bei der Verwendung von git svn mit Subversion interagierst. SVN ist ein System, das ganz anders funktioniert als Git. Obwohl du lokales Branching und Merging durchführen kannst, ist es im Allgemeinen ratsam, deinen Verlauf so linear wie möglich zu gestalten, indem du deine Arbeiten rebased. Vermeide dabei die gleichzeitige Interaktion mit einem Git Remote-Repository.

Schreibe deinen Verlauf nicht um und versuche nicht, diesen geänderten Verlauf erneut zu pushen. Pushe nicht in ein paralleles Git-Repository, um gleichzeitig mit anderen Git-Entwicklern zusammenzuarbeiten. Subversion kann nur einen einzigen linearen Verlauf haben und es ist sehr einfach, diesen Verlauf durcheinanderzubringen. Wenn du mit einem Team arbeitest und einige verwenden SVN und andere Git, stelle sicher, dass alle den SVN-Server für die gemeinsame Arbeit verwenden – das erleichtert dir deinen Alltag.

Einrichtung

Um diese Funktionalität zu demonstrieren, benötigst du ein SVN-Repository mit Schreibzugriff. Wenn du die Beispiele kopieren möchtest, musst du eine beschreibbare Kopie eines SVN-Test Repository erstellen. Um das zu tun, kannst du das Tool svnsync verwenden, das in einer Subversion-Installation enthalten ist.

Um dem Beispielen zu folgen, musst du zunächst ein neues lokales Subversion-Repository erstellen:

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

Aktiviere dann alle Benutzer, um revprops zu ändern. Der einfachste Weg ist, ein pre-revprop-change Skript hinzuzufügen, das immer den exit-Wert 0 hat:

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

Du kannst dieses Projekt nun auf deinem lokalen Rechner synchronisieren, indem du svnsync init mit den Repositorys to und from aufrufst.

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

Dadurch werden die Eigenschaften für die Ausführung der Synchronisierung festgelegt. Anschliessend kannst du den Code klonen, indem du Folgendes ausführst:

$ 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.
[…]

Obwohl dieser Vorgang nur wenige Minuten in Anspruch nehmen könnte, dauert der Prozess fast eine Stunde, wenn du versuchen solltest, das ursprüngliche Repository in ein anderes Remote-Repository anstelle eines lokalen zu kopieren, obwohl es weniger als 100 Commits gibt. Subversion muss eine Revision nach der anderen klonen und sie dann wieder in ein anderes Repository verschieben. Es ist äußerst ineffizient, aber es ist der einzige einfache Weg, das zu erreichen.

Erste Schritte

Jetzt, da du ein beschreibbares Subversion-Repository hast, kannst du einen normalen Workflow nutzen. Beginne mit dem Befehl git svn clone, der ein komplettes Subversion-Repository in ein lokales Git-Repository importiert. Beachte, dass du beim Import aus einem echten Subversion-Repository file:///tmp/test-svn durch die URL deines Subversion-Repositorys ersetzen musst:

$ 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

Das entspricht zwei Befehlen – git svn init gefolgt von git svn fetch – auf der von dir angegebenen URL. Dieser Vorgang kann einige Zeit dauern. Wenn beispielsweise das Testprojekt nur etwa 75 Commits hat und die Code-Basis nicht so groß ist, muss Git dennoch jede Version einzeln auschecken und einzeln committen. Bei einem Projekt mit Hunderten oder Tausenden von Commits kann es Stunden oder gar Tage dauern.

Der Teil -T trunk -b branches -t tags teilt Git mit, dass dieses Subversion-Repository den grundlegenden Branching- und Tagging-Konventionen folgt. Wenn du deinen Trunk, deine Branches oder Tags anders benennst, können sich diese Optionen ändern. Da dies so häufig vorkommt, kannst du den gesamten Teil durch -s ersetzen, was Standardlayout bedeutet und all diese Optionen beinhaltet. Das folgende Kommando ist dabei gleichwertig:

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

An dieser Stelle solltest du über ein valides Git-Repository verfügen, das deine Branches und Tags importiert hat:

$ 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

Beachte, dass dieses Tool Subversion-Tags als Remote-Referenzen (engl. refs) verwaltet. Werfen wir einen genaueren Blick auf den Git Low-Level-Befehl show-ref:

$ 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 macht das nicht, wenn man von einem Git-Server geklont hat. So sieht ein Repository mit Tags nach einem frischen Klon aus:

$ 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 die Tags direkt in refs/tags, anstatt sie mit entfernten Branches zu verknüpfen.

Zurück zu Subversion committen

Jetzt, da du ein Arbeitsverzeichnis hast, kannst du an dem Projekt arbeiten und deine Commits wieder zum Upstream pushen, indem du Git als SVN-Client verwendest. Wenn du eine der Dateien bearbeitest und überträgst, hast du einen Commit, der in Git lokal existiert aber nicht auf dem Subversion-Server vorhanden ist:

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

Als nächstes musst du deine Änderung zum Upstream pushen. Beachte, wie sich dies auf deine Arbeitsweise mit Subversion auswirkt. Du kannst mehrere Commits offline durchführen und diese dann alle auf einmal auf den Subversion-Server übertragen. Um zu einem Subversion-Server zu pushen, führe den Befehl git svn dcommit aus:

$ 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

Dies erstellt für jeden Commit, den du auf deinem Subversion Server-Codes gemacht hast, einen eigenen Subversion-Commit und schreibt deinen lokalen Git-Commit um, um einen eindeutigen Identifier einzufügen. Das ist wichtig, weil es bedeutet, dass sich alle SHA-1-Prüfsummen für deine Commits ändern. Aus diesem Grund ist es keine gute Idee, gleichzeitig mit Git-basierten Remotes deines Projekts und einem Subversion-Server zu arbeiten. Wenn du dir den letzten Commit ansiehst, siehst du die neu hinzugefügte git-svn-id:

$ 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

Es ist zu beachten, dass die SHA-1-Prüfsumme, die ursprünglich mit 4af61fd begann, als du die Daten übertragen hast, nun mit 95e0222 beginnt. Wenn du sowohl auf einen Git-Server als auch auf einen Subversion-Server pushen möchtest, musst du zuerst auf den Subversion-Server pushen (dcommit), da diese Aktion deine Commit-Daten ändert.

Neue Änderungen pullen

Wenn du mit anderen Entwicklern zusammenarbeitest, wird irgendwann einer von euch pushen, und andere versuchen, eine Änderung voranzutreiben, die Konflikte verursacht. Diese Änderung wird abgelehnt, bis du deren Arbeit mergst. In git svn sieht das so aus:

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

Um diese Situation zu lösen, kannst du git svn rebase ausführen, das alle Änderungen auf dem Server, die du noch nicht hast, pullt und alle deine lokalen Arbeiten auf den gepullten Code rebased:

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

Jetzt ist deine gesamte Arbeit auf den Code des Subversion-Servers rebased, so dass du dcommit erfolgreich einsetzen kannst:

$ 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

Im Unterschied zu Git, das voraussetzt, dass du Upstream-Arbeiten, die du noch nicht lokal hast, zuerst mergst, bevor du pushen kannst, zwingt dich git svn dazu nur dann, wenn die Änderungen im Konflikt stehen (ähnlich wie bei Subversion). Wenn jemand anderes eine Änderung an einer Datei macht und du eine Änderung an einer anderen Datei machst, funktioniert dein dcommit gut:

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

Es ist sehr wichtig, sich daran zu halten, denn das Ergebnis ist ein Projektstatus, der beim Push auf keinem deiner Computer vorhanden war. Wenn die Änderungen inkompatibel sind, aber keine Konflikte verursachen, können Probleme auftreten, die schwer zu diagnostizieren sind. Das ist ein Unterschied gegenüber der Nutzung eines Git-Servers. In Git kannst du den Zustand auf deinem Client-System vor der Veröffentlichung vollständig testen, während du in SVN nie sicher sein kannst, dass die Zustände unmittelbar vor dem Commit und nach dem Commit identisch sind.

Du solltest diesen Befehl auch ausführen, um Änderungen vom Subversion-Server einzubinden, auch wenn du noch nicht bereit bist, selbst zu committen. Es ist ratsam, git svn fetch auszuführen, um die neuen Daten zu holen, aber git svn rebase übernimmt bereits den Fetch und aktualisiert dann deine lokalen Commits.

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

Wenn du ab und zu git svn rebase laufen lässt, stellst du sicher, dass dein Code immer auf dem neuesten Stand ist. Du solltest jedoch überprüfen, ob dein Arbeitsverzeichnis sauber ist, wenn du diese Funktion ausführst. Wenn du lokale Änderungen hast, musst du deine Arbeit entweder verstecken (engl. stash) oder temporär committen, bevor du git svn rebase ausführst. Andernfalls wird der Befehl angehalten, wenn er erkennt, dass das Rebase zu einem Merge-Konflikt führen wird.

Git Branching Probleme

Sobald du dich mit einem Git-Workflow vertraut gemacht hast, wirst du wahrscheinlich Topic-Branches erstellen, an ihnen arbeiten und sie dann mergen wollen. Wenn du über git svn auf einen Subversion-Server pushst, kannst du deine Arbeit jedes Mal auf einen einzigen Branch rebasen, anstatt Branches zu mergen. Die Begründung für ein Rebasing ist, dass Subversion eine lineare Historie hat und sich nicht wie Git mit Merges beschäftigt. So folgt git svn bei der Konvertierung der Snapshots in Subversion Commits nur dem ersten Elternteil.

Nehmen wir an, dein Verlauf sieht wie folgt aus: Du hast einen experiment Branch erstellt, zwei Commits durchgeführt und diese dann wieder mit dem master zusammengeführt. Wenn du dcommit aufrufst, erscheint folgende Anzeige:

$ 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

Das Ausführen von dcommit auf einem Branch mit zusammengeführtem Verlauf funktioniert gut, außer wenn du dir dein Git-Projekt-Historie ansiehst. Es wurde keiner der Commits, die du auf dem experiment Branch gemacht hast, neu geschrieben – statt dessen erscheinen alle diese Änderungen in der SVN-Version eines einzelnen Merge-Commits.

Wenn jemand anderes diese Arbeit klont, sieht man nur den Merge-Commit mit der gesamten Arbeit, die in ihn zusammengeführt wurde, als ob du git merge --squash ausgeführt hättest. Man sieht die Commit-Daten nicht, weder woher sie stammen noch wann sie committed wurden.

Subversion Branching

Branching in Subversion ist nicht dasselbe wie Branching in Git. Es ist wahrscheinlich das Beste, wenn du es so gut es geht vermeidest. Du kannst aber mit git svn in Subversion Branches anlegen und dorthin committen.

Erstellen eines neuen SVN Branches

Um einen neuen Branch in Subversion zu erstellen, führe git svn branch [new-branch] aus:

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

Dadurch wird das Äquivalent des Befehls svn copy trunk branches/opera in Subversion ausgeführt und auf dem Subversion-Server angewendet. Es ist wichtig zu beachten, dass dieser Branch nicht ausgecheckt wird. Wenn du an diesem Punkt einen Commit durchführst, geht dieser Commit in den trunk auf dem Server, nicht in opera.

Aktive Branches wechseln

Git findet heraus, in welchen Branch deine dcommits hingehen, indem es nach dem Ende eines deiner Subversion Branches in deinem Verlauf sucht – du solltest nur einen haben, und es sollte der Letzte sein, der eine git-svn-id in deinem aktuellen Branchverlauf hat.

Wenn du an mehr als einem Branch gleichzeitig arbeiten möchtest, kannst du lokale Branches so einrichten, dass du dcommit für bestimmte Subversion-Branches ausführst, indem du diese beim importierten Subversion-Commit für diesen Branch startest. Einen opera Branch, mit dem du separat arbeiten kannst, kannst du folgendermaßen initialisieren:

$ git branch opera remotes/origin/opera

Um deinen opera Branch in trunk (deinen master Branch) zu mergen, kannst du das mit einem normalen git merge machen. Aber du solltest unbedingt eine beschreibende Commit-Meldung (via -m) angeben, sonst wird beim Merge anstelle von etwas Vernünftigem „Merge branch opera“ angezeigt.

Obwohl du für diese Operation git merge verwendest und der Merge wahrscheinlich viel einfacher ist als in Subversion (da Git automatisch die entsprechende Merge-Basis für dich erkennt), ist es kein normaler Git Merge-Commit. Du musst diese Daten an einen Subversion-Server zurück pushen, der keinen Commit mit mehr als einem Elternteil verarbeiten kann. Nachdem du ihn zum Server gepusht hast, sieht er also aus wie ein einzelner Commit, der die gesamte Arbeit eines anderen Branchs unter einem einzigen Commit zusammenfasst. Nachdem du einen Branch in einem anderen zusammengeführt haben, kannst du nicht einfach zurückgehen und an diesem Branch weiterarbeiten, wie du es normalerweise in Git tust. Das dcommit Kommando, das du ausführst, löscht alle Informationen, die zeigen, in welchen Branch zusammengeführt wurde, so dass nachfolgende Berechnungen der Merge-Basis falsch sind – dcommit lässt dein git merge Ergebnis aussehen, als ob du git merge --squash ausgeführt hättest. Leider gibt es keine gute Methode, diese Situation zu vermeiden – Subversion kann diese Informationen nicht speichern, daher wirst du immer von den Einschränkungen des Systems behindert, während du es als deinen Server verwendest. Um Fehler zu vermeiden, solltest du den lokalen Branch (in diesem Fall opera) löschen, nachdem du ihn in trunk eingefügt habst.

Subversion Kommandos

Das git svn Toolset bietet eine Reihe von Befehlen, die den Übergang zu Git erleichtern, indem es einige Funktionen bereitstellt, die denen ähneln, die du aus Subversion kennst. Wir haben hier ein paar Befehle, mit denen du das bekommst, was Subversion vorher konnte.

Verlauf im SVN-Format

Wenn du an Subversion gewöhnt bist und deinen Verlauf im SVN-Stil sehen möchtest, kannst du git svn log ausführen, um deinen Commit-Verlauf in SVN-Formatierung anzuzeigen:

$ 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

Du solltest zwei wichtige Dinge über git svn log wissen. Erstens funktioniert es offline, im Unterschied zum echten svn log Befehl, der den Subversion-Server nach den Daten fragt. Zweitens zeigt es dir nur Commits an, die zum Subversion-Server übertragen wurden. Lokale Git-Commits, die du noch nicht mit dcommit bestätigt hast, werden nicht angezeigt; ebenso wenig wie Commits, die von Leuten in der Zwischenzeit auf dem Subversion-Server gemacht wurden. Es ist mehr wie der letzte bekannte Zustand der Commits auf dem Subversion-Server.

SVN Annotation

So wie der Befehl git svn log den Befehl svn log offline simuliert, kannst du das Äquivalent von svn annotate abrufen, indem du git svn blame [FILE] ausführst. Die Ausgabe sieht wie folgt aus:

$ 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

Nochmal: Auch hier werden keine Commits angezeigt, die du lokal in Git gemacht hast oder die in der Zwischenzeit in Subversion verschoben wurden.

SVN Server-Information

Wenn du git svn info ausführst, kannst du die gleiche Art von Informationen erhalten, die dir svn info liefert:

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

Das ist wie bei blame und log, denn es läuft offline und ist nur ab der letzten Kommunikation mit dem Subversion-Server auf dem neuesten Stand.

Ignorieren, was Subversion ignoriert

Wenn du ein Subversion-Repository klonst, in dem irgendwo svn:ignore Eigenschaften gesetzt sind, wirst du wahrscheinlich entsprechende .gitignore Dateien setzen wollen, damit du nicht versehentlich Dateien überträgst, die nicht übertragen werden sollen. git svn verfügt über zwei Befehle, um bei diesem Problem zu helfen. Der Erste ist git svn create-ignore, der automatisch entsprechende .gitignore Dateien für dich erstellt, damit sie bei deinem nächsten Commit berücksichtigt werden.

Der zweite Befehl ist git svn show-ignore, der die Zeilen nach stdout ausgibt, die du in eine .gitignore Datei einfügen musst, damit du die Ausgabe in die Ausschlussdatei deines Projekts umleiten kannst:

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

Auf diese Weise überhäufst du das Projekt nicht mit .gitignore Dateien. Das ist eine gute Option, wenn du der einzige Git-Benutzer in einem Subversion-Team bist und deine Teamkollegen keine .gitignore Dateien im Projekt haben wollen.

git svn Zusammenfassung

Die git svn Tools sind nützlich, wenn du gezwungenermaßen mit einem Subversion-Server arbeiten musst. Du solltest es jedoch als verkapptes Git betrachten, oder du wirst Probleme in der Umsetzung haben, die dich und deine Mitstreiter verwirren könnten. Um keine Schwierigkeiten zu bekommen, versuche dich an diese Hinweise zu halten:

  • Führe einen linearen Git-Verlauf, der keine Merge-Commits von git merge enthält. Rebase alle Arbeiten, die du außerhalb deines Haupt-Branchs durchführst, wieder in diesen ein; merge sie nicht.

  • Richte keinen separaten Git-Server ein und arbeite nicht mit einem zusammen. Möglicherweise hast du einen, um Klone für neue Entwickler zu starten, aber pushe nichts, was nicht über einen git-svn-id Eintrag verfügt. Du kannst eventuell einen pre-receive Hook hinzufügen, der jede Commit-Nachricht auf einen git-svn-id überprüft und Pushes, die Commits ohne ihn enthalten, ablehnt.

Wenn du diese Leitlinien befolgst, kann die Arbeit mit einem Subversion-Server leichter umsetzbar sein. Mit einem Umstieg auf einen echten Git-Server kann dein Team erheblich mehr an Effizienz gewinnen.

Git und Mercurial

Das DVCS-Universum besteht nicht nur aus nur Git. In diesem Bereich gibt es viele andere Systeme, jedes hat seinen eigenen Ansatz, wie eine verteilte Versionskontrolle zu funktionieren hat. Neben Git ist Mercurial am populärsten und die beiden sind sich in vielerlei Hinsicht sehr ähnlich.

Die gute Nachricht, wenn du Gits clientseitiges Verhalten bevorzugst, aber mit einem Projekt arbeitest, dessen Quellcode mit Mercurial verwaltet wird, dann ist es möglich, Git als Client für ein von Mercurial gehostetes Repository zu verwenden. Da die Art und Weise, wie Git über Remotes mit Server-Repositorys kommuniziert, sollte es nicht überraschen, dass diese Bridge als Remote-Helfer implementiert ist. Der Name des Projekts lautet git-remote-hg und ist unter https://github.com/felipec/git-remote-hg zu finden.

git-remote-hg

Zuerst musst du git-remote-hg installieren. Im Wesentlichen geht es darum, die Datei irgendwo in deinem Pfad abzulegen, so wie hier:

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

…vorausgesetzt (in einer Linux-Umgebung), ~/bin ist in deinem $PATH. Git-remote-hg hat noch eine weitere Abhängigkeit: die mercurial Library für Python. Wenn du Python schon installiert hast, ist das einfach:

$ pip install mercurial

Wenn du Python noch nicht installiert hast, besuche https://www.python.org/ und installiere es.

Als Letztes brauchst du den Mercurial-Client. Gehe zu https://www.mercurial-scm.org/ und installiere ihn, falls du es noch nicht getan hast.

Jetzt bist du bereit zum abrocken. Alles, was du benötigst, ist ein Mercurial-Repository, auf das du zugreifen kannst. Glücklicherweise kann sich jedes Mercurial-Repository so verhalten. Also verwenden wir einfach das „hello world“-Repository, das jeder benutzt, um Mercurial zu lernen:

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

Erste Schritte

Nun, da wir über ein geeignetes „serverseitiges“ Repository verfügen, können wir einen typischen Workflow durchlaufen. Wie du sehen wirst, sind diese beiden Systeme ähnlich genug, dass es keine großen Abweichungen gibt.

Wie immer mit Git, wir klonen zuerst:

$ 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

Wie du siehst, verwendet man bei der Arbeit mit einem Mercurial-Repository den Standardbefehl git clone. Das liegt daran, dass git-remote-hg auf einem relativ niedrigen Level arbeitet und einen ähnlichen Mechanismus verwendet, wie es die Implementierung des HTTP/S-Protokolls in Git ist (Remote-Helfer). Da Git und Mercurial beide so konzipiert sind, dass jeder Client eine vollständige Kopie der Repository-Historie hat, erstellt dieser Befehl relativ schnell einen vollständigen Klon, einschließlich der gesamten Projekthistorie.

Der log-Befehl zeigt zwei Commits, von denen der letzte von einer ganzen Reihe von Refs angeführt wird. Wie sich herausstellt, sind einige davon nicht wirklich da. Werfen wir einen Blick darauf, was sich wirklich im .git Verzeichnis befindet:

$ 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 versucht sich idiomatisch (begrifflich) an Git anzunähern, aber im Hintergrund verwaltet es die konzeptionelle Zuordnung zwischen zwei leicht unterschiedlichen Systemen. Im Verzeichnis refs/hg werden die aktuellen Remote-Referenzen gespeichert. Zum Beispiel ist die refs/hg/origin/branches/default eine Git ref-Datei, die das SHA-1 enthält und mit „ac7955c“ beginnt. Das ist der Commit, auf den master zeigt. Das Verzeichnis refs/hg ist also eine Art gefälschtes refs/remotes/origin, aber es unterscheidet zusätzlich zwischen Lesezeichen und Branches.

Die Datei notes/hg ist der Ausgangspunkt dafür, wie git-remote-hg Git-Commit-Hashes auf Mercurial-Changeset-IDs abbildet. Lass uns ein wenig experimentieren:

$ 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

So zeigt refs/notes/hg auf einen Verzeichnisbaum, der in der Git-Objektdatenbank eine Liste anderer Objekte mit Namen ist. git ls-tree gibt Modus, Typ, Objekt-Hash und Dateiname für Elemente innerhalb eines Baums aus. Sobald wir uns auf eines der Baumelemente festgelegt haben, stellen wir fest, dass sich darin ein „ac9117f“ Blob (der SHA-1-Hash des Commit, auf den master zeigt) befindet. Inhaltlich ist er identisch mit „0a04b98“ (das ist die ID des Mercurial-Changesets am Ende des default Branch).

Die gute Nachricht ist, dass wir uns darüber meistens keine Sorgen machen müssen. Der typische Arbeitsablauf unterscheidet sich nicht wesentlich von der Arbeit mit einem Git-Remote.

Noch eine Besonderheit, um die wir uns kümmern sollten, bevor wir fortfahren: Das Ignorieren. Mercurial und Git verwenden dafür einen sehr ähnlichen Mechanismus, aber es ist durchaus möglich, dass du eine .gitignore Datei nicht wirklich in ein Mercurial Repository übertragen willst. Glücklicherweise hat Git eine Möglichkeit, Dateien zu ignorieren, die lokal in einem On-Disk-Repository liegen. Das Mercurial-Format ist kompatibel mit Git, so dass du sie nur kopieren musst:

$ cp .hgignore .git/info/exclude

Die Datei .git/info/exclude verhält sich wie eine .gitignore, wird aber nicht in den Commits aufgenommen.

Workflow

Nehmen wir an, wir haben einige Arbeiten erledigt und einige Commits auf den master Branch gemacht und du bist so weit, ihn in das Remote-Repository zu pushen. Nun sieht unser Repository momentan so aus:

$ 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

Unser master Branch ist zwei Commits vor dem origin/master, aber diese beiden Commits existieren nur auf unserem lokalen Rechner. Schauen wir mal nach, ob jemand anderes zur gleichen Zeit wichtige Arbeit geleistet hat:

$ 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

Da wir das --all Flag verwendet haben, sehen wir die „notes“ Refs, die intern von git-remote-hg verwendet werden, die wir aber ignorieren können. Den Rest haben wir erwartet. origin/master ist um einen Commit fortgeschritten. Unser Verlauf hat sich dadurch verändert. Anders als bei anderen Systemen, mit denen wir in diesem Kapitel arbeiten, ist Mercurial in der Lage, Merges zu verarbeiten, so dass wir nichts Ausgefallenes tun müssen.

$ 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

Perfekt. Wir führen die Tests durch und alles passt, also sind wir so weit, dass wir unsere Arbeit mit dem Rest des Teams teilen können:

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

Das wars! Wenn du einen Blick auf das Mercurial-Repository wirfst, wirst du feststellen, dass genau das passiert ist, was wir erwarten haben:

$ 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

Das Change-Set mit der Nummer 2 wurde von Mercurial vorgenommen, und die Change-Sets mit der Nummer 3 und 4 wurden von git-remote-hg durchgeführt, indem Commits mit Git gepusht wurden.

Branches und Bookmarks

Git hat nur eine Art von Branch: eine Referenz, die sich verschiebt, wenn Commits gemacht werden. In Mercurial wird diese Art von Referenz als „bookmark“ (dt. Lesezeichen) bezeichnet, und sie verhält sich ähnlich wie ein Git-Branch.

Das Konzept von Mercurial eines „Branchs“ ist schwergewichtiger. Der Branch, auf den ein Changeset durchgeführt wird, wird zusammen mit dem Changeset aufgezeichnet, d.h. er befindet sich immer im Repository-Verlauf. Hier ist ein Beispiel für einen Commit, der auf dem develop Branch gemacht wurde:

$ 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

Achte auf die Zeile, die mit „branch“ beginnt. Git kann das nicht wirklich nachahmen (und muss es auch nicht; beide Arten von Branches können als Git ref dargestellt werden), aber git-remote-hg muss den Unterschied erkennen, denn für Mercurial ist er wichtig.

Das Anlegen von Mercurial-Lesezeichen ist so einfach wie das Erstellen von Git-Branches. Auf der Seite vom Git machst du:

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

Das ist alles, was es dazu zu sagen gibt. Auf der Mercurial-Seite sieht es dann folgendermaßen aus:

$ 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

Beachte den neuen [featureA] Tag auf Revision 5. Diese verhalten sich genau wie Git-Branches auf der Git-Seite, mit einer Ausnahme: Du kannst ein Lesezeichen auf der Git-Seite nicht löschen (das ist eine Einschränkung des Remote-Helfers).

Du kannst auch an einem „schwergewichtigen“ Mercurial-Branch arbeiten: Bringe einfach einen Branch in den branches Namensraum:

$ 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

So sieht das dann auf der Mercurial-Seite aus:

$ 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'
[...]

Der Branch-Name „permanent“ wurde mit dem Change-Set 7 eingetragen.

Seitens von Git ist die Arbeit mit einem dieser Branch-Stile die gleiche: Einfach auschecken, committen, fetchen, mergen, pullen, und pushen, wie du es normalerweise auch machen würdest. Eine Sache, die du wissen solltest, ist, dass Mercurial das Überschreiben der Historie nicht unterstützt, sondern nur hinzufügt. Das Mercurial-Repository sieht nach einem interaktiven Rebase und einem Force-Push so aus:

$ 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

Die Changesets 8, 9 und 10 wurden angelegt und gehören zum permanent Branch, aber die alten Changesets sind immer noch vorhanden. Das kann für deine Teamkollegen, die Mercurial verwenden, sehr verwirrend sein, also versuche es zu vermeiden.

Mercurial Zusammenfassung

Git und Mercurial sind sich ähnlich genug, um über die eigene Umgebung hinaus schmerzlos miteinander zu arbeiten. Wenn du vermeidest, den Verlauf zu ändern, der deinen Computer bereits verlassen hat (was allgemein empfohlen wird), merkst du wahrscheinlich nicht einmal, dass am anderen Ende Mercurial verwendet wird.

Git und Perforce

Perforce ist ein sehr beliebtes Versionskontrollsystem in Unternehmen. Es existiert seit 1995 und ist damit das älteste in diesem Kapitel behandelte System. Altersbedingt hat das Konzept aus heutiger Sicht einige Einschränkungen. Es geht davon aus, dass du immer mit einem einzigen zentralen Server verbunden bist und nur eine Version auf der lokalen Festplatte gespeichert ist. Sicherlich sind seine Funktionen und Einschränkungen gut für einige spezielle Probleme geeignet, aber es gibt viele Projekte mit Perforce, bei denen Git möglicherweise besser geeignet wäre.

Es gibt zwei Möglichkeiten, wenn du Perforce und Git zusammen nutzen möchtest. Als erstes stellen wir die „Git Fusion“ Bridge des Herstellers von Perforce vor, mit der du Teilbäume deines Perforce-Depots als Read-Write-Git-Repository freigeben kannst. Bei der zweiten handelt es sich um git-p4, eine client-seitige Bridge, mit der du Git als Perforce-Client verwenden kannst, ohne dass der Perforce-Server neu konfiguriert werden muss.

Git Fusion

Preforce bietet mit Git Fusion ein Produkt (verfügbar unter https://www.perforce.com/git-fusion), das einen Perforce-Server mit Git-Repositorys auf der Serverseite synchronisiert.

Git Fusion einrichten

Für unsere Beispiele verwenden wir die einfachste Installationsmethode für Git Fusion, indem wir eine virtuelle Maschine herunterladen, auf der der Perforce-Daemon und Git Fusion laufen. Du kannst das Image der virtuellen Maschine von https://www.perforce.com/downloads/Perforce/20-User herunterladen. Wenn der Download abgeschlossen ist, importierst du es in deiner bevorzugten Virtualisierungssoftware (wir verwenden VirtualBox).

Beim ersten Start des Rechners wirst du aufgefordert, das Passwort für drei Linux-Benutzer (root, perforce, git) festzulegen und einen Instanznamen anzugeben, mit dem du diese Installation von anderen im selben Netzwerk unterscheiden kannst. Wenn das alles abgeschlossen ist, solltest du folgendes sehen:

Boot-Bildschirm der virtuellen Maschine von Git Fusion
Abbildung 171. Boot-Bildschirm der virtuellen Maschine von Git Fusion

Du solltest die hier angezeigte IP-Adresse notieren, wir werden sie später benutzen. Als nächstes erstellen wir einen Perforce-Benutzer. Wähle unten die Option „Login“ und drücke die Eingabetaste (oder verbinde dich per SSH mit dem Computer) und melde dich als root an. Verwende anschliessend folgenden Befehle, um einen Benutzer anzulegen:

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

Der erste öffnet einen VI-Editor, um den Benutzer zu personalisieren. Du kannst auch die Vorgaben übernehmen, indem du :wq eingibst und auf Enter drückst. Der zweite wird dich auffordern, ein Passwort zweimal einzugeben. Das ist alles, was wir mit einem Shell-Prompt zu tun haben werden, also beende die Sitzung.

Als nächstes musst du Git mitteilen, dass es keine SSL-Zertifikate überprüfen soll. Das Git Fusion-Image wird mit einem Zertifikat geliefert, aber es bezieht sich auf eine Domäne, die nicht mit der IP-Adresse deiner virtuellen Maschine übereinstimmt, weshalb Git die HTTPS-Verbindung abweisen würde. Wenn dies eine permanente Installation sein soll, lies das Handbuch von Perforce Git Fusion, um ein anderes Zertifikat zu installieren. Für unsere Beispielzwecke genügt diese Angabe:

$ export GIT_SSL_NO_VERIFY=true

Jetzt können wir testen, ob alles funktioniert.

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

Das Virtual-Machine-Image ist mit einem Beispielprojekt ausgestattet, das du klonen kannst. Hier klonen wir über HTTPS, mit dem Benutzer john, den wir oben erstellt haben. Git fragt nach Anmeldeinformationen für diese Verbindung, aber der Credential-Cache erlaubt es uns, diesen Schritt für alle nachfolgenden Anfragen zu überspringen.

Fusion Konfiguration

Sobald du Git Fusion installiert hast, solltest du die Konfiguration anpassen. Mit deinem favorisierten Perforce-Client ist das ganz einfach. Weise das Verzeichnis //.git-fusion auf dem Perforce-Server einfach deinem Arbeitsbereich zu. Die Dateistruktur sieht wie folgt aus:

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

498 directories, 287 files

Das Verzeichnis objects wird intern von Git Fusion verwendet, um Perforce-Objekte auf Git abzubilden und umgekehrt, so dass du es ignorieren kannst. In diesem Verzeichnis gibt es eine globale p4gf_config Datei sowie eine für jedes Repository – das sind die Konfigurationsdateien, die das Verhalten von Git Fusion bestimmen. Werfen wir einen Blick auf die Datei im Root:

[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

Wir werden hier nicht auf die Bedeutung all dieser Flags eingehen. Bedenke jedoch, dass es sich hierbei nur um eine INI-formatierte Textdatei handelt, ähnlich wie bei der Konfiguration mit Git. Diese Datei legt die globalen Optionen fest, die dann von repository-spezifischen Konfigurationsdateien wie repos/Talkhouse/p4gf_config überschrieben werden können. Wenn du diese Datei öffnest, siehst du einen Abschnitt [@repo] mit einigen Einstellungen, die sich von den globalen Standardeinstellungen unterscheiden. Du wirst auch Abschnitte sehen, die so aussehen:

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

Dabei handelt es sich um eine Zuordnung zwischen einem Perforce-Branch und einem Git-Branch. Der Abschnitt kann beliebig benannt werden, solange der Name eindeutig ist. Der git-branch-name ermöglicht dir, einen Depot-Pfad, der unter Git umständlich wäre, in einen benutzerfreundlicheren Namen zu konvertieren. Die Anzeige-Einstellung steuert, wie Perforce-Dateien in das Git-Repository mit Hilfe der Standard-Syntax für das View-Mapping abgebildet werden. Es kann mehr als ein Mapping angegeben werden, wie in diesem Beispiel:

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

Wenn dein normales Workspace-Mapping Änderungen in der Struktur der Verzeichnisse enthält, kannst du das auf diese Weise mit einem Git-Repository replizieren.

Die letzte Datei, die wir hier behandeln, ist users/p4gf_usermap, die Perforce-Benutzer auf Git-Benutzer abbildet und die du möglicherweise nicht einmal benötigst. Bei der Konvertierung eines Perforce Change-Sets in einen Git Commit sucht Git Fusion standardmäßig nach dem Perforce-Benutzer und verwendet die dort gespeicherte E-Mail-Adresse und den vollständigen Namen für das Autor/Committer-Feld in Git. Bei der umgekehrten Konvertierung wird standardmäßig der Perforce-Benutzer mit der E-Mail-Adresse gesucht, die im Autorenfeld des Git-Commits gespeichert ist, und das Änderungsset als dieser Benutzer übermittelt (mit entsprechenden Berechtigungen). In den meisten Fällen wird dieses Verhalten gut funktionieren, aber beachte die folgende Mapping-Datei:

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"

Jede Zeile hat das Format <user> <email> "<full name>" und erstellt eine einzige Benutzerzuordnung. Die beiden ersten Zeilen ordnen zwei verschiedene E-Mail-Adressen demselben Perforce-Benutzerkonto zu. Das ist praktisch, wenn du Git-Commits unter mehreren verschiedenen E-Mail-Adressen erstellt hast (oder sich deine E-Mail-Adresse ändert), diese aber dem gleichen Perforce-Benutzer zugeordnet werden sollen. Beim Erstellen eines Git-Commits aus einem Perforce Change-Set wird die erste Zeile, die dem Perforce-Benutzer entspricht, für die Angaben zur Git-Autorschaft verwendet.

Die letzten beiden Zeilen überdecken Bob und Joe’s tatsächliche Namen und E-Mail-Adressen aus den Git-Commits, die erstellt werden. Das ist sinnvoll, wenn du ein internes Projekt open-source-fähig machen willst, aber dein Mitarbeiterverzeichnis nicht auf der ganzen Welt veröffentlichen willst. Beachte, dass die E-Mail-Adressen und vollständigen Namen eindeutig sein sollten, es sei denn, du möchtest alle Git-Commits einem einzigen fiktiven Autor zuordnen.

Workflow (Arbeitsablauf)

Perforce Git Fusion ist eine bidirektionale Brücke zwischen der Perforce- und Git-Versionskontrolle. Betrachten wir die Arbeit von der Git-Seite aus. Wir gehen davon aus, dass wir im Projekt „Jam“ mit einer oben gezeigten Konfigurationsdatei gemapped sind, die wir so klonen können:

$ git clone https://10.0.1.254/Jam
Cloning into 'Jam'...
Username for 'https://10.0.1.254': john
Password for 'https://john@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.
[...]

Wenn du das zum ersten Mal machst, kann es einige Zeit in Anspruch nehmen. Git Fusion konvertiert alle anwendbaren Changesets in der Perforce-Historie in Git-Commits. Das passiert lokal auf dem Server, ist also relativ schnell, aber wenn man einen langen Verlauf hat, kann es trotzdem einige Zeit dauern. Nachfolgende Fetches führen eine inkrementelle Konvertierung durch, so dass es sich schon eher wie die native Geschwindigkeit von Git anfühlt.

Wie du siehst, sieht unser Repository genauso aus wie jedes andere Git-Repository, mit dem du arbeiten kannst. Es gibt drei Branches. Git hat einen lokalen master Branch erstellt, der origin/master tracked. Wir werden ein wenig arbeiten und ein paar neue Commits erstellen:

# ...
$ 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.
[...]

Wir haben zwei neue Commits. Nun lass uns überprüfen, ob jemand anderes auch daran gearbeitet hat:

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

Anscheinend hat jemand das tatsächlich getan! Du würdest es aus dieser Sicht nicht erkennen, aber der 6afeb15 Commit wurde mit einem Perforce Client erstellt. Es sieht aus der Perspektive von Git wie ein weiterer Commit aus. Das ist genau der Punkt. Betrachten wir, wie der Perforce-Server mit einem Merge-Commit umgeht:

$ 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 glaubt, dass es funktioniert hat. Werfen wir einen Blick auf den Verlauf der README Datei aus der Perspektive von Perforce, indem wir die Revisionsgraphenfunktion von p4v verwenden:

Perforce-Revisionsgraph resultierend aus dem Git-Push
Abbildung 172. Perforce-Revisionsgraph resultierend aus dem Git-Push

Wenn du diese Darstellung noch nie zuvor gesehen hast, mag sie verwirrend erscheinen, aber sie zeigt die gleichen Konzepte wie ein grafischer Viewer für die Git-Historie. Wir betrachten die Historie der README Datei, so dass der Verzeichnisbaum oben links nur diese Datei anzeigt, wenn sie in verschiedenen Branches auftaucht. Oben rechts haben wir ein Diagramm, das veranschaulicht, wie verschiedene Revisionen der Datei zusammenhängen, und die vergrößerte Ansicht dieses Diagramms befindet sich unten rechts. Der Rest der Darstellung wird der Detailansicht für die ausgewählte Revision (in diesem Fall 2) übergeben.

Auffällig ist, dass die Grafik genau so aussieht wie die in dem Verlauf von Git. Perforce hatte keinen namentlich benannten Branch, um die Commits 1 und 2 zu speichern. Also wurde ein „anonymer“ Branch im .git-fusion Verzeichnis erstellt, um sie zu speichern. Das gilt auch für benannte Git-Branches, die keinem benannten Perforce-Branch entsprechen (Du kannst sie später über die Konfigurationsdatei einem Perforce-Branch zuordnen).

Das meiste geschieht hinter den Kulissen, und das Ergebnis ist, dass eine Person in der Gruppe Git und eine andere Perforce verwenden kann wobei keine von ihnen von der Entscheidung der anderen Person weiß.

Git-Fusion Zusammenfassung

Wenn du Zugang zu deinem Perforce-Server hadt (oder erhalten kannst), ist Git Fusion eine gute Möglichkeit, Git und Perforce zum gegenseitigen Austausch zu bewegen. Es ist ein wenig Konfiguration erforderlich, aber die Lernkurve ist nicht sehr steil. Dieses ist einer der wenigen Abschnitte in diesem Kapitel, in denen Warnungen über die Verwendung von Gits voller Leistung nicht erscheinen. Das heißt nicht, dass Perforce mit allem, was du ihm zumutest, zufrieden sein wirst – wenn du versuchst, eine bereits gepushte Historie neu zu schreiben, wird Git Fusion sie ablehnen – aber Git Fusion gibt sich sehr große Mühe, sich nativ anzufühlen. Du kannst sogar Git-Submodule verwenden (obwohl sie für Perforce-Anwender seltsam aussehen werden) und Branches mergen (das wird als Integration auf der Perforce-Seite erfasst).

Wenn du den Administrator deines Servers nicht davon überzeugen kannst, Git Fusion einzurichten, gibt es noch eine weitere Möglichkeit, diese Tools gemeinsam zu nutzen.

Git-p4

Git-p4 ist eine bidirektionale Brücke zwischen Git und Perforce. Es läuft vollständig in deinem Git-Repository, so dass du keinen Zugriff auf den Perforce-Server benötigst (mit Ausnahme der Benutzer-Anmeldeinformationen). Git-p4 ist nicht so flexibel und keine Komplettlösung wie Git Fusion, aber es ermöglicht dir, das meiste von dem zu tun, was du tun möchtest, ohne die Serverumgebung zu beeinträchtigen.

Anmerkung

Du brauchst das p4 Tool an einer beliebigen Stelle in deinem PATH, um mit git-p4 zu arbeiten. Zur Zeit ist es unter https://www.perforce.com/downloads/Perforce/20-User frei verfügbar.

Einrichtung

So werden wir beispielsweise den Perforce-Server wie oben gezeigt von der Git Fusion OVA (Open-Virtualization-Archive-Datei) aus verwenden, aber wir umgehen den Git Fusion-Server und gehen direkt zur Perforce-Versionskontrolle.

Um den von git-p4 benötigten Befehlszeilen-Client p4 verwenden zu können, musst du ein paar Umgebungsvariablen setzen:

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

Wie bei allem in Git ist das Klonen der erste Befehl:

$ 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

Dadurch entsteht ein Git-technisch „flacher“ Klon. Nur die allerletzte Perforce-Revision wird in Git importiert. Denke daran, Perforce ist nicht dazu gedacht, jedem Benutzer alle Revisionen zu übergeben. Das ist ausreichend, um Git als Perforce-Client zu verwenden, ist aber für andere Zwecke ungeeignet.

Sobald es abgeschlossen ist, haben wir ein voll funktionsfähiges 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

Beachte, dass es für den Perforce-Server einen „p4“ Remote gibt, aber alles andere sieht aus wie ein Standardklon. Eigentlich ist das etwas irreführend; es gibt dort nicht wirklich einen Remote.

$ git remote -v

In diesem Repository existieren überhaupt keine Remotes. Git-p4 hat einige Referenzen erstellt, um den Zustand des Servers darzustellen. Für git log sehen sie aus wie Remote-Referenzen, aber sie werden nicht von Git selbst verwaltet. Man kann nicht zu ihnen pushen.

Workflow

Okay, lass uns ein paar Arbeiten erledigen. Nehmen wir an, du hast einige Änderungen bei einem sehr wichtigen Feature gemacht und bist bereit, es dem Rest deines Teams zu zeigen.

$ 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

Wir haben zwei neue Commits erstellt, die wir an den Perforce-Server übermitteln können. Schauen wir mal, ob heute noch jemand anderes Änderungen gemacht hat:

$ 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

Tatsächlich wurde etwas geändert. Die Branches master und p4/master sind auseinander gelaufen. Das Branching-System von Perforce ist nicht wie das von Git, so dass das Übertragen von Merge-Commits keinen Sinn macht. Git-p4 empfiehlt, dass du deine Commits rebased und bietet sogar eine Kurzform dafür:

$ 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(-)

Vermutlich kannst du das an der Ausgabe erkennen: git p4 rebase ist eine Abkürzung für git p4 sync gefolgt von git rebase p4/master. Das ist zwar noch ein bisschen cleverer, besonders bei der Arbeit mit mehreren Branches, ist aber eine gute Annäherung.

Jetzt ist unser Verlauf wieder linear und bereit, in Perforce eingereicht zu werden. Der Befehl git p4 submit versucht, für jeden Git-Commit zwischen p4/master und master, eine neue Perforce-Revision zu erstellen. Beim Ausführen werden wir in unseren bevorzugten Editor weitergeleitet, der Inhalt der Datei sieht dann ungefähr so aus:

# 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>

Das ist im Wesentlichen derselbe Inhalt, den du bei der Ausführung von p4 submit sehen würdest. Ausgenommen sind die Dinge am Ende, die git-p4 sinnvollerweise mit aufgenommen hat. Git-p4 versucht, deine Git- und Perforce-Einstellungen individuell zu berücksichtigen, wenn es einen Namen für ein Commit- oder Changeset angeben muss. In einigen Fällen willst du ihn eventuell überschreiben. Wenn beispielsweise der Git-Commit, den du importierst, von einem Mitwirkenden geschrieben wurde, der kein Perforce-Benutzerkonto hat, kannst du trotzdem wollen, dass sich das daraus ergebende Changeset so aussieht, als hätte er es geschrieben (und nicht du).

Git-p4 hat die Nachricht aus dem Git-Commit zweckmäßigerweise als Inhalt für dieses Perforce Changeset importiert. Alles, was wir tun müssen, ist zweimal speichern (einmal für jeden Commit) und beenden. Die resultierende Shell-Ausgabe sieht in etwa so aus:

$ 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

Das Ergebnis sieht so aus, als ob wir gerade ein git push gemacht hätten. Das kommt dem, was tatsächlich passiert sehr nahe.

Beachte, dass während dieses Prozesses jeder Git-Commit in einen Perforce Changeset umgewandelt wird. Wenn du ihn in einen einzelnen Changeset zusammenfassen möchtest, kannst du dies mit einem interaktiven Rebase tun, bevor du git p4 submit ausführst. Bitte bedenke, dass die SHA-1-Hashes aller Commits, die als Changesets eingereicht wurden, sich geändert haben. Der Grund dafür ist, dass git-p4 am Ende jedes Commits, den es konvertiert, eine Zeile hinzufügt:

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

Was passiert, wenn du versuchst, einen Merge-Commit einzureichen? Versuchen wir es einmal. Dies ist der Zustand, in die wir jetzt sind:

$ 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

Der Verlauf von Git und Perforce weicht nach 775a46f voneinander ab. Die Git-Seite hat zwei Commits, dann einen Merge-Commit mit dem Perforce Head, dann einen weiteren Commit. Wir werden versuchen, diese auf einem einzelnen Changeset auf der Perforce-Seite einzureichen. Schauen wir mal was passiert, wenn wir versuchen, ihn jetzt einzubringen:

$ 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

Das -n Flag ist die Abkürzung für --dry-run. Das bedeutet der Befehlt wir nur simuliert und nicht wirklich ausgeführt. Es erfolgt jedoch eine Ausgabe, was passieren würde, würde man ihn wirklich ausführen, was sehr hilfreich ist. Hier sieht es so aus, als hätten wir drei Perforce-Änderungssets erstellt, die den drei Nicht-Merge-Commits entsprechen, die noch nicht auf dem Perforce-Server vorhanden sind. Das hört sich nach genau dem an, was wir wollen. Lass es uns jetzt wirklich ausführen und sehen was passiert:

$ 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

Unsere Historie wurde linearisiert, genau so, als hätten wir vor dem Einreichen rebased, was auch tatsächlich passiert ist. So kannst du auf der Git-Seite selbst Branches erstellen, darauf arbeiten, verwerfen und mergen, ohne befürchten zu müssen, dass dein Verlauf sich mit Perforce nicht mehr verträgt. Wenn du einen Rebase durchführen kannst, ist es auch möglich, ihn zu einem Perforce-Server beizusteuern.

Branching

Wenn dein Perforce-Projekt mehrere Branches hat, hast du Glück gehabt. git-p4 kann damit so umgehen, dass es sich wie Git anfühlt. Angenommen, dein Perforce-Depot ist so aufgebaut:

//depot
  └── project
      ├── main
      └── dev

Nehmen wir weiter an, du hast einen dev Branch, der eine View-Spezifikation hat, die so aussieht:

//depot/project/main/... //depot/project/dev/...

Git-p4 kann diese Situation automatisch erkennen und das Richtige tun:

$ 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

Beachte den „@all“ Spezifikator im Depotpfad, der git-p4 anweist, nicht nur das neueste Changeset für diesen Teilbaum, sondern alle Changesets, die jemals diese Pfade beeinflusst haben, zu klonen. Das ist näher an Gits Konzept des Klonens, aber wenn du an einem Projekt mit einer langen Historie arbeitest, könnte es einige Zeit dauern den Klon zu kopieren.

Das --detect-branches Flag weist git-p4 an, die Branch-Spezifikationen von Perforce zu verwenden, um die Branches den Git refs zuzuordnen. Sind diese Zuordnungen nicht auf dem Perforce-Server vorhanden (was eine absolut zulässige Methode zur Verwendung von Perforce ist), kannst du git-p4 mitteilen, wie die Zuordnung der Branches zu sein hat. Du erhältst dann das gleiche Ergebnis:

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

Das Setzen der Konfigurationsvariablen git-p4.branchList auf main:dev teilt git-p4 mit, dass „main“ und „dev“ beides Branches sind, wobei der zweite ein untergeordnetes Element des ersten ist.

Machen wir jetzt neben git checkout -b dev p4/project/dev noch einige Commits, dann ist git-p4 klug genug, um den richtigen Branch anzusprechen, wenn wir anschließend git p4 submit ausführen. Leider kann git-p4 keine flachen Klone mit mehreren Branches mischen. Wenn du ein großes Projekt hast und an mehr als einem Branch arbeiten willst, musst du jeden Branch, den du einreichen möchtest, einmal mit git p4 clone erstellen.

Für die Erstellung oder Integration von Branches musst du einen Perforce-Client verwenden. Git-p4 kann nur synchronisieren und an bestehende Branches senden und es kann nur einen linearen Changeset auf einmal durchführen. Bei dem Mergen zweier Branches in Git und dem Versuch, das neue Changeset einzureichen, wird nur ein Bündel von Dateiänderungen aufgezeichnet. Die Metadaten über die an der Integration beteiligten Branches gehen dabei verloren.

Git und Perforce, Zusammenfassung

Git-p4 ermöglicht die Verwendung eines Git-Workflows mit einem Perforce-Server und ist darin ziemlich gut. Es ist jedoch wichtig, sich klar zu machen, dass Perforce für den Quellcode verantwortlich ist und du Git nur für die lokale Arbeit verwendest. Sei einfach sehr vorsichtig bei der Weitergabe von Git-Commits. Wenn du einen Remote hast, den auch andere Benutzer verwenden, pushe keine Commits, die zuvor noch nicht an den Perforce-Server übertragen wurden.

Möchtest du die Verwendung von Perforce und Git als Clients für die Versionskontrolle frei kombinieren, dann musst du den Server-Administrator davon überzeugen, Git zu installieren. Git Fusion macht die Verwendung von Git zu einem erstklassigen Versionskontroll-Client für einen Perforce-Server.

scroll-to-top