Die Community zu .NET und Classic VB.
Menü

Versionsverwaltung mit Mercurial (Teil 2)

 von 

Rückblick und Ausblick 

Im ersten Teil wurde das zentrale Handwerk für die Arbeit mit Mercurial vermittelt. Dieser Teil geht nun auf das Zusammenspiel mehrerer verschiedener Repositories ein, wie sie gerade bei Entwicklungsteams häufig vorkommen.

Für das grundsätzliche Verständnis einiger Zusammenhänge sollte hierbei jedoch erst der Unterschied zwischen zwei Arten von SCMs geklärt werden: zentralisierte und verteilte Systeme.

Zentralisierte Systeme  

Zu den zentralisierten Systemen zählen beispielsweise CVS und der Nachfolger SVN (Subversion). Bei diesen Systemen ist ein Server involviert, auf welchem das Repository mit den Daten der Historie gespeichert ist. Die Clients speichern lokal keine Änderungen, sondern nur die Arbeitskopie.

Sollten zwei Benutzer gleichzeitig an der selben Datei arbeiten, muss sich der Server um die Verschmelzung, den Merge der beiden Versionen kümmern. Dies passiert automatisch, so lange die Änderungen ausreichend weit entfernt sind, ansonsten ist manuelle Intervention notwendig.

Verteilte Systeme  

Verteilte Systeme, mit den Vertretern Mercurial, Git und einigen anderen, basieren auf einem anderen Prinzip. Es ist nicht zwingend ein Server erforderlich, sondern die Daten des Repos sind auf die Clients verteilt. Jeder Benutzer besitzt entsprechend eine Arbeitskopie, sowie die Daten der gesamten Historie des Repositories. Gearbeitet wird dann bei jedem Benutzer so wie in den vorherigen Abschnitten beschrieben, doch von Zeit zu Zeit werden die Änderungen wieder zwischen den Clients synchronisiert.

Gerade bei Entwicklungsteams ist es dennoch oft angebracht, einen Server als zentrale Instanz zu haben. Dies ist selbstverständlich problemlos möglich, allerdings ist die Rolle des Servers ganz eine andere als bei zentralisierten Systemen: Im Grunde übernimmt er hier nur die Rolle als Datenspeicher. Neue Benutzer können den gesamten Inhalt (inkl. Versionsgeschichte) herunterladen, bzw. klonen. Nachdem Änderungen vorgenommen wurden, können diese vom lokalen Repository auf den Server kopiert werden. Die anderen Benutzer werden dann gelegentlich die neuesten Änderungen auf dem Server wieder in ihre Kopien einspielen.

Diese Verteilung von Daten bietet einige grosse Vorteile gegenüber zentralisierten Systemen:

  • Es gibt automatisch eine Redundanz: Ein Ausfall des Servers führt in der Regel nicht zu Datenverlust, das Repo kann einfach wieder von einem User auf den (neuen) Server kopiert werden.
  • Während der Arbeit werden alle Änderungen lokal eingecheckt. Das ist einerseits viel schneller und erfordert andererseits keine Internetverbindung, kann also beispielsweise unterwegs durchgeführt werden.
  • Es ist problemlos möglich, ein Repository in ein anderes Verzeichnis zu klonen und dort zum Beispiel einige Versuche durchzuführen, ohne die Hauptentwicklungslinie zu stören.
  • Ein Server ist nicht zwingend notwendig. Änderungen können grundsätzlich auch direkt zwischen mehreren Usern synchronisiert werden.

Gesamtes Repository klonen: hg clone  

Der erste wichtige Befehl im Zusammenhang mit verteilten Repos ist oben erwähntes klonen:

test$ ls
andere_datei.txt  neuer_name.txt
test$ cd ..
Desktop$ hg clone test klon
updating to branch default
2 files updated, 0 files merged, 0 files removed, 0 files unresolved
Desktop$ cd klon/
klon$ ls
andere_datei.txt  neuer_name.txt

hg clone benötigt als Argument einen Ordner mit einem Mercurial-Repository, sowie optional einen Zielordner für den Klon. Wird kein Zielordner angegeben, wird derselbe Name wie beim Ausgangsordner verwendet und im aktuellen Verzeichnis erstellt.

Liegt das zu klonende Repository auf einem Server, kann anstelle des Ordnernamens einfach der entsprechende URI angegeben werden:

klon$ cd ..
Desktop$ hg clone http://hb9etc.ch/hg/tut_versionsverwaltung
destination directory: tut_versionsverwaltung
requesting all changes
adding changesets
adding manifests
adding file changes
added 24 changesets with 25 changes to 3 files
updating to branch default
3 files updated, 0 files merged, 0 files removed, 0 files unresolved

Beim klonen wird auch, wie oben erwähnt, die gesamte Versionsgeschichte des Repos kopiert:

Desktop$ cd klon/
klon$ hg log -r 7:tip
changeset:   7:204c5be58c52
user:        phip
date:        Mon Jan 23 06:27:10 2012 +0100
summary:     Zeile ergänzt.

changeset:   8:614d933bb214
tag:         tip
user:        phip
date:        Mon Jan 23 06:30:22 2012 +0100
summary:     Andere Datei erstellt.

Nicht kopiert wird jedoch die Konfigurationsdatei .hg/hgrc, denn geklont wird ja häufig zwischen verschiedenen Benutzern. Da wäre es nicht sinnvoll, wenn alle Einstellungen (wie z.B. der Benutzername) des einen Users plötzlich auch bei einem anderen eingestellt wären. Stattdessen wird eine neue Konfigurationsdatei mit default als einzigem gesetztem Attribut erzeugt. Der Wert dieses Attributs zeigt auf den Pfad zum ursprünglichen Repo:

klon$ cat .hg/hgrc 
[paths]
default = /home/phip/Desktop/test

Die Bedeutung dieses Attributs wird im nächsten Abschnitt behandelt.

Änderungen verteilen: hg pull; hg push  

Üblicherweise wird in einem Entwicklungsteam auf dem zentralen Server ein Repo erstellt und dann von jedem Entwickler geklont. Anschliessend arbeitet jeder Entwickler mit seiner lokalen Kopie. Um die Änderungen nun wieder zu synchronisieren, müssen die Changesets zwischen den beteiligten Personen (und dem Server) ausgetauscht werden. Für diesen Zweck existieren die Befehle hg pull und hg push.

Der Unterschied der Befehle ist offensichtlich: hg pull zieht die Änderungen aus einem anderen Repository in das aktuelle Repo, während hg push lokale Änderungen in ein anderes Repo drückt. Auch für diese Befehle gilt: Nebst lokalen Pfadangaben können direkt URIs angegeben werden, falls sich das Ziel auf einem entfernten Rechner befinden. Beachtet werden müssen dabei jedoch die Zugriffsberechtigungen: hg pull benötigt im entfernten Repo nur Leserechte, hg push jedoch auch Schreibrechte.

hg push

Nach der Theorie nun also ein Beispiel. Zuerst benötigen wir in unserem Klon eine Änderung, damit es überhaupt erst etwas zum Synchronisieren gibt:

klon$ echo "klondatei" > klondatei.txt
klon$ hg add klondatei.txt 
klon$ hg commit -m 'klondatei.txt erstellt.'

Nun ist es an der Zeit, diese Änderung zurück in unser ursprüngliches Repo zu kopieren:

klon$ hg push ../test 
pushing to ../test
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files

Wie erwartet wurde ein Changeset mit einer Änderung in einer Datei kopiert. Das wollen wir nun kontrollieren:

klon$ cd ../test/
test$ hg log -r 8:tip
changeset:   8:614d933bb214
user:        phip
date:        Mon Jan 23 06:30:22 2012 +0100
summary:     Andere Datei erstellt.

changeset:   9:df44cfb9c97b
tag:         tip
user:        phip
date:        Fri Feb 03 18:13:28 2012 +0100
summary:     klondatei.txt erstellt.

Soweit alles ok. Oder nicht?

test$ ls -l
total 24
-rw-rw-r-- 1 phip phip 24 2012-01-23 19:16 andere_datei.txt
-rw-rw-r-- 1 phip phip 18 2012-01-25 06:06 neuer_name.txt

Offensichtlich existiert die Änderung im Repo, doch sie wurde noch nicht auf die Arbeitskopie angewendet. Dies liegt daran, dass die Arbeitskopie vom Repository getrennt existiert und in einer beliebigen Revision vorliegen kann. Dass dieses Verhalten sinnvoll ist, zeigt auch folgendes einfaches Gedankenexperiment: Angenommen, wir hätten im "test" Repo noch offene Änderungen, dann wäre es natürlich verheerend, wenn "jemand anderes" von einem anderen Repo Änderungen bei uns in die Arbeitskopie schieben könnte.

Langer Rede kurzer Sinn: Wir müssen die Arbeitskopie noch aktualisieren:

test$ hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
test$ ls -l
total 36
-rw-rw-r-- 1 phip phip 24 2012-01-23 19:16 andere_datei.txt
-rw-rw-r-- 1 phip phip 10 2012-02-03 18:23 klondatei.txt
-rw-rw-r-- 1 phip phip 18 2012-01-25 06:06 neuer_name.txt

Nun ist auch die Arbeitskopie wieder auf dem aktuellen Stand und bereit für weitere Änderungen.

hg pull

Alternativ zum Senden von Änderungen können diese natürlich auch geholt werden. Auch dazu benötigen wir erst wieder eine Änderung, diesmal im "test" Repository:

test$ hg rm klondatei.txt 
test$ hg commit -m 'klondatei.txt gelöscht.'
test$ ls -l
total 24
-rw-rw-r-- 1 phip phip 24 2012-01-23 19:16 andere_datei.txt
-rw-rw-r-- 1 phip phip 18 2012-01-25 06:06 neuer_name.txt

Im nächsten Schritt wechseln wir wieder zu unserem Klon und holen die Änderungen:

test$ cd ../klon/
klon$ hg pull
pulling from /home/phip/Desktop/test
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 0 changes to 0 files
(run 'hg update' to get a working copy)
klon$ hg update
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
klon$ ls -l
total 24
-rw-rw-r-- 1 phip phip 24 2012-02-03 17:38 andere_datei.txt
-rw-rw-r-- 1 phip phip 18 2012-02-03 17:38 neuer_name.txt

Hier zeigt sich nun auch das Verhalten des weiter oben erwähnten default-Attributs: Damit wird Mercurial mitgeteilt, von wo Änderungen standardmässig geholt werden sollen. Daher kann beim Aufruf von hg pull die Pfadangabe entfallen. Diese Funktionalität existiert auch für die Gegenrichtung, doch heisst das Attribut dann default-push.

Nach dem Ausführen von hg pull erinnert Mercurial auch daran, hg update auszuführen, was in der nächsten Zeile passiert. Und schon sind die Repos wieder synchronisiert.

Wer nun denkt, dieses elende hg update nach jedem Verteilen sei mühsam, der denkt richtig. Daher gibt es zur Zusammenfassung von hg pull und hg update die Abkürzung hg pull -u, welche zuerst ein pull und anschliessend ein update durchführt. Weiterhin sei an dieser Stelle auf die Erweiterung fetch hingewiesen, welche im Kapitel "Extensions" näher vorgestellt wird.

hg incoming und hg outgoing

hg pull und hg push haben zwei enge Verwandte: hg incoming und hg outgoing. Diese Befehle führen die selben Prüfungen wie pull und push durch, listen aber nur auf, was zu tun wäre.

Änderungen verschmelzen: hg merge  

Verschiedene Repositories mit verschiedenen Änderungen

Bisher wurde bei allen Änderungen darauf geachtet, dass immer nur die aktuelle Arbeitskopie verändert wird. Bei einem einzigen Benutzer (und einem Repository) ist dies auch die Regel, denn üblicherweise wird ja am aktuellen Stand eines Projektes weitergearbeitet. Man kann auch sagen, die Versionsgeschichte bleibe linear.

Sobald nun jedoch mehrere Benutzer involviert sind, ist das ganze nicht mehr so einfach. In zentralisierten Systemen behilft man sich damit, dass zu jedem Zeitpunkt nach wie vor nur eine aktuelle Version existiert, nämlich die auf dem Server. Bei verteilten Systemen ist keine solche "letzte Instanz" verfügbar, daher muss ein anderer Mechanismus zur Lösung dieser Anwendungsfälle gefunden werden. Doch zuerst soll an einem Beispiel gezeigt werden, worum es denn eigentlich geht:

Angenommen, unsere beiden Repos von weiter oben, "test" und "klon" sind synchron, sie beinhalten also die selben Änderungen. Nun wird in "klon" eine neue Datei erstellt:

klon$ echo 'Von "klon" erstellte Datei' > datei_von_klon.txt
klon$ ls
andere_datei.txt  datei_von_klon.txt  neuer_name.txt
klon$ hg add datei_von_klon.txt 
klon$ hg commit -m 'datei_von_klon.txt erstellt.'
klon$ hg log -r 10:
changeset:   10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 18:27:48 2012 +0100
summary:     klondatei.txt gelöscht.

changeset:   11:bce19940449e
tag:         tip
user:        phip
date:        Fri Feb 03 19:28:11 2012 +0100
summary:     datei_von_klon.txt erstellt.

Ohne Synchronisierung wird nun in "test" eine Datei gelöscht:

klon$ cd ../test/
test$ hg rm andere_datei.txt 
test$ ls
neuer_name.txt
test$ hg commit -m 'andere_datei.txt gelöscht.'
test$ hg log -r 10:
changeset:   10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 18:27:48 2012 +0100
summary:     klondatei.txt gelöscht.

changeset:   11:e5088b70e741
tag:         tip
user:        phip
date:        Fri Feb 03 19:29:40 2012 +0100
summary:     andere_datei.txt gelöscht.

Kopfversionen (heads)

Nun sollen die Repos wieder synchronisiert werden, als ersten Versuch von "test" in "klon":

test$ hg push ../klon 
pushing to ../klon
searching for changes
abort: push creates new remote head e5088b70e741!
(you should pull and merge or use push -f to force)

Was ist passiert? Mercurial weigert sich, im anderen Repo einen zweiten "head" zu erstellen. Ein "head", auch Kopfversion genannt, ist die jeweils aktuellste Version einer Änderungslinie. An dieser Stelle ist es nun notwendig, dass Changesets nicht mehr über Revisionsnummern, sondern über ihren Hash (die zwölfstellige Hexzahl) identifiziert werden. Anhand der Revisionslogs sehen wir nun Folgendes: Änderung 10 mit dem Hash c4d4768fb66b ist bei beiden Repos vorhanden und identisch. Anschliessend folgt beide Male Änderung 11, doch mit zwei verschiedenen Hashes, bce19940449e in "klon" und e5088b70e741 in "test".

Wenn wir nun versuchen, Changeset e5088b70e741 in "klon" zu pushen, merkt Mercurial, dass e5088b70e741 auf c4d4768fb66b (Revision 10) folgen muss. In "klon" ist dieser Platz jedoch bereits von bce19940449e besetzt. Ein Überschreiben des bestehenden Changesets kommt natürlich nicht in Frage, denn das würde ja zu Datenverlust führen. Anhängen ist ebenfalls nicht möglich, dies würde zu unterschiedlichen Versionsgeschichten führen. Also bleibt nur noch, die Änderung e5088b70e741 auf die gleiche Stufe neben bce19940449e zu setzen. Damit sind beide Änderungen jeweils "aktuell" und damit Kopfversionen. Mercurial liesse sich mittels hg push -f dazu überreden, diese Transaktion tatsächlich durchzuführen, allerdings ist das nicht besonders nett, weil damit die Zusammenführung der beiden Kopfversionen auf das andere Repo abgeschoben wird. Daher gehen wir den anderen Weg und ziehen die letzte Änderung (bce19940449e) von "klon":

test$ hg pull ../klon 
pulling from ../klon
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

Auch bei dieser Operation wird eine zusätzliche Kopfversion erstellt, jedoch diesmal in "unserem" Repo. Das ist völlig in Ordnung, daher führt Mercurial den Vorgang ohne zu meckern aus, weist uns jedoch auf den Umstand hin. Wenn wir uns nun die Kopfversionen anschauen, sehen wir folgendes:

test$ hg heads
changeset:   12:bce19940449e
tag:         tip
parent:      10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 19:28:11 2012 +0100
summary:     datei_von_klon.txt erstellt.

changeset:   11:e5088b70e741
user:        phip
date:        Fri Feb 03 19:29:40 2012 +0100
summary:     andere_datei.txt gelöscht.

Offensichtlich sind zwei Kopfversionen vorhanden, wie erwartet. Hier ist nun auch ersichtlich, weshalb Revisionsnummern nicht repoübergreifend gelten: Changeset bce19940449e ist Nummer 11 in "klon", jedoch Nummer 12 in "test"!

Kopfversionen vereinen (merge)

Neben dem Tipp mit hg heads schlägt uns Mercurial auch noch vor, hg merge zu verwenden. Das tun wir nun auch, denn damit werden die beiden Kopfversionen wieder vereint:

test$ hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)
test$ hg status
M datei_von_klon.txt
test$ hg diff
diff -r e5088b70e741 datei_von_klon.txt
--- /dev/null    Thu Jan 01 00:00:00 1970 +0000
+++ b/datei_von_klon.txt    Fri Feb 03 19:59:30 2012 +0100
@@ -0,0 +1,1 @@
+Von "klon" erstellte Datei

Hier ist zu beachten, dass hg merge noch nichts am Repo ändert. Es wird lediglich die Arbeitskopie so verändert, dass sie der Kombination der beiden heads entspricht. Aus diesem Grund sind auch bis zum nächsten hg commit nach wie vor beide Kopfversionen vorhanden:

test$ hg heads
changeset:   12:bce19940449e
tag:         tip
parent:      10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 19:28:11 2012 +0100
summary:     datei_von_klon.txt erstellt.

changeset:   11:e5088b70e741
user:        phip
date:        Fri Feb 03 19:29:40 2012 +0100
summary:     andere_datei.txt gelöscht.

test$ hg commit -m 'Änderungen zusammengeführt.'
test$ hg heads
changeset:   13:7f552d0520d2
tag:         tip
parent:      11:e5088b70e741
parent:      12:bce19940449e
user:        phip
date:        Fri Feb 03 20:03:42 2012 +0100
summary:     Änderungen zusammengeführt.

Auch die Versionshistorie hat sich etwas verändert:

test$ hg log -r 10:
changeset:   10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 18:27:48 2012 +0100
summary:     klondatei.txt gelöscht.

changeset:   11:e5088b70e741
user:        phip
date:        Fri Feb 03 19:29:40 2012 +0100
summary:     andere_datei.txt gelöscht.

changeset:   12:bce19940449e
parent:      10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 19:28:11 2012 +0100
summary:     datei_von_klon.txt erstellt.

changeset:   13:7f552d0520d2
tag:         tip
parent:      11:e5088b70e741
parent:      12:bce19940449e
user:        phip
date:        Fri Feb 03 20:03:42 2012 +0100
summary:     Änderungen zusammengeführt.

Changeset 13 (7f552d0520d2) ist nun die Zusammenführung von e5088b70e741 und bce19940449e. Entsprechend hat sich auch die Arbeitskopie verändert, hier sind nun sowohl die Änderungen von "klon", als auch die Änderungen von "test" eingeflossen:

test$ ls -l
total 24
-rw-rw-r-- 1 phip phip 27 2012-02-03 19:59 datei_von_klon.txt
-rw-rw-r-- 1 phip phip 18 2012-01-25 06:06 neuer_name.txt
test$ cat datei_von_klon.txt 
Von "klon" erstellte Datei

Synchronisierung

Diese Changesets können nun problemlos an unseren Klon weitergegeben werden:

test$ hg push ../klon 
pushing to ../klon
searching for changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 0 changes to 0 files
test$ cd ../klon/
klon$ ls -l
total 36
-rw-rw-r-- 1 phip phip 24 2012-02-03 17:38 andere_datei.txt
-rw-rw-r-- 1 phip phip 27 2012-02-03 19:27 datei_von_klon.txt
-rw-rw-r-- 1 phip phip 18 2012-02-03 17:38 neuer_name.txt
klon$ hg update
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
klon$ ls -l
total 24
-rw-rw-r-- 1 phip phip 27 2012-02-03 19:27 datei_von_klon.txt
-rw-rw-r-- 1 phip phip 18 2012-02-03 17:38 neuer_name.txt

Auch die Versionshistorie von "klon" sieht nun etwas anders aus:

klon$ hg log -r 10:tip
changeset:   10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 18:27:48 2012 +0100
summary:     klondatei.txt gelöscht.

changeset:   11:bce19940449e
user:        phip
date:        Fri Feb 03 19:28:11 2012 +0100
summary:     datei_von_klon.txt erstellt.

changeset:   12:e5088b70e741
parent:      10:c4d4768fb66b
user:        phip
date:        Fri Feb 03 19:29:40 2012 +0100
summary:     andere_datei.txt gelöscht.

changeset:   13:7f552d0520d2
tag:         tip
parent:      12:e5088b70e741
parent:      11:bce19940449e
user:        phip
date:        Fri Feb 03 20:03:42 2012 +0100
summary:     Änderungen zusammengeführt.

Zusammenfassung

Dies war nun relativ viel Text für einen aus Anwendersicht eigentlich relativ simplen Vorgang. Daher nochmals ein Beispiel am Stück, ohne unnötige Kommentare dazwischen:

klon$ echo "neu mit zwei Zeilen." >> datei_von_klon.txt 
klon$ cat datei_von_klon.txt 
Von "klon" erstellte Datei
neu mit zwei Zeilen.
klon$ hg commit -m 'Zeile ergänzt.'

klon$ cd ../test/

test$ echo 'Von "test" erstellte Datei' > datei_von_test.txt
test$ hg add datei_von_test.txt 
test$ hg commit -m 'Datei erstellt.'

test$ hg pull ../klon 
pulling from ../klon
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

test$ hg merge
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)

test$ hg commit -m 'Merge.'

test$ hg push ../klon 
pushing to ../klon
searching for changes
adding changesets
adding manifests
adding file changes
added 2 changesets with 1 changes to 1 files

test$ ls -l
total 36
-rw-rw-r-- 1 phip phip 48 2012-02-03 20:23 datei_von_klon.txt
-rw-rw-r-- 1 phip phip 27 2012-02-03 20:22 datei_von_test.txt
-rw-rw-r-- 1 phip phip 18 2012-01-25 06:06 neuer_name.txt
test$ cat datei_von_klon.txt 
Von "klon" erstellte Datei
neu mit zwei Zeilen.

Auch hier sei nochmals auf die Erweiterung fetch verwiesen, denn neben pull und update kümmert sich diese bei Bedarf auch gleich um merge und commit.

Konfliktbehebung: hg resolve  

Bisher konnten die Repositories vollständig automatisiert gemergt werden. Die liegt daran, dass die Änderungen jeweils ausreichend weit entfernt waren, sie betrafen genau genommen sogar immer verschiedene Dateien. Dies sollte die Regel sein, doch kann man in der Praxis nicht ausschliessen, dass es beim Zusammenführen von Änderungen zu Konflikten kommt. Dies passiert vor Allem im Zusammenhang mit binären Dateien und bei verschiedenen Änderungen an ein und derselben Zeile in einer Textdatei.

Zuerst ein Beispiel, was dann passiert:

test$ echo 'Eine Änderung von "test".' >> datei_von_klon.txt 
test$ cat datei_von_klon.txt 
Von "klon" erstellte Datei
neu mit zwei Zeilen.
Eine Änderung von "test".
test$ hg commit -m 'datei_von_klon.txt erweitert.'
test$ cd ../klon/
klon$ echo 'Eine Änderung von "klon".' >> datei_von_klon.txt 
klon$ hg commit -m 'datei_von_klon.txt erweitert.'
klon$ hg pull ../test 
pulling from ../test
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

klon$ hg merge
merging datei_von_klon.txt
warning: conflicts during merge.
merging datei_von_klon.txt failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon

klon$ hg status
M datei_von_klon.txt
? datei_von_klon.txt.orig

klon$ cat datei_von_klon.txt
Von "klon" erstellte Datei
neu mit zwei Zeilen.
<<<<<<< local
Eine Änderung von "klon".
=======
Eine Änderung von "test".
>>>>>>> other

klon$ cat datei_von_klon.txt.orig 
Von "klon" erstellte Datei
neu mit zwei Zeilen.
Eine Änderung von "klon".

Offensichtlich ist hg merge nicht in der Lage, die Änderungen zu kombinieren. Dies ist auch verständlich, denn aus beiden Repositories wurde die Datei am Ende um eine Zeile erweitert, doch welche soll zuerst und welche anschliessend eingefügt werden?

Zum Lösen des Konflikts existieren mehrere Möglichkeiten, die da wären:

  1. Lokale Änderungen verwerfen und Version des anderen Repos verwenden.
  2. Lokale Änderungen behalten und andere Änderungen verwerfen.
  3. Konflikt manuell auflösen.

In jedem Fall muss der Benutzer entscheiden, welche Variante im speziellen Fall angewendet werden soll.

Auch wenn es bisher so aussah, als hätte Mercurial die Änderungen selbstständig kombiniert, wurde in Wahrheit ein anderes Programm dafür aufgerufen. Ohne weitere Angaben ist dies jedoch "internal:merge", also ein internes Merge-tool von Mercurial. Dieses macht bei "normalen" Änderungen an Textdateien eine gute Arbeit, ist nicht interaktiv, daher kann es sich nicht um die Behebung von Konflikten kümmern. hg help merge-tools zeigt eine Liste von weiteren internen Werkzeugen. Eine einfache Alternative, welche den User zwischen den oben angegebenen Varianten 1 und 2 entscheiden lässt, ist "internal:prompt":

klon$ hg resolve -t internal:prompt --all
 no tool found to merge datei_von_klon.txt
keep (l)ocal or take (o)ther? l
klon$ hg status
M datei_von_klon.txt
? datei_von_klon.txt.orig
klon$ cat datei_von_klon.txt
Von "klon" erstellte Datei
neu mit zwei Zeilen.
Eine Änderung von "klon".
klon$ hg resolve -l
R datei_von_klon.txt

Mit dieser Option wird dem Benutzer die Auswahl geboten zwischen lokaler (l) und anderer (o) Version der Datei. Dabei ist jedoch zu beachten, dass sich diese Option auf die gesamte Datei bezieht, also auch auf möglicherweise automatisch kombinierbare Unterschiede. Der Aufruf von hg resolve -l zeigt, dass der Konflikt in der Datei gelöst (Resolved) wurde.

Einfach nur die komplette lokale oder andere Version zu nehmen mag einfach erscheinen, doch sie ist natürlich nicht immer angebracht. In unserem Beispiel wäre es schöner, beide Zeilen in der Datei zu haben. Dafür müssen wir als erstes dafür sorgen, dass der Konflikt wieder existiert:

klon$ hg resolve -u --all
klon$ hg resolve -l
U datei_von_klon.txt

Nun ist sie wieder Unresolved, der Konflikt ist also ungelöst. Mit diesem Vorgang wird die Datei jedoch nicht verändert, die Konfliktmarkierungen sind daher nicht mehr vorhanden. Doch diese lassen sich durch einen erneuten Aufruf von hg merge ebenfalls wiederherstellen:

klon$ hg update -C
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
klon$ hg merge
merging datei_von_klon.txt
warning: conflicts during merge.
merging datei_von_klon.txt failed!
0 files updated, 0 files merged, 0 files removed, 1 files unresolved
use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
klon$ cat datei_von_klon.txt
Von "klon" erstellte Datei
neu mit zwei Zeilen.
<<<<<<< local
Eine Änderung von "test".
=======
Eine Änderung von "klon".
>>>>>>> other

Wir können die Datei nun in einem beliebigen Texteditor öffnen und die Konfliktmarkierungen entfernen, was dann in folgendem Dateiinhalt resultiert:

klon$ cat datei_von_klon.txt
Von "klon" erstellte Datei
neu mit zwei Zeilen.
Eine Änderung von "test".
Eine Änderung von "klon".

Um die Änderung einchecken zu können, müssen wir Mercurial nun noch mitteilen, dass der Konflikt gelöst wurde:

klon$ hg commit
abort: unresolved merge conflicts (see hg help resolve)
klon$ hg resolve -m datei_von_klon.txt
klon$ hg resolve -l
R datei_von_klon.txt
klon$ hg commit -m 'Konflikt gelöst.'

Solche "kleinen" Konflikte lassen sich problemlos mit einem einfachen Texteditor lösen. Schwieriger wird es bei vielen Änderungen, dann bietet sich die Verwendung eines grafischen Merge-Tools an. Vertreter dieser Sparte wären beispielsweise meld oder kdiff3. Zur Verwendung eines externen Werkzeugs wird dann einfach dessen Name als Argument für die Option -t angegeben, beispielswese:

klon$ hg resolve -t meld

Eine Anmerkung an dieser Stelle zu Microsoft Word-Files: Diese Dateien sind bekanntlich in einem binären Format gespeichert und Konflikte können daher nicht einfach mit den simplen Tools gelöst werden. Word selbst kann jedoch auch als solches Werkzeug verwendet werden. TortoiseHg bietet diese Variante als docdiff an.

Wichtige Hinweise  

Bis hierher wurden die grundlegenden Funktionen von Mercurial anhand von einfachen Beispielen vorgestellt. Es sollte für den aufmerksamen Leser nun problemlos möglich sein, eigene Repositories zu erstellen und damit zu arbeiten. Bevor jedoch die etwas spezielleren Anwendungsgebiete vorgestellt werden, sollen an dieser Stelle noch einige wichtige Tipps und Hinweise angebracht werden:

Mercurial ist plattformunabhängig. Das bedeutet jedoch auch, dass von verschiedenen Betriebssystemen auf die selben Daten zugegriffen werden kann. Dies führt besonders bei der Kombination von unixoiden (Linux, Mac OSX, etc.) und DOS-basierten Betriebssystemen (Windows) teilweise zu Problemen.

Ein Punkt betrifft die darunterliegenden Dateisysteme: Während bei unixoiden Systemen normalerweise zwischen Gross- und Kleinschreibung in Dateinamen unterschieden wird, so ist dies bei Windows in der Regel nicht der Fall. Dies führt dann zu Problemen, wenn beispielsweise mit einem Mac der Name einer Datei von "test.txt" in "Test.txt" geändert wird. Diese Änderung wird dort als vollkommen in Ordnung angesehen und übernommen. Wenn sie nun jedoch auf einem Windowsrechner angewendet werden soll, gibt es ein Problem: Im Changeset ist eine Änderung vermerkt, die eigentlich nichts tut. Dies kann dazu führen, dass sich Mercurial weigert, irgendwelche weiteren Änderungen einzuchecken. Das Problem lässt sich dann teilweise dadurch beheben, indem auf eine ältere Version und anschliessend wieder auf tip aktualisiert wird, wodurch die fragwürdige Änderung umgangen wird. Alternativ könnte auch das gesamte Repo neu geklont werden, doch das ist eher eine Notlösung.

Ein anderer Punkt betrifft die Zeilenenden: Windows verwendet CrLf (Carriage return + Linefeed, ASCII 13 und 10), während Linux und MAC OSX typischerweise nur Lf verwenden. Dies ist in der Regel kein Problem, doch wenn ein Editor des einen Betriebssystems alle Zeilenenden in "sein" Format konvertiert, ist ein manuelles Verfolgen der Änderungen kaum noch möglich, da dann jede Zeile verändert wurde.

Zuletzt gibt es noch das leidige Thema mit verschiedenen Zeichenkodierungen. Generell ist es nicht verkehrt, in Dateinamen nur Zeichen des ASCII-Bereichs (0 bis 127) zu verwenden, was jedoch auch Umlaute (ä, ö und ü) ausschliesst. Alle anderen Zeichen können dazu führen, dass Dateinamen auf einem "fremden" System nicht mehr korrekt dargestellt werden können oder dass die Dateien bei jedem Speichern jeweils umbenannt werden (müssen).

Ausblick  

Damit wäre auch der zweite Teil des Tutorials bereits zu Ende. Im dritten Teil werden nun noch weitere Möglichkeiten und Funktionalitäten von Mercurial vorgestellt, welche das Leben eines Entwicklers durchaus erleichtern können.

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.