he sc e ut ab De usg A
Projekte effizient und flexibel verwalten
Versionskontrolle mit
Git
O’REILLY
Jon Loeliger Deutsche Übersetzung von Kathrin Lichtenberg
Versionskontrolle mit Git
Jon Loeliger Deutsche Übersetzung von Kathrin Lichtenberg
Beijing · Cambridge · Farnham · Köln · Sebastopol · Taipei · Tokyo
Die Informationen in diesem Buch wurden mit größter Sorgfalt erarbeitet. Dennoch können Fehler nicht vollständig ausgeschlossen werden. Verlag, Autoren und Übersetzer übernehmen keine juristische Verantwortung oder irgendeine Haftung für eventuell verbliebene Fehler und deren Folgen. Alle Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt und sind möglicherweise eingetragene Warenzeichen. Der Verlag richtet sich im Wesentlichen nach den Schreibweisen der Hersteller. Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Kommentare und Fragen können Sie gerne an uns richten: O’Reilly Verlag Balthasarstr. 81 50670 Köln Tel.: 0221/9731600 Fax: 0221/9731608 E-Mail:
[email protected]
Copyright der deutschen Ausgabe: © 2010 by O’Reilly Verlag GmbH & Co. KG
Die Originalausgabe erschien 2009 unter dem Titel Version Control with Git im Verlag O’Reilly Media, Inc.
Die Darstellung einer Langohrfledermaus im Zusammenhang mit dem Thema Git ist ein Warenzeichen von O’Reilly Media, Inc.
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Lektorat: Susanne Gerbert, Köln Korrektorat: Eike Nitz, Köln Fachgutachten: Sven Riedel, München Satz: III-satz, Husby, www.drei-satz.de Umschlaggestaltung: Michael Oreal, Köln Produktion: Andrea Miß, Köln Belichtung, Druck und buchbinderische Verarbeitung: Druckerei Kösel, Krugzell; www.koeselbuch.de ISBN 978-3-89721-945-8 Dieses Buch ist auf 100% chlorfrei gebleichtem Papier gedruckt.
Inhalt
1
2
3
4
Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
VII
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Hintergrund . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Geburt von Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Vorgeschichte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der weitere Verlauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namen sind Schall und Rauch? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1 2 5 6 7
1
Git installieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
Die Linux-Binärdistributionen benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine Quellversion beschaffen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kompilieren und Installieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Git unter Windows installieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9 11 12 13
Erste Schritte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
Die Git-Kommandozeile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kurze Einführung in die Benutzung von Git . . . . . . . . . . . . . . . . . . . . . . . . . . Konfigurationsdateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19 21 28 31
Grundlegende Git-Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
Grundlegende Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Darstellungen des Objektspeichers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Git-Konzepte am Werk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33 38 41
| III
5
6
7
8
9
Dateiverwaltung und der Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
51
Es geht um den Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dateiklassifikationen in Git . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git add benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einige Hinweise zur Benutzung von git commit . . . . . . . . . . . . . . . . . . . . . . . . git rm benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git mv benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Hinweis zum Überwachen des Umbenennens . . . . . . . . . . . . . . . . . . . . . . Die Datei .gitignore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eine ausführliche Darstellung des Objektmodells und der Dateien von Git . .
52 52 54 56 58 60 62 63 65
Commits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
71
Atomare Änderungsmengen (Changesets) . . . . . . . . . . . . . . . . . . . . . . . . . . . . Commits festlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Commit-Verlauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Commits suchen (und finden) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
72 73 78 90
Zweige . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97
Gründe für den Einsatz von Zweigen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweignamen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweige benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweige erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweignamen auflisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweige anschauen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweige auschecken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweige löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97 99 100 101 102 103 105 112
Diffs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
115
Formen des git diff-Befehls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein einfaches git diff-Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git diff und Commit-Bereiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git diff mit Pfadbegrenzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wie Subversion und Git Diffs ableiten – ein Vergleich . . . . . . . . . . . . . . . . . . .
116 120 124 126 128
Merges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
131
Merge-Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mit Merge-Konflikten arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Merge-Strategien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wie Git über Merges denkt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
132 138 148 158
IV | Inhalt
10 Commits verändern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
163
Vorsicht beim Verändern des Verlaufs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git reset benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git cherry-pick benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . git revert benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . reset, revert und checkout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das oberste Commit ändern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Rebasing von Commits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
165 166 174 176 176 178 180
11 Entfernte Repositories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
193
Repository-Konzepte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Auf andere Repositories verweisen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ein Beispiel mit entfernten Repositories . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Entfernte Repository-Operationen in Bildern . . . . . . . . . . . . . . . . . . . . . . . . . . Entfernte Zweige hinzufügen und löschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Remote-Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Bare-Repositories und git push . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Repositories veröffentlichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
194 198 202 214 221 222 224 226
12 Repository-Verwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
233
Die Repository-Struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . In einer verteilten Entwicklung leben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wissen, wo Sie stehen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mit mehreren Repositories arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
233 237 240 245
13 Patches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
253
Wozu Patches benutzen? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Patches generieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Patches per E-Mail verschicken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Patches anwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schlechte Patches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Patching und Merging im Vergleich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
254 255 264 267 274 274
14 Hooks – Einstiegspunkte für die Automatisierung . . . . . . . . . . . . . . . . . . . . . . . . .
277
Hooks installieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verfügbare Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
279 282
15 Projekte kombinieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
289
Die alte Lösung: partielle Checkouts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die offensichtliche Lösung: den Code in Ihr Projekt importieren . . . . . . . . . .
290 292
Inhalt
| V
Die automatisierte Lösung: Teilprojekte mit eigenen Skripten auschecken . . . Die native Lösung: Gitlinks und git submodule . . . . . . . . . . . . . . . . . . . . . . . .
299 301
16 Git mit Subversion-Repositories benutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
309
Beispiel: ein einfacher Klon eines einzigen Zweigs . . . . . . . . . . . . . . . . . . . . . . Verschieben, Ziehen, Verzweigen und Zusammenführen mit git svn . . . . . . . Verschiedene Hinweise zum Arbeiten mit Subversion . . . . . . . . . . . . . . . . . . .
309 316 323
Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
327
VI | Inhalt
Vorwort
Zielgruppe Eine gewisse Vertrautheit mit Revisionskontrollsystemen ist zwar von Vorteil, aber auch ein Leser, der noch kein solches System kennt, sollte in der Lage sein, genügend über grundlegende Git-Operationen zu lernen, um schon nach kurzer Zeit erfolgreich loszulegen. Erfahrenere Leser können einen Einblick in den inneren Aufbau von Git erlangen und auf diese Weise einige seiner leistungsfähigeren Techniken meistern. Die angepeilte Zielgruppe für dieses Buch sollte mit der Unix-Shell, wesentlichen ShellBefehlen und allgemeinen Programmierkonzepten vertraut sein.
Voraussetzungen Fast alle Beispiele und Erörterungen in diesem Buch gehen davon aus, dass der Leser ein Unix-artiges System mit einer Kommandozeilenschnittstelle besitzt. Der Autor hat diese Beispiele in Debian- und Ubuntu-Linux-Umgebungen entwickelt. Die Beispiele sollten auch in anderen Umgebungen funktionieren, etwa Mac OS X oder Solaris, allerdings muss der Leser in diesen Fällen leichte Abweichungen erwarten. Einige Beispiele erfordern root-Zugang auf den Maschinen, wenn Systemoperationen erforderlich sind. Natürlich müssen Sie sich in diesen Situationen der Verantwortung des root-Zugriffs bewusst sein.
Der Aufbau des Buches und Auslassungen Dieses Buch ist in Form einer fortschreitenden Folge von Themen gestaltet, die jeweils auf den zuvor eingeführten Konzepten aufbauen. Die ersten zehn Kapitel konzentrieren sich auf die Konzepte und Operationen, die ein Repository betreffen. Sie formen die Grundlage für die komplexeren Operationen an mehreren Repositories, die in den letzten sechs Kapiteln behandelt werden.
| VII
Wenn Sie Git bereits installiert oder vielleicht sogar schon kurz benutzt haben, werden Sie die einführenden Informationen sowie die Erklärungen zur Installation nicht benötigen, die in den ersten beiden Kapiteln präsentiert werden, genausowenig wie den kurzen Überblick im dritten Kapitel. Die Konzepte, die in Kapitel 4 behandelt werden, sind wichtig für ein solides Verständnis des Git-Objektmodells. Sie bilden die Grundlage und bereiten den Leser für ein klareres Verständnis vieler komplexerer Git-Operationen vor. Kapitel 5 bis Kapitel 10 behandeln die verschiedenen Themen genauer. Kapitel 5 beschreibt den Index und die Dateiverwaltung. Kapitel 6 und Kapitel 10 diskutieren die Grundlagen des Bestätigens mit Commits und des Arbeitens mit ihnen, um eine beständige Entwicklungslinie zu bilden. In Kapitel 7 werden Zweige eingeführt, mit deren Hilfe sich unterschiedliche entwicklungslinien von einem lokalen Repository aus manipulieren lassen. Kapitel 8 erläutert, wie Git »Diffs« ableitet und präsentiert. Git bietet eine umfassende und leistungsfähige Fähigkeit, unterschiedliche Entwicklungszweige zusammenzufassen. Die Grundlagen des Zusammenführens von Zweigen in Merges und des Auflösens von Merge-Konflikten werden in Kapitel 9 behandelt. Ein wichtiger Einblick in das Git-Modell ist die Erkenntnis, dass das gesamte Zusammenführen in Merges durch Git im lokalen Repository im Kontext Ihres aktuellen Arbeitsverzeichnisses geschieht. Die Grundlagen des Benennens und Austauschens von Daten mit einem anderen, entfernten Repository werden in Kapitel 11 behandelt. Nachdem wir die Grundlagen des Merging gemeistert haben, wird demonstriert, dass das Arbeiten mit mehreren Repositories einfach eine Kombination aus einem Austauschschritt und einem Merge-Schritt ist. Der Austausch ist das neue Konzept, das in diesem Kapitel gezeigt wird, der MergeSchritt wurde in Kapitel 9 behandelt. Kapitel 12 bietet einen eher philosophischen und abstrakten Einblick in die RepositoryVerwaltung »im Ganzen«. Es bereitet außerdem einen Kontext für Kapitel 13 vor, in dem Patches behandelt werden, die zum Einsatz kommen, wenn ein direkter Austausch von Repository-Informationen mit den Git-eigenen Übertragungsprotokollen nicht möglich ist. Die verbleibenden drei Kapitel behandeln fortgeschrittene Themen: die Verwendung von Hooks zum Automatisieren von Schritten, das Kombinieren von Projekten und mehreren Repositories zu einem Superprojekt sowie die Zusammenarbeit mit Subversion-Repositories. Git entwickelt sich immer noch rasend schnell, weil es eine aktive Entwicklergemeinde gibt. Glauben Sie nicht, dass Git nicht ausgereift genug ist, um für die Entwicklung eingesetzt zu werden; dennoch werden laufend Verbesserungen an der Benutzungsschnittstelle und weitere Verfeinerungen veröffentlicht. Selbst während dieses Buch entstand, entwickelte Git sich weiter. Ich entschuldige mich, falls ich nicht ganz Schritt halten konnte.
VIII |
Vorwort
Den Befehl gitk behandle ich nicht so ausführlich, wie er es verdient hätte. Wenn Sie grafische Darstellungen des Verlaufs innerhalb eines Repository haben wollen, sollten Sie sich gitk anschauen. Es gibt noch weitere Werkzeuge zum Visualisieren von Verläufen, aber auch die werden hier nicht behandelt. Auch die sich rapide entwickelnde und anwachsende Gruppe der anderen Git-bezogenen Werkzeuge konnte ich nicht berücksichtigen. Nicht einmal alle Git-eigenen Werkzeuge und Optionen konnte ich in diesem Buch gründlich zeigen. Auch dafür meine Entschuldigung. Wahrscheinlich gibt es aber genügend Hinweise, Tipps und Anweisungen, um Sie, die Leser, anzuregen, Ihre eigenen Erkundungen einzuholen!
Konventionen in diesem Buch In diesem Buch gelten folgende typographische Konventionen: Kursivschrift Kennzeichnet neue Begriffe, URLs, E-Mail-Adressen, Dateinamen und Dateierweiterungen. Nichtproportionalschrift
Wird für Programmlistings sowie innerhalb von Absätzen zum Kennzeichnen von Programmelementen wie Variablen- oder Funktionsnamen, Datenbanken, Datentypen, Umgebungsvariablen, Anweisungen und Schlüsselwörter verwendet. Nichtproportionalschrift fett
Zeigt Befehle oder anderen Text, der unverändert vom Benutzer eingegeben werden muss. Nichtproportionalschrift kursiv
Zeigt Text, der durch benutzereigene Werte oder Werte ersetzt werden soll, die sich aus dem Kontext ergeben. Dieses Icon kennzeichnet einen Tipp, einen Vorschlag, einen nützlichen Hinweis oder eine allgemeine Anmerkung.
Dieses Icon kennzeichnet eine Warnung oder einen Aufruf zur Vorsicht.
Darüber hinaus sollten Sie mit den grundlegenden Shell-Befehlen zum Manipulieren von Dateien und Verzeichnissen vertraut sein. Viele Beispiele enthalten solche Befehle zum Hinzufügen oder Entfernen von Verzeichnissen, Kopieren von Dateien oder Anlegen einfacher Dateien:
Konventionen in diesem Buch
|
IX
$ $ $ $ $ $
cp datei.txt kopie-von-datei.txt mkdir neuesverzeichnis rm datei rmdir verzeichnis echo "Testzeile" > datei echo "Noch eine Zeile" >> datei
Befehle, die mit root-Berechtigung ausgeführt werden müssen, erscheinen zusammen mit einer sudo-Operation: # Installieren des Git-Kernpakets $ sudo apt-get install git-core
Wie Sie Dateien bearbeiten oder Änderungen innerhalb Ihres Arbeitsverzeichnisses herbeiführen, ist ganz Ihnen überlassen. Sie sollten mit einem Texteditor vertraut sein. In diesem Buch deute ich das Verarbeiten einer Datei entweder durch einen direkten Kommentar oder durch einen Pseudobefehl an: # datei.c bearbeiten, um neuen Text zu erhalten $ edit index.html
Die Codebeispiele benutzen Dieses Buch soll Ihnen dabei helfen, Ihre Arbeit zu erledigen. Im Allgemeinen können Sie den Code aus diesem Buch in Ihren Programmen und Dokumentationen benutzen. Sie müssen uns nur dann um Erlaubnis fragen, wenn Sie einen wesentlichen Teil des Codes reproduzieren wollen. Ein Programm zu schreiben, das mehrere Codeschnippsel aus diesem Buch verwendet, erfordert z.B. keine gesonderte Erlaubnis. Das Verkaufen oder Vertreiben einer CD-ROM mit Beispielen aus O’Reilly-Büchern hingegen erfordert eine Erlaubnis. Das Beantworten einer Frage mithilfe dieses Buchs und eines zitierten Beispielcodes erfordert keine Erlaubnis. Das Aufnehmen einer größeren Menge Beispielcode aus diesem Buch in die Dokumentation Ihres Produkts aber erfordert eine Erlaubnis. Wir begrüßen es, wenn wir als Quelle genannt werden, verlangen es aber nicht. Eine Nennung enthält normalerweise den Titel, den Autor, den Verlag und die ISBN. Zum Beispiel: »Versionskontrolle mit Git von Jon Loeliger. Copyright 2010 Jon Loeliger, 9780-596-52012-0.« Falls Sie das Gefühl haben, dass Ihr Einsatz des Beispielcodes einer Erlaubnis bedarf oder Sie sich nicht sicher sind, dann schreiben Sie uns:
[email protected]
Danksagungen Ohne die Hilfe vieler Leute wäre diese Arbeit nicht entstanden. Ich möchte Avery Pennarun dafür danken, dass er wesentliches Material zu Kapitel 14, Kapitel 15 und Kapitel 16 beigesteuert hat. Er unterstützte mich auch bei Kapitel 4 und Kapitel 9. Ich weiß seine
X | Vorwort
Hilfe zu würdigen. Ich möchte öffentlich denjenigen danken, die sich die Zeit genommen haben, das Buch in den verschiedenen Stadien zu begutachten: Robert P. J. Day, Alan Hasty, Paul Jimenez, Barton Massey, Tom Rix, Jamey Sharp, Sarah Sharp, Larry Streepy, Andy Wilcox und Andy Wingo. Außerdem danke ich meiner Frau Rhonda und meinen Töchtern Brandi und Heather für ihre moralische Unterstützung, ihr sanftes Antreiben, den Pinot Noir und die gelegentlichen Grammatiktipps. Danke auch an Mylo, meinen Langhaardackel, der während der gesamten Schreibarbeit zusammengerollt auf meinem Schoß ausgeharrt hat. Einen besonderen Dank möchte ich an K. C. Dignan schicken, der genügend moralische Unterstützung und doppelseitiges Klebeband geliefert hat, um meinen Hintern so lange auf dem Stuhl zu halten, dass das Buch schließlich fertiggestellt werden konnte! Schließlich danke ich den Mitarbeitern bei O’Reilly und ganz besonders meinen Lektoren Andy Oram und Martin Streicher.
Danksagungen
|
XI
Kapitel 1
KAPITEL 1
Einführung
Hintergrund Niemand, der bei einigermaßen klarem Verstand ist, startet ein Projekt heutzutage ohne Backup-Strategie. Da die Daten vergänglich sind und leicht verloren gehen können – sei es durch eine irrtümliche Codeänderung oder einen katastrophalen Festplatten-Crash –, tut man gut daran, ein lebendiges Archiv seiner gesamten Arbeit vorzuhalten. Bei Text- und Codeprojekten umfasst diese Backup-Strategie üblicherweise eine Versionskontrolle oder die Verfolgung und Verwaltung von Änderungen. Jeder Entwickler darf mehrere Änderungen pro Tag vornehmen. Die stetig anwachsende Sammlung dient gleichzeitig als Repository (Lager), Projektgeschichte, Kommunikationsmedium und Werkzeug zur Team- und Produktverwaltung. Angesichts ihrer zentralen Rolle funktioniert die Versionskontrolle am effektivsten, wenn sie auf die Arbeitsgewohnheiten und -ziele des Projektteams zugeschnitten ist. Ein Werkzeug, das unterschiedliche Versionen von Software oder anderen Inhalten verwaltet und überwacht, wird im Allgemeinen als Versionskontrollsystem (Version Control System; VCS), Source Code Manager (SCM), Revision Control System (RCS) oder mit anderen Kombinationen der Wörter »Revision«, »Version«, »Code«, »Content«, »Control«, »Management« und »System« bezeichnet. Auch wenn sich die Autoren und Benutzer der einzelnen Programme möglicherweise über esoterische Dinge streiten, dienen alle Systeme den gleichen Problemstellungen: der Entwicklung und Pflege eines Repository mit Inhalten, der Ermöglichung des Zugriffs auf historische Ausgaben der einzelnen Daten und der Aufzeichnung aller Änderungen in einem Log. In diesem Buch bezieht sich der Begriff Versionskontrollsystem ganz allgemein auf jede Form solcher Systeme. Dieses Buch behandelt Git, eine besonders leistungsstarke und flexible Versionskontrolle mit geringem Overhead, die gemeinschaftliche Entwicklung begünstigt. Git wurde von Linus Torvalds erfunden, um die Entwicklung des Linux-Kernels zu unterstützen. Mittlerweile hat es sich bei einer Vielzahl unterschiedlicher Projekte bewährt.
| 1
Die Geburt von Git Existiert ein Missverhältnis zwischen einem Werkzeug und einem Projekt, stellen die Entwickler oft einfach ein neues Werkzeug her. Um genau zu sein, ist die Verlockung, sich neue Werkzeuge zu schaffen, in der Welt der Software meist unglaublich groß. Angesichts der vielen vorhandenen Versionskontrollsysteme sollte man allerdings nicht leichtfertig beschließen, ein weiteres herzustellen. Bei entsprechendem Bedarf, einer gewissen Einsicht und einer gehörigen Portion Motivation kann es sich jedoch als völlig richtig erweisen, ein neues Werkzeug aus dem Boden zu stampfen. Git, das von seinem Schöpfer auch liebevoll als »Information Manager from Hell« bezeichnet wird, ist ein solches Werkzeug. Die genauen Umstände und der exakte Zeitpunkt seiner Entstehung sind zwar im Zuge des politischen Gezänks innerhalb der Linux-Kernel-Community untergegangen, aber es besteht kein Zweifel daran, dass mit Git ein wohldurchdachtes Versionskontrollsystem geschaffen wurde, das in der Lage ist, die weltweit verteilte Entwicklung von Software in großem Maßstab zu unterstützen. Vor Git wurde bei der Entwicklung des Linux-Kernels das kommerzielle System BitKeeper benutzt, das komplizierte Operationen ermöglichte, die von den damaligen freien Versionskontrollsystemen wie RCS und CVS nicht geboten wurden. Als jedoch das Unternehmen, dem BitKeeper gehörte, im Frühjahr 2005 seine freie, d. h. kostenlose Version weiter einschränkte, fand die Linux-Gemeinschaft, dass BitKeeper keine praktikable Lösung mehr darstellte. Linus suchte nach Alternativen. Kommerzielle Lösungen meidend, untersuchte er die freien Softwarepakete, fand bei ihnen jedoch die gleichen Einschränkungen und Mängel, die bereits früher zu ihrer Ablehnung geführt hatten. Was stimmte nur nicht mit den vorhandenen Versionskontrollsystemen? Welche exotischen Funktionen oder Eigenschaften wollte Linus unbedingt haben, konnte sie aber nicht finden? Unterstützung der verteilten Entwicklung Die »verteilte Entwicklung« besitzt viele Facetten, und Linus wollte ein neues VCS haben, das die meisten von ihnen abdeckte. Es sollte sowohl parallele als auch unabhängige und gleichzeitige Entwicklung in privaten Repositories erlauben, ohne dass ständig eine Synchronisation mit einem zentralen Repository nötig wäre, die einen Entwicklungsengpass bilden könnte. Es müsste mehrere Entwickler an mehreren Orten zulassen, selbst wenn einige von ihnen zeitweise offline wären. Tausende von Entwicklern Es reicht nicht, nur ein verteiltes Entwicklungsmodell zu haben. Linus wusste, dass an jeder Linux-Version Tausende von Entwicklern mitarbeiteten, sodass jedes neue VCS mit einer großen Anzahl von Entwicklern zurechtkommen musste – ob sie nun an denselben oder an unterschiedlichen Teilen eines gemeinsamen Projekts arbeiteten. Und das neue VCS musste dazu in der Lage sein, ihre gesamte Arbeit zuverlässig zu integrieren.
2 |
Kapitel 1: Einführung
Schnelle und effiziente Ausführung Linus wollte das neue VCS unbedingt schnell und effizient haben. Um die schiere Masse von Update-Operationen zu unterstützen, die allein am Linux-Kernel vorgenommen werden würden, müssten sowohl die einzelnen Update-Operationen als auch die Operationen zur Netzwerkübertragung sehr schnell sein. Um Platz und damit Übertragungszeit zu sparen, würden Komprimierungs- und »Delta«-Techniken benötigt werden. Der Einsatz eines verteilten Modells anstelle eines zentralisierten Modells würde sicherstellen, dass die tägliche Entwicklung nicht durch die Netzwerklatenz behindert wird. Integrität und Vertrauen bewahren Da es sich bei Git um ein verteiltes Revisionskontrollsystem handelt, ist es unerlässlich, die absolute Sicherheit zu haben, dass die Integrität der Daten gewahrt und nicht in irgendeiner Weise verletzt wird. Woher wissen Sie, dass die Daten beim Übergang von einem Entwickler oder einem Repository zum nächsten nicht verändert wurden? Und woher wissen Sie, dass die Daten in einem Git-Repository wirklich das sind, was sie zu sein vorgeben? Git benutzt eine gebräuchliche kryptographische Hash-Funktion namens Secure Hash Function (SHA1), um Objekte in seiner Datenbank zu benennen und zu identifizieren. Diese Funktion bietet vielleicht keinen absoluten Schutz, hat sich aber in der Praxis als solide genug erwiesen, um die Integrität und das Vertrauen für alle verteilten Repositories von Git zu sichern. Verantwortlichkeit erzwingen Einer der wesentlichen Aspekte bei einem Versionskontrollsystem besteht darin zu wissen, wer die Dateien geändert hat und weshalb dies geschehen ist (falls sich das überhaupt sagen lässt). Git erzwingt bei jedem Commit (Änderungsbestätigung), das eine Datei ändert, einen Protokolleintrag in einem Änderungslog. Welche Informationen in diesem Änderungslog gespeichert werden, hängt von den einzelnen Entwicklern, den Projektanforderungen, dem Management, den vereinbarten Konventionen usw. ab. Git sorgt dafür, dass Dateien, die der Versionskontrolle unterliegen, nicht mysteriöserweise geändert werden, da sich die Verantwortlichkeit für alle Änderungen nachverfolgen lässt. Unveränderbarkeit Gits Repository-Datenbank enthält Datenobjekte, die unveränderbar sind. Das bedeutet: Sobald sie erzeugt und in der Datenbank abgelegt wurden, können sie nicht mehr verändert werden. Man kann sie natürlich auf andere Weise neu anlegen, die Originaldaten lassen sich jedoch nicht folgenlos ändern. Das Design der GitDatenbank bewirkt, dass auch die gesamte History, die in der Versionskontrolldatenbank gespeichert ist, unveränderbar ist. Die Benutzung unveränderbarer Objekte bringt mehrere Vorteile mit sich, wie etwa die Möglichkeit, schnelle Vergleiche durchzuführen.
Die Geburt von Git | 3
Atomare Transaktionen Bei atomaren Transaktionen werden unterschiedliche, aber verwandte Änderungen entweder zusammen oder gar nicht ausgeführt. Diese Eigenschaft sorgt dafür, dass die Versionskontrolldatenbank nicht in einem teilweise geänderten (und damit möglicherweise unzulässigen) Zustand hinterlassen wird, wenn ein Update oder Commit durchgeführt wird. Git implementiert atomare Transaktionen, indem es vollständige, diskrete Repository-Zustände aufzeichnet, die nicht in einzelne oder kleinere Zustandsänderungen unterteilt werden können. Unterstützung und Förderung der verzweigten Entwicklung Fast alle VCS können unterschiedliche Entwicklungsstammbäume innerhalb eines einzigen Projekts benennen. So könnte z.B. eine Folge von Codeänderungen mit »Entwicklung« bezeichnet werden, während eine andere »Test« heißt. Jedes Versionskontrollsystem ist außerdem in der Lage, eine einzelne Entwicklungslinie in mehrere Entwicklungslinien aufzuteilen und dann die verschiedenen Fäden wieder zu vereinigen oder zusammenzuführen. Genau wie die meisten VCS nennt Git eine Entwicklungslinie Zweig (engl. branch) und weist jedem Zweig einen Namen zu. Neben dem Verzweigen gibt es das Vereinigen. Linus wollte nicht nur das Verzweigen vereinfachen, um alternative Linien zu fördern, sondern auch das Vereinigen dieser Zweige sollte leicht vonstatten gehen. Da das Vereinigen von Zweigen in anderen Versionskontrollsystemen oft eine leidige und schwierige Operation war, sollte es in Git auf jeden Fall sauber, schnell und leicht gehen. Vollständige Repositories Damit einzelne Entwickler nicht einen zentralisierten Repository-Server nach historischen Revisionsinformationen abfragen müssen, war es entscheidend, dass jedes Repository für jede Datei eine vollständige Kopie aller vergangenen Revisionen erhält. Ein sauberes internes Design Ein Endanwender mag sich über ein sauberes internes Design vielleicht nicht sonderlich viele Gedanken machen, für Linus und entsprechend auch für andere Git-Entwickler war es dagegen natürlich sehr wichtig. Das Objektmodell von Git weist einfache Strukturen auf, die grundlegende Konzepte für die Rohdaten, die Verzeichnisstruktur, das Aufzeichnen von Änderungen usw. abdecken. Dieses Objektmodell in Verbindung mit einer global eindeutigen Identifizierungstechnik erlaubt im Ergebnis ein sehr sauberes Datenmodell, das in einer verteilten Entwicklungsumgebung verwaltet werden kann. Frei wie in Freiheit (oder in Freibier) Muss man mehr sagen? Da es offensichtlich Bedarf für ein neues VCS gab, setzten sich viele talentierte Softwareprogrammierer zusammen und schufen Git. Es entstand also aus reiner Notwendigkeit!
4 |
Kapitel 1: Einführung
Die Vorgeschichte Der Versuch, die Geschichte der Versionskontrollsysteme vollständig darzulegen, würde den Rahmen dieses Buches sprengen. Es gibt jedoch einige wegweisende, innovative Systeme, die die Grundlage bildeten oder direkt zur Entwicklung von Git führten. (Dieser Abschnitt soll nur punktuell zeigen, wann neue Funktionen eingeführt oder in der FreeSoftware-Gemeinschaft populär wurden.) Das Source Code Control System (SCCS) war eines der ursprünglichen Systeme unter Unix. Es wurde in den frühen 70er Jahren des 20. Jahrhunderts von M. J. Rochkind entwickelt.1 Dabei handelt es sich wohl um das erste VCS, das auf einem Unix-System verfügbar war. Der zentrale Speicher, den das SCCS bereitstellte, wurde als »Repository« bezeichnet, und dieses grundlegende Konzept gibt es bis heute. SCCS bot darüber hinaus ein einfaches Locking-Modell, um die Entwicklung zu serialisieren. Musste ein Entwickler Dateien ausführen und ein Programm testen, dann checkte er sie ohne Sperren aus. Um eine Datei jedoch zu bearbeiten, musste er sie mit einer Sperre auschecken (diese Konvention wird vom Unix-Dateisystem erzwungen). Nach Beendigung der Arbeit checkte er die Datei wieder im Repository ein und entfernte die Sperre. In den frühen 80er Jahren des 20. Jahrhunderts stellte Walter Tichy das Revision Control System (RCS)2 vor. RCS führte Vorwärts- und Rückwärts-Deltas ein, um unterschiedliche Dateirevisionen effizient speichern zu können. Das Concurrent Version System (CVS), 1986 von Dick Grune entworfen und erstmals implementiert, vier Jahre später dann von Berliner et al. neugeschrieben, erweiterte und modifizierte das RCS-Modell, und das mit großem Erfolg. CVS wurde sehr beliebt und war über eine lange Zeit der De-facto-Standard in der Open Source-Gemeinde. CVS wies gegenüber RCS mehrere Vorteile auf, darunter eine verteilte Entwicklung und Repository-weite Änderungssätze für ganze »Module«. Außerdem führte CVS ein neues Paradigma für die Sperren (Locks) ein. Während frühere Systeme verlangt hatten, dass der Entwickler jede Datei sperrte, bevor er sie änderte, und die Entwickler dann entsprechend warten mussten, bis sie an der Reihe waren, gab CVS jedem Entwickler die Berechtigung, auf seine private Arbeitskopie zu schreiben. Änderungen von unterschiedlichen Entwicklern konnten später automatisch vom CVS zusammengeführt werden, es sei denn, zwei Entwickler versuchten, dieselbe Zeile zu ändern. In diesem Fall wurde der Konflikt gekennzeichnet, und die Entwickler mussten eine Lösung finden. Die neuen Regeln für die Sperren erlaubten es unterschiedlichen Entwicklern, gleichzeitig und parallel am Code zu arbeiten.
1
»The Source Code Control System«, IEEE Transactions on Software Engineering 1(4) (1975): 364–370.
2
»RCS – A System for Version Control«, Software Practice and Experience 15 (7) (Juli 1985): 637–654.
Die Vorgeschichte
| 5
Wie es so kommt, führten erkannte Schwächen und Fehler in CVS schließlich zu einem neuen Versionskontrollsystem. Subversion (SVN), eingeführt um das Jahr 2001 herum, wurde in der Free-Software-Gemeinschaft schnell populär. Anders als CVS bestätigt SVN Änderungen atomar und bietet eine deutlich bessere Unterstützung für Verzweigungen. BitKeeper und Mercurial wichen radikal von all den erwähnten Lösungen ab. Beide eliminierten das zentrale Repository; stattdessen wurde der Speicher verteilt, sodass jedem Entwickler seine eigene freigegebene Kopie zur Verfügung gestellt wird. Git wurde von diesem Peer-to-Peer-Modell abgeleitet. Für Mercurial und Monotone schließlich wurde ein Hash-Fingerprint erfunden, mit dem sich der Inhalt einer Datei eindeutig identifizieren lässt. Der Name, der der Datei zugewiesen wird, ist ein Spitzname und bildet lediglich einen bequemen Alias für den Benutzer, nichts weiter. Git greift diese Idee ebenfalls auf. Intern basiert der Git-Identifikator auf dem Inhalt der Datei, ein Konzept, das man als Content-addressable File Store bezeichnet. Das Konzept ist nicht neu. (Siehe z.B. »The Venti Filesystem,« (Plan 9), Bell Labs, http://www.usenix.org/events/fast02/quinlan/quinlan_html/index.html.) Git übernahm laut Linus die Idee umgehend von Monotone.3 Mercurial implementierte das Konzept gleichzeitig mit Git.
Der weitere Verlauf Nachdem die Grundlage geschaffen worden war, von außen noch einige Anstöße gekommen waren und die VCS-Krise sich weiter verschärft hatte, erblickte Git im April 2005 das Licht der Welt. Git wurde am 7. April mit diesem Commit »self-hosted« (es hat sich quasi selbst verwaltet): commit e83c5163316f89bfbde7d9ab23ca2e25604af29 Author: Linus Torvalds
Date: Thu Apr 7 15:13:13 2005 -0700 Initial revision of "git", the information manager from hell
Kurze Zeit später wurde das erste Linux-Commit ausgeführt: commit 1da177e4c3f41524e886b7f1b8a0c1fc7321cac2 Author: Linus Torvalds Date: Sat Apr 16 15:20:36 2005 -0700 Linux-2.6.12-rc2 Initial git repository build. I'm not bothering with the full even though we have it. We can create a separate "historical" archive of that later if we want to, and in the meantime it's 3.2GB when imported into git - space that would just make the
3
Private E-Mail.
6 |
Kapitel 1: Einführung
history, git about early
git days unnecessarily complicated, when we don't have a lot of good infrastructure for it. Let it rip!
Dieses eine Commit überstellte den Hauptteil des gesamten Linux-Kernels in ein GitRepository.4 Es bestand aus folgenden Dingen: 17291 files changed, 6718755 insertions(+), 0 deletions(-)
Ja, genau, damit wurden 6,7 Millionen Zeilen Code übertragen! Nur drei Minuten später wurde der erste Patch, der Git benutzte, auf den Kernel angewandt. Überzeugt davon, dass es funktioniert, kündigte Linus ihn am 20. April 2005 auf der Linux-Kernel-Mailingliste an. Da er sich nun wieder der eigentlichen Entwicklung des Kernels widmen wollte, übergab Linus die Pflege des Git-Quellcodes am 25. Juli 2005 an Junio Hamano, und zwar mit der Bemerkung, dass Junio »die logische Wahl war«. Etwa zwei Monate später wurde die Version 2.6.12 des Linux-Kernels mithilfe von Git veröffentlicht.
Namen sind Schall und Rauch? Linus selbst erklärt den Namen »Git« so: »Ich bin ein egoistischer Kerl und nenne alle meine Projekte nach mir selbst. Erst Linux, jetzt git.«5 Sicher, der Name »Linux« für den Kernel war eine Mischung aus Linus und Minix. Aber ganz offensichtlich kommt eine gehörige Portion Ironie ins Spiel, wenn jemand den britisch-englischen Ausdruck für eine blöde oder nichtsnutzige Person verwendet. In der Zwischenzeit sind andere Leute mit alternativen, vernünftigeren Interpretationen auf den Plan getreten: Global Information Tracker scheint darunter am beliebtesten zu sein.
4
Unter http://kerneltrap.org/node/13996 können Sie nachlesen, wie die alten BitKeeper-Logs (vor 2.5) in ein GitRepository importiert wurden.
5
Siehe http://www.infoworld.com/article/05/04/19/HNtorvaldswork_1.html.
Namen sind Schall und Rauch? | 7
Kapitel 2
KAPITEL 2
Git installieren
Momentan ist Git (anscheinend) nicht standardmäßig auf irgendeiner GNU/Linux-Distribution oder einem anderen Betriebssystem installiert. Bevor Sie daher Git benutzen können, müssen Sie es installieren. Die dazu nötigen Schritte hängen stark vom Hersteller und der Version Ihres Betriebssystems ab. In diesem Kapitel wird beschrieben, wie man Git unter Linux und auf Microsoft Windows sowie innerhalb von Cygwin installiert.
Die Linux-Binärdistributionen benutzen Viele Linux-Hersteller bieten vorkompilierte Binärpakete an, um die Installation neuer Anwendungen, Werkzeuge und Dienstprogramme zu vereinfachen. Jedes Paket gibt seine Abhängigkeiten an, und der Paketmanager der Distribution installiert typischerweise die Voraussetzungen und das gewünschte Paket auf einmal (wohlgeordnet und automatisch) und in einem Rutsch.
Debian/Ubuntu Auf den meisten Debian- und Ubuntu-Systemen wird Git als Sammlung aus Paketen angeboten, die je nach Bedarf einzeln installiert werden können. Das wichtigste GitPaket heißt git-core, die Dokumentation gibt es in git-doc, weitere Pakete sind die folgenden: git-arch, git-cvs, git-svn Falls Sie ein Projekt von Arch, CVS oder Subversion nach Git übertragen müssen oder den umgekehrten Weg gehen wollen, dann installieren Sie eines oder mehrere dieser Pakete. git-gui, gitk, gitweb Wenn Sie die Repositories in einer grafischen Anwendung oder in Ihrem Webbrowser betrachten wollen, installieren Sie diese Pakete. git-gui ist eine Tcl/Tk-basierte
| 9
grafische Benutzungsoberfläche für Git; gitk ist ein weiterer Git-Browser, geschrieben in Tcl/Tk, der sich allerdings mehr auf die Visualisierung der Projektgeschichte konzentriert. gitweb ist in Perl geschrieben und zeigt ein Git-Repository im Browserfenster an. git-email Dies ist eine wichtige Komponente, falls Sie Git-Patches per E-Mail verschicken wollen, was in manchen Projekten eine gängige Praxis ist. git-daemon-run Installieren Sie dieses Paket, um Ihr Repository freizugeben. Es erzeugt einen Daemon-Dienst, der Ihnen erlaubt, Ihre Repositories über anonyme Download-Anforderungen für die gemeinsame Nutzung freizugeben. Da die Distributionen ganz unterschiedlich sind, ist es am besten, wenn Sie im Paketverzeichnis Ihrer Distribution nach einer vollständigen Liste der Pakete suchen, die mit Git zu tun haben. Vor allem git-doc und git-email sind ausdrücklich zu empfehlen. Debian und Ubuntu enthalten ein Paket namens git, das allerdings nicht Teil des in diesem Buch besprochenen Versionskontrollsystems Git ist. Bei git handelt es sich um ein völlig anderes Programm namens GNU Interactive Tools. Passen Sie auf, dass Sie nicht versehentlich das falsche Paket installieren!
Folgender Befehl installiert die wichtigen Git-Pakete, indem er apt-get als root ausführt: $ sudo apt-get install git-core git-doc gitweb \ git-gui gitk git-email git-svn
Andere Binärdistributionen Um Git auf anderen Linux-Distributionen zu installieren, suchen Sie das (oder die) entsprechende(n) Paket(e). Zur Installation der Software benutzen Sie den distributionseigenen Paketmanager. Auf Gentoo-Systemen nehmen Sie z.B. emerge: $ sudo emerge dev-util/git
Auf Fedora verwenden Sie yum: $ sudo yum install git
Das git von Fedora entspricht ungefähr dem git-core von Debian. Andere i386-FedoraPakete enthalten: git.i386 Die wesentlichen git-Werkzeuge git-all.i386 Ein Metapaket zum Einbeziehen aller Git-Werkzeuge
10 | Kapitel 2: Git installieren
git-arch.i386 Git-Werkzeuge zum Importieren von Arch-Repositories git-cvs.i386 Git-Werkzeuge zum Importieren von CVS-Repositories git-daemon.i386 Git-Protokoll-Daemon git-debuginfo.i386 Debug-Information für das Paket Git git-email.i386 Git-Werkzeuge zum Verschicken von E-Mails git-gui.i386 Grafisches Git-Werkzeug git-svn.i386 Git-Werkzeuge zum Importieren von Subversion-Repositories gitk.i386 Visualisierer des Git-Revisionsbaums Denken Sie auch hier daran, dass manche Distributionen wie bei Debian die Git-Bestandteile auf viele verschiedene Pakete aufteilen. Falls Ihrem System ein bestimmter GitBefehl zu fehlen scheint, müssen Sie wahrscheinlich ein weiteres Paket installieren. Achten Sie darauf, dass die Git-Pakete Ihrer Distribution einigermaßen aktuell sind. Führen Sie git --version aus, nachdem Sie Git auf Ihrem System installiert haben. Falls Ihre Mitarbeiter eine modernere Version von Git benutzen, müssen Sie die vorkompilierten Git-Pakete Ihrer Distribution durch eine selbstkompilierte Version ersetzen. In der Dokumentation des Paketmanagers erfahren Sie, wie Sie bereits installierte Pakete entfernen; im nächsten Abschnitt erklären wir Ihnen, wie Sie Git aus den Quellen kompilieren.
Eine Quellversion beschaffen Besuchen Sie das Git-Master-Repository, falls Sie es vorziehen, den Git-Code von seiner offiziellen Quelle herunterzuladen, oder Sie die wirklich allerneueste Version haben wollen. Momentan ist das Master-Repository für Git-Quellen http://git.kernel.org im Verzeichnis pub/software/scm. Die in diesem Buch beschriebene Version von Git ist etwa 1.6.0, aber sicher werden Sie aus den Quellen die neueste Version herunterladen. Eine Liste aller verfügbaren Versionen finden Sie unter http://kernel.org/pub/software/scm/git. Um das Kompilieren zu beginnen, laden Sie den Quellcode für Version 1.6.0 (oder später) herunter und entpacken ihn:
Eine Quellversion beschaffen
| 11
$ wget http://kernel.org/pub/software/scm/git/git-1.6.0.tar.gz $ tar xzf git-1.6.0.tar.gz $ cd git-1.6.0
Kompilieren und Installieren Git ist vergleichbar mit anderen Open Source-Programmen. Sie konfigurieren es einfach, tippen make und installieren es. Kein Problem, oder? Vielleicht doch. Wenn Ihr System die passenden Bibliotheken sowie eine robuste Build-Umgebung aufweist und Sie Git nicht anpassen müssen, ist das Kompilieren des Codes ein Kinderspiel. Falls Ihrer Maschine dagegen ein Compiler oder die Server- und Softwareentwicklungsbibliotheken fehlen oder Sie noch nie eine komplexe Anwendung aus den Quellen zusammengestellt und kompiliert haben, dann sollten Sie diese Lösung nur als letzten Ausweg betrachten. Git lässt sich in hohem Maße konfigurieren, und das Kompilieren sollte nicht auf die leichte Schulter genommen werden. Um das Kompilieren fortzusetzen, werfen Sie einen Blick in die INSTALL-Datei im GitQuellpaket. Die Datei listet verschiedene externe Abhängigkeiten auf, einschließlich der zlib-, openssl- und libcurl-Bibliotheken. Einige der erforderlichen Bibliotheken und Pakete sind ein wenig unklar oder gehören zu größeren Paketen. Hier sind drei Tipps für eine Debian-Distribution (stable): • curl-config, ein kleines Werkzeug zum Extrahieren von Informationen über die lokale curl-Installation, befindet sich im libcurl3-openssl-dev-Paket. • Die Header-Datei expat.h stammt aus dem libexpat1-dev-Paket. • Das Dienstprogramm msgfmt gehört zum gettext-Paket. Da das Kompilieren aus den Quellen als »Entwicklungsarbeit« betrachtet wird, sind die normalen Binärversionen der installierten Bibliotheken nicht ausreichend. Stattdessen benötigen Sie die -dev-Versionen, weil die Entwicklungsvarianten auch noch HeaderDateien mitliefern, die während der Kompilierung gebraucht werden. Falls Sie einige dieser Pakete oder eine notwendige Bibliothek auf Ihrem System nicht finden können, dann zeigen das Makefile und die Konfigurationsoptionen Alternativen. Sollte Ihnen z.B. die expat-Bibliothek fehlen, dann können Sie die Option NO_EXPAT im Makefile setzen. Allerdings fehlen Ihrer Version dann einige Funktionen, wie im Makefile angemerkt. So werden Sie z.B. nicht in der Lage sein, Änderungen mithilfe der Transportmechanismen HTTP und HTTPS auf ein entferntes Repository zu schieben. Andere Makefile-Konfigurationsoptionen unterstützen Portierungen auf verschiedene Plattformen und Distributionen. So betreffen einige Flags das Darwin-Betriebssystem von Mac OS X. Modifizieren Sie die Einstellungen entweder von Hand und stellen Sie die passenden Optionen ein oder stellen Sie fest, welche Parameter automatisch in der obersten INSTALL-Datei gesetzt werden.
12 | Kapitel 2: Git installieren
Sobald Ihre System- und Kompilierungsoptionen gesetzt sind, ist der Rest ganz einfach. Git wird standardmäßig im Home-Verzeichnis in den Unterverzeichnissen ~/bin/, ~/lib/ und ~/share/ installiert. Im Allgemeinen ist diese Standardeinstellung nur dann sinnvoll, wenn Sie Git persönlich benutzen und es nicht für andere Benutzer freigeben müssen. Diese Befehle kompilieren und installieren Git in Ihrem Home-Verzeichnis: $ $ $ $
cd git-1.6.0 ./configure make all make install
Falls Sie Git an einer anderen Stelle installieren wollen, wie etwa /usr/local/, um einen allgemeinen Zugriff zu ermöglichen, fügen Sie dem ./configure-Befehl die Option --prefix=/usr/local hinzu. Führen Sie anschließend make als normaler Benutzer, make install dagegen als root aus: $ $ $ $
cd git-1.6.0 ./configure --prefix=/usr/local make all sudo make install
Um die Git-Dokumentation zu installieren, fügen Sie den make- bzw. make install-Befehlen die Targets doc und install-doc hinzu: $ cd git-1.6.0 $ make all doc $ sudo make install install-doc
Es sind noch einige weitere Bibliotheken erforderlich, um die Dokumentation komplett zusammenzustellen. Alternativ gibt es vorkompilierte Manpages und HTML-Seiten, die auch getrennt installiert werden können; passen Sie allerdings auf, dass Sie sich keine Probleme mit unterschiedlichen Versionen einhandeln, wenn Sie diesen Weg wählen. Eine Version aus den Quellen enthält alle Git-Unterpakete und -Befehle wie etwa gitemail und gitk. Es ist nicht nötig, diese Dienstprogramme extra zu kompilieren oder zu installieren.
Git unter Windows installieren Es gibt für Windows zwei konkurrierende Git-Pakete: ein Cygwin-basiertes Git und eine »native« Version namens msysGit. Ursprünglich wurde nur die Cygwin-Version unterstützt und msysGit war experimentell und instabil. Zur Zeit der Drucklegung dieses Buches jedoch funktionierten beide Versionen gut und unterstützten fast die gleichen Funktionen. Die wichtigste Ausnahme besteht momentan, also bei Git 1.6.0, darin, dass msysGit git-svn nicht richtig unterstützt. Falls Sie auf Interoperabilität zwischen Git und Subversion angewiesen sind, müssen Sie die Cygwin-Version von Git benutzen. Ansonsten ist es eine Frage der persönlichen Vorlieben, für welche Version Sie sich entscheiden.
Git unter Windows installieren
| 13
Wenn Sie sich nicht sicher sind, wie Sie sich entscheiden sollen, schauen Sie diese Faustregeln an: • Falls Sie Cygwin bereits unter Windows benutzen, dann verwenden Sie das CygwinGit, weil es besser mit Ihrer Cygwin-Installation zusammenarbeitet. Z.B. funktionieren alle Dateinamen im Cygwin-Stil in Git, und das Umleiten der Programmein- und -ausgaben funktioniert immer genau wie erwartet. • Falls Sie Cygwin nicht benutzen, ist es einfacher, msysGit zu installieren, weil es sein eigenes Installationsprogramm mitbringt. • Falls Sie Git in die Windows Explorer-Shell integrieren wollen (um z.B. in der Lage zu sein, mit der rechten Maustaste auf einen Ordner zu klicken und »Git GUI Here« oder »Git Bash Here« auszuführen), installieren Sie msysGit. Wollen Sie diese Funktionalität benutzen, ziehen aber dennoch Cygwin vor, dann installieren Sie einfach beide Pakete – das ist ohne Probleme möglich. Sollten Sie nun immer noch Zweifel haben, welches Paket Sie benutzen sollen, dann installieren Sie msysGit. Achten Sie darauf, die neueste Version zu verwenden (1.5.6.1 oder höher), da die Qualität der Windows-Unterstützung von Git in nachfolgenden Versionen immer weiter zunimmt.
Das Cygwin-Git-Paket installieren Beim Cygwin-Git-Paket handelt es sich, wie der Name andeutet, um ein Paket innerhalb des Cygwin-Systems selbst. Um es zu installieren, führen Sie das Cygwin-Programm setup.exe aus, das Sie von http://cygwin.com herunterladen können. Nach dem Start von setup.exe stellen Sie für die meisten Optionen die Standardwerte ein, bis Sie zu der Liste der zu installierenden Pakete kommen. Die Git-Pakete befinden sich in der Kategorie devel, wie Abbildung 2-1 zeigt. Wenn Sie die gewünschten Pakete ausgewählt haben, klicken Sie ein paarmal auf Weiter, bis die Cygwin-Installation abeschlossen ist. Sie können nun die Cygwin Bash Shell aus dem Startmenü heraus starten, das nun den Befehl git enthalten sollte (siehe Abbildung 2-2). Falls Ihre Cygwin-Konfiguration verschiedene Compilerwerkzeuge wie gcc und make enthält, können Sie alternativ auch Ihre eigene Git-Kopie aus dem Quellcode herstellen, indem Sie den gleichen Anweisungen folgen wie unter Linux.
Ein unabhängiges Git installieren (msysGit) Das msysGit-Paket lässt sich auf einem Windows-System leicht installieren, da das Paket all seine Abhängigkeiten enthält. Es besitzt sogar SSH-Befehle zum Generieren der Schlüssel, die die Betreuer des Repository benötigen, um den Zugriff zu steuern. msysGit soll sich gut in die nativen Windows-Anwendungen integrieren, etwa die Windows Explorer-Shell.
14 | Kapitel 2: Git installieren
Abbildung 2-1: Das Cygwin-Setup
Abbildung 2-2: Die Cygwin-Shell
Git unter Windows installieren
| 15
Laden Sie zuerst die neueste Version des Installationsprogramms von http://code.google. com/p/msysgit herunter. Die gesuchte Datei heißt normalerweise ungefähr so: Git-1.5.6. 1-preview20080701.exe. Nach dem Herunterladen führen Sie das Installationsprogramm aus. Die Darstellung sollte in etwa so aussehen wie in Abbildung 2-3.
Abbildung 2-3: Das msysGit-Setup
Je nach der tatsächlich installierten Version müssen Sie bei einem Kompatibilitätshinweis wie in Abbildung 2-4 Next klicken oder auch nicht. Dieser Hinweis betrifft Inkompatibilitäten zwischen Zeilenenden im Windows-Stil und im Unix-Stil, sogenannte CRLF bzw. LF. Klicken Sie einige Male Next, bis Sie die Darstellung aus Abbildung 2-5 sehen. Am besten führen Sie msysGit täglich über den Windows Explorer aus, klicken Sie deshalb die beiden entsprechenden Checkboxen an. Zusätzlich wird ein Icon zum Starten der Git-Bash (einer Kommandozeile, über die die git-Befehle zur Verfügung stehen) im Startmenü im Abschnitt Git installiert. Da die meisten Beispiele in diesem Buch die Kommandozeile benutzen, nehmen Sie die Git-Bash für den Einstieg. Alle Beispiele in diesem Buch funktionieren unter Linux und Windows gleichermaßen gut, wobei Sie allerdings Folgendes beachten müssen: msysGit für Windows benutzt die älteren Git-Befehlsnamen, die in »Die Git-Kommandozeile« auf Seite 19 erwähnt werden. Um den Beispielen mit msysGit zu folgen, geben Sie git-add für git add ein.
16 | Kapitel 2: Git installieren
Abbildung 2-4: msysGit-Hinweis
Abbildung 2-5: msysGit-Wahlmöglichkeiten
Git unter Windows installieren
| 17
Kapitel 3
KAPITEL 3
Erste Schritte
Git verwaltet Veränderungen. Daraus folgt, dass Git viel mit anderen Versionskontrollsystemen gemein hat. Viele Grundsätze – die Idee eines Commit, das Änderungslog, das Repository – sind gleich, und die Abläufe sind bei einem Großteil der Werkzeuge konzeptuell ähnlich. Git bietet allerdings auch viele Neuheiten. Die Begriffe und Verfahren anderer Versionskontrollsysteme funktionieren in Git möglicherweise anders oder gelten gar nicht. Wie auch immer Ihre bisherigen Erfahrungen aussehen, in diesem Buch erfahren Sie, wie Git arbeitet und wie Sie es zu Ihrem größtmöglichen Nutzen einsetzen. Fangen wir an.
Die Git-Kommandozeile Die Benutzung von Git ist ganz leicht. Geben Sie einfach git ein. Ohne Argumente listet Git seine Optionen und die gebräuchlichsten Unterbefehle auf: $ git git [--version] [--exec-path[=GIT_EXEC_PATH]] [-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR] [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS] The most commonly used git commands are: add Add file contents to the index bisect Find the change that introduced a bug by binary search branch List, create, or delete branches checkout Checkout and switch to a branch clone Clone a repository into a new directory commit Record changes to the repository diff Show changes between commits, the commit and working trees, etc. fetch Download objects and refs from another repository grep Print lines matching a pattern init Create an empty git repository or reinitialize an existing one log Show commit logs merge Join two or more development histories
| 19
mv pull push rebase reset rm show status tag
Move or rename a file, a directory, or a symlink Fetch from and merge with another repository or a local branch Update remote refs along with associated objects Forward-port local commits to the updated upstream head Reset current HEAD to the specified state Remove files from the working tree and from the index Show various types of objects Show the working tree status Create, list, delete, or verify a tag object signed with GPG
Eine vollständige (und ziemlich abschreckende) Liste der git-Unterbefehle erhalten Sie, wenn Sie git help --all eintippen. Wie Sie anhand des Benutzungshinweises erkennen, gibt es ein paar Optionen für git. Die meisten Optionen, gezeigt als [ARGS] in dem Hinweis, gelten für bestimmte Unterbefehle. Z.B. beeinflusst die Option --version den git-Befehl und gibt eine Versionsnummer aus: $ git --version git version 1.6.0
Im Gegensatz dazu ist --amend ein Beispiel für eine Option des git-Unterbefehls commit: $ git commit --amend
Manche Aufrufe erfordern beide Optionsformen (die zusätzlichen Leerzeichen in der Kommandozeile dienen hier lediglich dazu, den Unterbefehl vom Basisbefehl zu trennen, und sind nicht erforderlich): $ git --git-dir=project.git
repack -d
Übrigens erhalten Sie die Dokumentation für die einzelnen git-Unterbefehle, indem Sie entweder git help Unterbefehl oder git Unterbefehl --help eingeben. Ursprünglich wurde Git als eine Sammlung vieler einfacher, getrennter, unabhängiger Befehle geliefert, die getreu der »Unix-Werkzeugkasten«-Philosophie entwickelt wurden: Stelle kleine, kompatible Werkzeuge her. Jeder Befehl trug einen Namen, dessen Bestandteile durch einen Bindestrich getrennt sind, wie etwa git-commit und git-log. Der momentane Trend unter den Entwicklern sieht jedoch so aus, dass ein einziges gitProgramm benutzt wird, hinter das man einen Unterbefehl hängt. Das heißt also, dass beide Formen identisch sind: git commit und git-commit. Wenn Sie http://www.kernel.org/pub/software/scm/git/docs/ besuchen, können Sie die vollständige Git-Dokumentation online lesen.
Git-Befehle verstehen sowohl »kurze« als auch »lange« Optionen. So behandelt z.B. der Befehl git commit die folgenden Beispiele als gleichbedeutend:
20 | Kapitel 3: Erste Schritte
$ git commit -m "Tippfehler entfernt." $ git commit --message="Tippfehler entfernt."
Die Kurzform -m benutzt einen einzelnen Bindestrich, während die Langform --message zwei verwendet. (Das ist konsistent mit der GNU Long Options Extension.) Manche Optionen existieren nur in einer Form. Schließlich können Sie Optionen von einer Liste mit Argumenten über die »DoppelterBindestrich«-Konvention trennen. Benutzen Sie z.B. den doppelten Bindestrich, um den Steuerteil der Befehlszeile von einer Liste mit Operanden, wie etwa Dateinamen, abzuheben: $ git diff -w master origin -- tools/Makefile
Möglicherweise müssen Sie den doppelten Bindestrich einsetzen, um Dateinamen zu trennen und explizit zu kennzeichnen, wenn diese ansonsten fälschlicherweise für einen weiteren Teil des Befehls gehalten werden könnten. Falls Sie z.B. sowohl eine Datei als auch ein Tag namens main.c haben, bekommen Sie unterschiedliche Ergebnisse: # Das Tag namens "main.c" auschecken $ git checkout main.c # Die Datei namens "main.c" auschecken $ git checkout -- main.c
Kurze Einführung in die Benutzung von Git Um git in Aktion zu sehen, wollen wir ein neues Repository anlegen, Inhalt hinzufügen und einige Revisionen verarbeiten. Es gibt zwei grundlegende Techniken zum Etablieren eines Git-Repository. Sie können es entweder von Grund auf neu anlegen, wobei Sie es mit einem vorhandenen Projekt füllen, oder Sie können ein vorhandenes Repository kopieren oder klonen. Es ist einfacher, mit einem leeren Repository zu beginnen, deshalb machen wir es so.
Ein leeres Repository erzeugen Um eine typische Situation nachzuempfinden, wollen wir ein Repository für Ihre persönliche Website aus dem Verzeichnis ~/public_html anlegen und in einem Git-Repository ablegen. Falls Sie noch keinen Inhalt für eine Website in ~/public_html haben, legen Sie das Verzeichnis an und setzen einige einfache Informationen in eine Datei namens index.html: $ mkdir ~/public_html $ cd ~/public_html $ echo 'Meine Website ist da!' > index.html
Kurze Einführung in die Benutzung von Git
| 21
Wandeln Sie ~/public_html oder ein anderes Verzeichnis in ein Git-Repository um, indem Sie git init ausführen: $ git init Initialized empty Git repository in .git/
Git ist es egal, ob Sie mit einem völlig leeren Verzeichnis beginnen oder mit einem Verzeichnis, das voller Dateien ist. Das Konvertieren des Verzeichnisses in ein Git-Repository verläuft in beiden Fällen gleich. Um anzuzeigen, dass Ihr Verzeichnis ein Git-Repository ist, erzeugt der Befehl git init ein verborgenes Verzeichnis namens .git auf der obersten Ebene Ihres Projekts. CVS und Subversion platzieren in jedem der Verzeichnisse Ihres Projektes Unterverzeichnisse namens CVS bzw. .svn für Revisionsinformationen; Git dagegen setzt all seine Revisionsinformationen in dieses eine .git-Verzeichnis auf der obersten Ebene. Inhalt und Zweck der Datendateien werden im Abschnitt »Im .git-Verzeichnis« auf Seite 41 näher vorgestellt. Der Inhalt des ~/public_html-Verzeichnisses bleibt unberührt. Git betrachtet es als Arbeitsverzeichnis Ihres Projekts oder als das Verzeichnis, in dem Sie die Dateien bearbeiten. Im Gegensatz dazu wird das Repository, das in .git verborgen ist, von Git gepflegt.
Eine Datei zum Repository hinzufügen git init legt ein neues Git-Repository an. Zu Anfang ist jedes Git-Repository leer. Damit
Inhalt verwaltet wird, müssen Sie ihn explizit im Repository deponieren. Dieser bewusste Schritt trennt die Arbeitsdateien von den wichtigen Dateien. Mit git add Datei befördern Sie Datei in das Repository: $ git add index.html
Wenn Sie ein Verzeichnis mit mehreren Dateien haben lassen Sie Git alle Dateien im Verzeichnis und in allen Unterverzeichnissen mit git add . hinzufügen. (Das Argument ., der einzelne Punkt, ist eine Abkürzung für das aktuelle Verzeichnis.)
Nach einem add weiß Git, dass die Datei index.html zum Repository gehört. Allerdings hat Git die Datei bisher nur bereitgestellt, also einen Zwischenschritt vor der eigentlichen Bestätigung ausgeführt. Git trennt die Schritte add und commit, um das ganze System beständiger zu machen. Stellen Sie sich vor, wie störend, verwirrend und zeitraubend es wäre, wenn man jedesmal das Repository aktualisieren müsste, nachdem man eine Datei hinzugefügt, entfernt oder geändert hat. Stattdessen können mehrere vorläufige und verwandte Schritte, wie etwa ein add, »stapelweise« verarbeitet werden, wodurch der Zustand des Repository stabil und konsistent bleibt.
22 | Kapitel 3: Erste Schritte
Der Befehl git status zeigt den Zwischenzustand von index.html: $ git status # On branch master # # Initial commit # # Changes to be committed: # (use "git rm --cached ..." to unstage) # # new file: index.html
Der Befehl gibt aus, dass die neue Datei index.html beim nächsten Commit in das Repository aufgenommen wird. Zusätzlich zu den eigentlichen Änderungen am Verzeichnis und den Dateiinhalten zeichnet Git bei jedem Commit weitere Metadaten auf, darunter eine Lognachricht und den Verfasser der Änderung. Bei einem vollständigen git commit-Befehl werden eine Lognachricht und ein Autor angegeben: $ git commit -m "Anfaenglicher Inhalt von public_html" \ --author="Jon Loeliger <[email protected]>" Created initial commit 9da581d: Anfaenglicher Inhalt von public_html 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 index.html
Natürlich können Sie auf der Kommandozeile eine Lognachricht festlegen, typischerweise wird die Meldung jedoch während einer interaktiven Bearbeitungssitzung erzeugt. Das bietet Ihnen die Möglichkeit, in Ihrem bevorzugten Editor eine vollständige und detaillierte Nachricht zu schreiben. Damit während eines git commit Ihr Lieblingseditor gestartet wird, stellen Sie die Umgebungsvariable GIT_EDITOR entsprechend ein: # In tcsh $ setenv GIT_EDITOR emacs # In bash $ export GIT_EDITOR=vim
Nachdem Sie das Hinzufügen der neuen Datei zum Repository bestätigt haben, zeigt git status an, dass es keine ausstehenden, bereitgestellten Änderungen mehr gibt, die noch bestätigt werden müssten: $ git status # On branch master nothing to commit (working directory clean)
Git nimmt sich auch die Zeit, Ihnen mitzuteilen, dass Ihr Arbeitsverzeichnis sauber ist, was bedeutet, dass das Arbeitsverzeichnis keine unbekannten oder veränderten Dateien enthält, die von dem abweichen, was sich im Repository befindet.
Kurze Einführung in die Benutzung von Git
| 23
Seltsame Fehlermeldungen Git setzt alles daran, den Verfasser jedes Commit festzustellen. Falls Sie Ihren Namen und Ihre E-Mail-Adresse nicht so eingestellt haben, dass Git sie finden kann, werden Sie seltsame Warnungen bemerken. Sie müssen allerdings nicht in eine Existenzkrise verfallen, wenn Sie eine solche kryptische Fehlermeldung sehen: You don't exist. Go away! (Sie existieren gar nicht. Verschwinden Sie!) Your parents must have hated you! (Ihre Eltern müssen Sie gehasst haben!) Your sysadmin must hate you! (Ihr Sysadmin hasst Sie!)
Der Fehler zeigt an, dass Git nicht in der Lage ist, Ihren wirklichen Namen zu ermitteln, vermutlich wegen eines Problems (Vorhandensein, Lesbarkeit, Länge) mit Ihrer Unix-»gecos«Information. Das Problem kann behoben werden, indem Sie Ihre Namens- und E-MailKonfigurationsinformationen einstellen, wie in »Den Commit-Verfasser konfigurieren« auf Seite 24 (s. u.) beschrieben.
Den Commit-Verfasser konfigurieren Bevor Sie viele Commits an einem Repository vornehmen, sollten Sie einige grundlegende Umgebungs- und Konfigurationsoptionen eintragen. Git muss zumindest Ihren Namen und Ihre E-Mail-Adresse kennen. Sie können Ihre Identität auf jeder CommitBefehlszeile angeben, wie bereits gezeigt, aber das ist eine umständliche Methode, die schnell lästig wird. Speichern Sie stattdessen Ihre Identität mithilfe des Befehls git config in einer Konfigurationsdatei: $ git config user.name "Jon Loeliger" $ git config user.email "[email protected]"
Sie können Git Ihren Namen und Ihre E-Mail-Adresse auch über die Umgebungsvariablen GIT_AUTHOR_NAME und GIT_AUTHOR_EMAIL mitteilen. Diese Variablen setzen alle Konfigurationseinstellungen außer Kraft.
Ein weiteres Commit ausführen Um weitere Eigenschaften von Git zu demonstrieren, wollen wir einige Modifikationen vornehmen und einen komplexen Verlauf von Änderungen im Repository erzeugen. Bestätigen Sie eine Änderung an der Datei index.html. Öffnen Sie dazu die Datei, wandeln Sie sie in HTML um und speichern Sie sie: $ cd ~/public_html # Bearbeiten der Datei index.html
24 | Kapitel 3: Erste Schritte
$ cat index.html Meine Website ist da! $ git commit index.html
Wenn der Editor erscheint, geben Sie einen Commit-Logeintrag ein, z.B. »In HTML konvertieren«, und verlassen den Editor wieder. Es gibt jetzt zwei Versionen von index.html im Repository.
Ihre Commits anschauen Sobald Sie ein oder mehrere Commits im Repository haben, können Sie sie auf vielfältige Weise betrachten. Manche Git-Befehle zeigen die Abfolge der einzelnen Commits, andere zeigen die Zusammenfassung für ein einzelnes Commit und wieder andere zeigen die vollständigen Einzelheiten aller Commits im Repository. Der Befehl git log liefert einen sequenziellen Verlauf der einzelnen Commits im Repository: $ git log commit ec232cddfb94e0dfd5b5855af8ded7f5eb5c90d6 Author: Jon Loeliger <[email protected]> Date: Wed Apr 2 16:47:42 2008 -0500 In HTML konvertieren commit 9da581d910c9c4ac93557ca4859e767f5caf5169 Author: Jon Loeliger <[email protected]> Date: Thu Mar 13 22:38:13 2008 -0500 Anfaenglicher Inhalt von public_html
Die Einträge werden der Reihe nach von den neuesten zu den ältesten1 (der Originaldatei) aufgeführt; jeder Eintrag zeigt den Namen sowie die E-Mail-Adresse des Verfassers des jeweiligen Commit, das Datum des Commit, die Lognachricht für die Änderung und die interne Identifikationsnummer des Commit. Die Commit-ID wird in »Inhaltsadressierbare Namen« auf Seite 36 erläutert, Commits werden in Kapitel 6 besprochen. Um nähere Einzelheiten über ein bestimmtes Commit zu erfahren, verwenden Sie den Befehl git show mit einer Commit-Nummer:
1
Genauer gesagt, sind sie nicht in zeitlicher Reihenfolge angegeben, sondern eher in einer topologischen Anordnung der Commits.
Kurze Einführung in die Benutzung von Git
| 25
$ git show 9da581d910c9c4ac93557ca4859e767f5caf5169 commit 9da581d910c9c4ac93557ca4859e767f5caf5169 Author: Jon Loeliger <[email protected]> Date: Thu Mar 13 22:38:13 2008 -0500 Anfaenglicher Inhalt von public_html diff --git a/index.html b/index.html new file mode 100644 index 0000000..34217e9 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +Meine Website ist da!
Wenn Sie git show ohne spezielle Commit-Nummer ausführen, zeigt der Befehl einfach die Details des neuesten Commit. Eine andere Darstellung, show-branch, liefert knappe, einzeilige Zusammenfassungen für den aktuellen Entwicklungszweig: $ git show-branch --more=10 [master] In HTML konvertieren [master^] Anfaenglicher Inhalt von public_html
Die Phrase --more=10 zeigt die letzten zehn Versionen. Allerdings existieren momentan nur zwei, und so werden diese beiden gezeigt. (Standardmäßig wird nur das neueste Commit aufgeführt.) Der Name master ist der vorgegebene Name für diesen Zweig. Kapitel 7 geht näher auf Verzweigungen ein, im Abschnitt »Zweige anschauen« auf Seite 103 wird der Befehl git show-branch genauer beschrieben.
Unterschiede zwischen Commits betrachten Um die Unterschiede zwischen den beiden Revisionen von index.html zu sehen, nehmen Sie die beiden vollständigen Commit-ID-Namen und führen damit git diff aus: $ git diff 9da581d910c9c4ac93557ca4859e767f5caf5169 \ ec232cddfb94e0dfd5b5855af8ded7f5eb5c90d6 diff --git a/index.html b/index.html index 34217e9..40b00ff 100644 --- a/index.html +++ b/index.html @@ -1 +1,5 @@ + + Meine Website ist da! + +
26 | Kapitel 3: Erste Schritte
Diese Ausgabe sollte Ihnen vertraut sein: Sie ähnelt dem, was das diff-Programm erzeugt. Laut Konvention ist die erste Revision, 9da581d910c9c4ac93557ca4859e767f5caf5169, die ältere Version des Inhalts, während die zweite Revision, ec232cddfb94e0dfd5b5855af 8ded7f5eb5c90d6, die neuere darstellt. Daher steht vor jeder Zeile neuen Inhalts ein Pluszeichen (+). Macht Ihnen das Angst? Fürchten Sie sich nicht vor diesen einschüchternden Hexadezimalzahlen. Dankenswerterweise bietet Git noch viele kürzere und einfachere Methoden, um solche Befehle auszuführen, ohne dass man riesige, komplizierte Zahlen angeben muss.
Dateien aus Ihrem Repository entfernen und umbenennen Das Entfernen einer Datei aus einem Repository erfolgt analog zum Hinzufügen einer Datei, allerdings mit dem Befehl git rm. Nehmen Sie einmal an, Sie haben die Datei poem. html auf Ihrer Website, und sie wird nicht mehr benötigt: $ cd ~/public_html $ ls index.html poem.html $ git rm poem.html rm 'poem.html' $ git commit -m "Ein Gedicht entfernen" Created commit 364a708: Ein Gedicht entfernen 0 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 poem.html
Das Löschen erfordert genau wie das Hinzufügen zwei Schritte: git rm drückt Ihr Verlangen aus, die Datei zu entfernen, und bereitet die Änderung vor. Anschließend wird die Änderung im Repository von git commit tatsächlich durchgeführt. Auch hier können Sie die Option -m weglassen und eine Lognachricht, wie »Ein Gedicht entfernen«, interaktiv in Ihrem Lieblingstexteditor eingeben. Sie können eine Datei indirekt umbenennen, indem Sie eine Kombination aus git rm und git add benutzen. Schneller und direkter geht es mit git mv. Hier ein Beispiel der ersten Form: $ mv foo.html bar.html $ git rm foo.html rm 'foo.html' $ git add bar.html
In dieser Sequenz müssen Sie mv foo.html bar.html am Anfang ausführen, damit git rm die Datei foo.html nicht permanent aus dem Dateisystem löscht. Hier ist die gleiche Operation, ausgeführt mit git mv. $ git mv foo.html bar.html
Kurze Einführung in die Benutzung von Git
| 27
In beiden Fällen müssen die vorbereiteten Änderungen anschließend bestätigt werden: $ git commit -m "foo nach bar verschoben" Created commit 8805821: foo nach bar verschoben 1 files changed, 0 insertions(+), 0 deletions(-) rename foo.html => bar.html (100%)
Git verarbeitet Operationen zum Verschieben von Dateien anders als die meisten verwandten Systeme, indem es einen Mechanismus anwendet, der auf der Ähnlichkeit des Inhalts zwischen zwei Dateiversionen basiert. Die Eigenheiten dieses Ansatzes werden in Kapitel 5 beschrieben.
Eine Kopie Ihres Repository herstellen Falls Sie den vorangegangenen Schritten gefolgt sind und ein erstes Repository im Verzeichnis ~/public_html angelegt haben, können Sie nun eine vollständige Kopie oder einen Klon dieses Repository herstellen. Dabei hilft Ihnen der Befehl git clone. Auf diese Weise wird Git weltweit für das Arbeiten an Projekten mit denselben Dateien und für das Synchronisieren mit anderen Repositories eingesetzt. Für die Zwecke dieser Anleitung legen wir einfach eine Kopie Ihres Home-Verzeichnisses an und nennen diese meine_website: $ cd ~ $ git clone public_html meine_website
Diese beiden Git-Repositories enthalten nun zwar exakt die gleichen Objekte, Dateien und Verzeichnisse, aber gibt es einige feine Unterschiede. Sie könnten diese Unterschiede mit folgenden Befehlen untersuchen: $ ls -lsa public_html meine_website $ diff -r public_html meine_website
Auf einem lokalen Dateisystem ähnelt das Benutzen von git clone zum Erzeugen einer Kopie eines Repository cp -a oder rsync. Allerdings unterstützt Git eine größere Menge von Repository-Quellen, darunter Netzwerknamen, zum Benennen des zu klonenden Repository. Diese Formen und ihre Verwendung werden in Kapitel 11 erläutert. Nachdem Sie ein Repository geklont haben, können Sie die geklonte Version modifizieren, neue Commits durchführen, seine Logs und den Verlauf untersuchen usw. Es ist ein vollständiges Repository mit einem kompletten Verlauf (History).
Konfigurationsdateien Bei den Konfigurationsdateien von Git handelt es sich um einfache Textdateien im Stil der .ini-Dateien. Sie verzeichnen die verschiedenen Auswahlen und Einstellungen, die von vielen Git-Befehlen benutzt werden. Manche Einstellungen repräsentieren rein persönliche Vorlieben (Sollte ein color.pager verwendet werden?), andere sind wichtig,
28 | Kapitel 3: Erste Schritte
damit ein Repository richtig funktioniert (core.repositoryformatversion), während wieder andere das Verhalten von Befehlen ein wenig verändern (gc.auto). Wie viele Werkzeuge unterstützt auch Git eine Hierarchie von Konfigurationsdateien. In absteigender Rangordnung sind das die folgenden: .git/config Repository-spezifische Konfigurationseinstellungen, die mit der Option --file oder per Default manipuliert werden. Diese Einstellungen haben den höchsten Rang. ~/.gitconfig Benutzerspezifische Konfigurationseinstellungen, die mit der Option --global manipuliert werden. /etc/gitconfig Systemweite Konfigurationseinstellungen, die mit der Option --system manipuliert werden, wenn Sie die richtigen Unix-Dateischreibberechtigungen gesetzt haben. Diese Einstellungen haben den niedrigsten Rang. Je nach Ihrer tatsächlichen Installation kann die Systemeinstellungsdatei irgendwo anders sein, etwa unter /usr/local/ etc/gitconfig, sie kann aber auch komplett fehlen. Um z.B. einen Verfassernamen und eine E-Mail-Adresse für alle Commits in allen Repositories einzurichten, konfigurieren Sie mit dem Befehl git config --global Werte für user.name und user.email in der Datei $HOME/.gitconfig: $ git config --global user.name "Jon Loeliger" $ git config --global user.email "[email protected]"
Um dagegen einen Repository-spezifischen Namen und eine E-Mail-Adresse zu setzen, die eine --global-Einstellung außer Kraft setzen, lassen Sie einfach das Flag --global weg: $ git config user.name "Jon Loeliger" $ git config user.email "[email protected]"
Mit git config -l listen Sie die Einstellungen aller Variablen auf, die in allen Konfigurationsdateien zu finden sind: # $ $ $
Stellt ein brandneues, leeres Repository her mkdir /tmp/new cd /tmp/new git init
# $ $ $
Setzt einige Konfigurationswerte git config --global user.name "Jon Loeliger" git config --global user.email "[email protected]" git config user.email "[email protected]"
$ git config -l user.name=Jon Loeliger [email protected] core.repositoryformatversion=0 core.filemode=true
Konfigurationsdateien
| 29
core.bare=false core.logallrefupdates=true [email protected]
Da die Konfigurationsdateien einfach nur Textdateien sind, können Sie ihren Inhalt mit cat anschauen und mit Ihrem Lieblingstexteditor bearbeiten: # Zeigt nur die Repository-spezifischen Einstellungen an $ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [user] email = [email protected]
Benutzen Sie die Option --unset, um eine Einstellung zu entfernen: $ git config --unset --global user.email
Oft gibt es mehrere Konfigurationsoptionen und Umgebungsvariablen, die den gleichen Zweck erfüllen. Z.B. folgt der Editor, der zum Verfassen einer Commit-Lognachricht verwendet werden soll, diesen Schritten in der angezeigten Reihenfolge: 1. GIT_EDITOR-Umgebungsvariable 2. core.editor-Konfigurationsoption 3. VISUAL-Umgebungsvariable 4. EDITOR-Umgebungsvariable 5. vi-Befehl Es gibt mehrere Hundert Konfigurationsparameter. Ich werde Sie hier nicht mit ihnen langweilen, aber im Laufe der Zeit auf einige wichtige hinweisen. Eine umfassende (aber immer noch unvollständige) Liste finden Sie auf der git config-Manpage.
Einen Alias konfigurieren Für Anfänger kommt hier ein Tipp zum Einrichten von Befehlsaliasen. Wenn es einen gebräuchlichen, aber komplexen Git-Befehl gibt, den Sie häufig eintippen, sollten Sie einen einfachen Git-Alias dafür einrichten: $ git config --global alias.show-graph \ 'log --graph --abbrev-commit --pretty=oneline'
In diesem Beispiel habe ich den show-graph-Alias angelegt und zur Benutzung in all meinen Repositories freigegeben. Wenn ich nun den Befehl git show-graph verwende, ist das so, als hätte ich diesen langen git log-Befehl mit den ganzen Optionen eingetippt.
30 | Kapitel 3: Erste Schritte
Ausblick Sicher sind nach den hier durchgeführten Aktionen eine Menge Fragen zur Funktion von Git offen. Wie speichert Git z.B. die einzelnen Versionen einer Datei? Was passiert wirklich bei einem Commit? Woher kommen diese lustigen Commit-Nummern? Wieso heißt es master? Und ist ein »Zweig« wirklich das, was ich vermute? Gute Fragen. Im nächsten Kapitel werden einige Begriffe definiert, Git-Konzepte vorgestellt und die Grundlagen für die Lektionen im Rest des Buches gelegt.
Ausblick | 31
Kapitel 4
KAPITEL 4
Grundlegende Git-Konzepte
Grundlegende Konzepte Das vorangegangene Kapitel hat eine typische Anwendung von Git präsentiert – und wahrscheinlich einen Haufen Fragen aufgeworfen. Speichert Git bei jedem Commit die gesamte Datei? Was ist die Aufgabe des .git-Verzeichnisses? Wieso sieht eine Commit-ID so seltsam aus? Muss ich sie überhaupt zur Kenntnis nehmen? Wenn Sie schon einmal ein anderes Versionskontrollsystem (VCS) benutzt haben, etwa Subversion oder CVS, dann werden Ihnen die Befehle im letzten Kapitel wahrscheinlich vertraut vorgekommen sein. Git stellt schließlich auch die gleichen Funktionen und Operationen zur Verfügung wie jedes andere moderne VCS. Andererseits gibt es einige grundsätzliche und überraschende Unterschiede. In diesem Kapitel untersuchen wir, wieso und wo sich Git von anderen VCS unterscheidet, indem wir die wesentlichen Komponenten seiner Architektur sowie einige wichtige Konzepte betrachten. Wir konzentrieren uns hier auf die Grundlagen und demonstrieren, wie man mit einem Repository arbeitet. In Kapitel 11 wird dann erläutert, wie man mit vielen Repositories arbeitet, die miteinander verbunden sind. Es mag eine abschreckende Aussicht sein, den Überblick über mehrere Repositories behalten zu müssen, aber dabei gelten genau die Grundlagen, die Sie in diesem Kapitel kennenlernen werden.
Repositories Ein Git-Repository ist einfach eine Datenbank mit allen Informationen, die erforderlich sind, um die Revisionen und den Verlauf eines Projekts aufzubewahren und zu verwalten. Wie bei den meisten Versionskontrollsystemen enthält ein Repository in Git eine vollständige Kopie des gesamten Projekts während seiner ganzen Lebensdauer. Im Gegensatz zu den meisten anderen VCS jedoch bietet das Git-Repository nicht nur eine vollständige Arbeitskopie aller Dateien im Repository, sondern auch eine Kopie des Repository selbst, mit dem gearbeitet werden soll.
| 33
Git unterhält in jedem Repository einen Satz von Konfigurationswerten. Einige von ihnen haben Sie im vorangegangenen Kapitel bereits gesehen, etwa den Namen und die E-MailAdresse des Repository-Benutzers. Im Gegensatz zu den Dateidaten und anderen Repository-Metadaten werden die Konfigurationseinstellungen nicht während des Klonens oder Duplizierens von einem Repository zum nächsten weitergereicht. Stattdessen pflegt und untersucht Git die Informationen zum Konfigurieren und Einrichten je Site, je Benutzer und je Repository. Innerhalb eines Repository unterstützt Git zwei hauptsächliche Datenstrukturen, den Objektspeicher und den Index. All diese Repository-Daten werden in der Wurzel Ihres Arbeitsverzeichnisses in einem verborgenen Unterverzeichnis namens .git gespeichert. Der Objektspeicher soll während einer Klonoperation effizient kopiert werden. Er ist Teil des Mechanismus, der ein vollständig verteiltes VCS ermöglicht. Die Informationen im Index sind flüchtig, gehören nur zu einem Repository und können bei Bedarf erzeugt oder verändert werden. In den beiden nächsten Abschnitten werden der Objektspeicher und der Index näher beschrieben.
Git-Objekttypen Im Kern der Git-Repository-Implementierung befindet sich der Objektspeicher. Er enthält Ihre Datendateien im Original und alle Lognachrichten, Autoreninformationen, Datumsangaben und weiteren Informationen, die erforderlich sind, um eine beliebige Version oder Verzweigung eines Projekts wiederherzustellen. Git setzt nur vier Arten von Objekten in den Objektspeicher: Blobs, Bäume, Commits und Tags. Diese vier atomaren Objekte bilden die Grundlage der höheren Datenstrukturen von Git: Blobs Jede Version einer Datei wird als Blob dargestellt. »Blob« ist die Kurzform von »Binary Large Object« (großes binäres Objekt). Dieser Begriff wird in der Computertechnik häufig benutzt, um eine Variable oder Datei zu bezeichnen, die beliebige Daten enthalten kann und deren interne Struktur vom Programm ignoriert wird. Ein Blob wird als undurchsichtig betrachtet. Ein Blob enthält die Daten einer Datei, aber keine Metadaten über die Datei oder gar ihren Namen. Bäume Ein Baumobjekt repräsentiert eine Ebene von Verzeichnisinformationen. Es verzeichnet Blob-Identifikatoren, Pfadnamen und einige Metadaten für alle Dateien in einem Verzeichnis. Es kann außerdem rekursiv auf andere (Teil-)Baumobjekte verweisen und auf diese Weise eine komplette Hierarchie aus Dateien und Unterverzeichnissen aufbauen.
34 | Kapitel 4: Grundlegende Git-Konzepte
Commits Ein Commit-Objekt enthält Metadaten für jede Änderung, die am Repository vorgenommen wird. Das umfasst auch den Autor, denjenigen, der das Commit ausgeführt hat, den Zeitpunkt des Commit sowie eine Lognachricht. Jedes Commit verweist auf ein Baumobjekt, das in einem vollständigen Schnappschuss den Zustand des Repository zu dem Zeitpunkt erfasst, an dem das Commit ausgeführt wurde. Das erste Commit, auch als Root-Commit bezeichnet, besitzt kein Eltern-Commit. Die meisten Commits besitzen ein Eltern-Commit, allerdings erklären wir in Kapitel 9, wie ein Commit auf mehr als ein Eltern-Commit verweisen kann. Tags Ein Tag-Objekt weist einem bestimmten Objekt, meist einem Commit, einen beliebigen, aber vermutlich für den Benutzer lesbaren Namen zu. Zwar verweist 9da581d910c9c4ac93557ca4859e767f5caf5169 auf ein exaktes und wohldefiniertes Commit, ein vertrauterer Tag-Name wie Ver-1.0-Alpha ist aber sicher sinnvoller! Mit der Zeit ändern sich die Informationen im Objektspeicher und nehmen zu, wobei sie Ihre Bearbeitungsschritte, die Zusätze und Löschungen an Ihrem Projekt verfolgen und modellieren. Um den Festplattenplatz und die Netzwerkbandbreite effizient auszunutzen, komprimiert Git die Objekte und speichert sie in sogenannten Pack-Files, die ebenfalls im Objektspeicher abgelegt werden.
Index Der Index ist eine temporäre und dynamische Binärdatei, die die Verzeichnisstruktur des gesamten Repository beschreibt. Genauer gesagt: Der Index erfasst eine Version der Gesamtstruktur des Projekts zu einem beliebigen Zeitpunkt. Der Zustand des Projekts könnte durch ein Commit und einen Baum von einem beliebigen Punkt im Projektverlauf dargestellt werden, es könnte aber auch ein künftiger Zustand sein, den Sie aktiv entwickeln. Eines der wichtigsten Unterscheidungsmerkmale von Git besteht darin, dass es Ihnen erlaubt, den Inhalt des Index in systematischen, wohldefinierten Schritten zu verändern. Der Index erlaubt eine Trennung zwischen einzelnen Entwicklungsschritten und dem Bestätigen dieser Änderungen durch Commits. Und so funktioniert es. Als Entwickler führen Sie Git-Befehle aus, um Änderungen im Index bereitzustellen. Bei Änderungen werden normalerweise eine Datei oder eine ganze Gruppe von Dateien hinzugefügt, gelöscht oder bearbeitet. Der Index zeichnet diese Änderungen auf und speichert sie sicher, bis Sie bereit sind, sie zu bestätigen. Man kann Änderungen im Index auch ersetzen oder sie aus ihm entfernen. Das bedeutet, dass der Index, angeleitet durch Sie selbst, einen schrittweisen Übergang von einem komplexen Repository-Zustand zu einem anderen, vorzugsweise besseren, Zustand erlaubt.
Grundlegende Konzepte
| 35
Wie Sie in Kapitel 9 sehen werden, spielt der Index eine wichtige Rolle beim Zusammenführen, was es Ihnen ermöglicht, mehrere Versionen derselben Datei gleichzeitig zu verwalten, zu untersuchen und zu manipulieren.
Inhaltsadressierbare Namen Der Git-Objektspeicher ist als inhaltsadressierbares Speichersystem (Content-addressable Storage System, auch: Assoziativspeicher) organisiert und implementiert. Das bedeutet, dass jedes Objekt im Objektspeicher einen eindeutigen Namen besitzt, der durch das Anwenden von SHA1 auf den Inhalt des Objekts erzeugt wird, wodurch sich ein SHA1Hash-Wert ergibt. Da der komplette Inhalt eines Objekts zum Hash-Wert beiträgt, und da der Hash-Wert tatsächlich als eindeutig für diesen speziellen Inhalt betrachtet wird, ist der SHA1-Hash als Index oder Name für dieses Objekt in der Objektdatenbank ausreichend. Jede noch so winzige Änderung an einer Datei verursacht eine Änderung des SHA1-Hash, wodurch die neue Version der Datei separat indiziert wird. SHA1-Werte sind 160-Bit-Werte, die normalerweise als 40-stellige Hexadezimalzahl dargestellt werden, wie etwa 9da581d910c9c4ac93557ca4859e767f5caf5169. Manchmal werden die SHA1-Werte während der Anzeige zu einem kleineren, eindeutigen Präfix abgekürzt. Git-Benutzer sprechen wahlweise von SHA1, Hash-Code oder manchmal auch Objekt-ID.
Global eindeutige Identifikatoren Eine wichtige Eigenschaft der SHA1-Hash-Berechnung besteht darin, dass immer dieselbe ID für identischen Inhalt berechnet wird, unabhängig davon, wo sich dieser Inhalt befindet. Mit anderen Worten: Der gleiche Dateiinhalt in unterschiedlichen Verzeichnissen und sogar auf unterschiedlichen Maschinen ergibt die exakt gleiche SHA1-Hash-ID. Das bedeutet, dass die SHA1-Hash-ID einer Datei global eindeutig ist. Eine entscheidende Folge dieser Eigenschaft ist, dass Dateien oder Blobs beliebiger Größe über das Internet auf Gleichheit untersucht werden können, indem man einfach ihre SHA1-Identifikatoren vergleicht.
Git überwacht den Inhalt Ihnen sollte bewusst sein, dass Git mehr als ein Versionskontrollsystem ist: Git ist ein Content Tracking System (System zur Inhaltsüberwachung bzw. -verfolgung). Dieser Unterschied bestimmt, obschon er nur klein ist, zu einem Großteil das Design von Git und ist vielleicht die Hauptursache dafür, dass Git interne Datenmanipulationen relativ einfach durchführen kann. Allerdings ist es wahrscheinlich auch eines der schwierigsten Konzepte, die neue Benutzer verstehen müssen. Deshalb wollen wir ein wenig ausholen.
36 | Kapitel 4: Grundlegende Git-Konzepte
Die Inhaltsüberwachung von Git drückt sich in zwei entscheidenden Methoden aus, die sich grundlegend von fast allen anderen1 Revisionskontrollsystemen unterscheiden. Erstens beruht der Objektspeicher von Git auf der Hash-Berechnung des Inhalts seiner Objekte, nicht auf den Datei- oder Verzeichnisnamen des ursprünglichen Dateiaufbaus des Benutzers. Das bedeutet, wenn Git eine Datei im Objektspeicher ablegt, dann tut es dies auf der Grundlage des Hash der Daten und nicht auf der Grundlage des Namens der Datei. Um genau zu sein, überwacht Git auch die Datei- oder Verzeichnisnamen nicht, die mit den Dateien auf untergeordnete Weise verknüpft sind. Auch hier verfolgt Git den Inhalt anstelle der Dateien. Falls zwei separate Dateien, die sich in zwei unterschiedlichen Verzeichnissen befinden, exakt den gleichen Inhalt aufweisen, speichert Git eine einzige Kopie dieses Inhalts als Blob im Objektspeicher. Git berechnet den Hash-Code der einzelnen Dateien einzig anhand ihres Inhalts, stellt fest, dass die Dateien dieselben SHA1-Werte und damit den gleichen Inhalt besitzen, legt das Blob-Objekt in den Objektspeicher und indiziert es mit diesem SHA1-Wert. Beide Dateien im Projekt benutzen dieses Objekt für Ihren Inhalt, unabhängig davon, wo sie sich in der Verzeichnisstruktur des Benutzers befinden. Wenn sich eine dieser Dateien ändert, berechnet Git einen neuen SHA1-Wert für sie, stellt fest, dass es sich nun um ein anderes Blob-Objekt handelt, und fügt den neuen Blob in den Objektspeicher ein. Der ursprüngliche Blob verbleibt für die unveränderte Datei im Objektspeicher. Zweitens speichert Gits interne Datenbank effizient jede Version jeder Datei – nicht ihre Unterschiede –, wenn die Dateien von einer Revision zur nächsten übergehen. Da Git den Hash des ganzen Inhalts einer Datei als Namen für diese Datei benutzt, muss es auf jeder vollständigen Kopie der Datei arbeiten. Es kann seine Arbeit oder seine Objektspeichereinträge nicht nur auf einen Teil des Dateiinhalts oder auf die Unterschiede zwischen zwei Revisionen dieser Datei stützen. Die typische Benutzeransicht einer Datei – dass sie Revisionen aufweist und sich von einer Revision zur nächsten zu entwickeln scheint – ist einfach das Ergebnis. Git berechnet diesen Verlauf als Menge der Änderungen zwischen unterschiedlichen Blobs mit variierenden Hashes, anstatt einen Dateinamen und eine Menge von Unterschieden direkt zu speichern. Es wirkt vielleicht seltsam, aber diese Eigenschaft erlaubt Git, bestimmte Aufgaben mit Leichtigkeit durchzuführen.
Pfadname und Inhalt im Vergleich Genau wie viele andere VCS muss Git eine explizite Liste mit Dateien pflegen, die den Inhalt des Repository bilden. Allerdings erfordert das nicht, dass die Liste von Git auf Dateinamen beruht. Git behandelt die Namen der Dateien sogar als eine Form von 1
Monotone, Mercurial, OpenCMS und Venti sind hier nennenswerte Ausnahmen.
Grundlegende Konzepte
| 37
Daten, die sich vom Inhalt dieser Dateien unterscheiden. Auf diese Weise trennt es »Index« von »Daten« im traditionellen Datenbanksinn. Wahrscheinlich hilft es, einen Blick in Tabelle 4-1 zu werfen, wo Git mit anderen bekannten Systemen verglichen wird. Tabelle 4-1: Datenbankvergleich System
Indexmechanismus
Datenspeicher
Traditionelle Datenbank
ISAM
Daten-Records
Unix-Dateisystem
Verzeichnisse (/Pfad/zu/Datei)
Datenblöcke
Git
.git/objects/hash, Baumobjektinhalt
Blob-Objekte, Baumobjekte
Die Namen der Dateien und Verzeichnisse stammen aus dem zugrunde liegenden Dateisystem, allerdings kümmert sich Git eigentlich nicht um die Namen. Git zeichnet lediglich die einzelnen Pfadnamen auf und stellt sicher, dass es die Dateien und Verzeichnisse aus seinem Inhalt, den es anhand des Hash-Wertes indiziert hat, akkurat reproduzieren kann. Gits physischer Datenaufbau ist nicht nach der Dateiverzeichnisstruktur des Benutzers modelliert. Stattdessen weist es eine völlig andere Struktur auf, die nichtsdestotrotz den Originalaufbau des Benutzers wiedergeben kann. Die interne Datenstruktur von Git ist für seine eigenen internen Operationen und Speicherbetrachtungen viel effizienter. Wenn Git ein Arbeitsverzeichnis anlegen muss, sagt es dem Dateisystem: »Hallo! Ich habe diesen großen Blob mit Daten, der unter dem Pfadnamen Pfad/zu/Verzeichnis/Datei abgelegt werden soll. Klingt das für Dich sinnvoll?« Das Dateisystem ist dafür verantwortlich, zu antworten: »Ah, ja, ich erkenne diesen String als eine Menge von Unterverzeichnisnamen und weiß, wo ich den Blob mit Deinen Daten hinpacken soll! Danke!«
Darstellungen des Objektspeichers Wir wollen uns einmal anschauen, wie die Objekte von Git zusammenwirken, um das vollständige System zu formen. Das Blob-Objekt befindet sich ganz »unten« in der Datenstruktur; es verweist auf nichts und wird nur von Baumobjekten referenziert. In den folgenden Abbildungen wird ein Blob jeweils durch ein Rechteck dargestellt. Baumobjekte zeigen auf Blobs und möglicherweise auch auf andere Bäume. Auf jedes Baumobjekt kann auch von vielen unterschiedlichen Commit-Objekten verwiesen werden. Ein Baum wird jeweils durch ein Dreieck repräsentiert. Ein Kreis stellt ein Commit dar. Ein Commit verweist auf einen speziellen Baum, der durch das Commit in das Repository eingeführt wurde. Ein Tag (Markierung) wird durch ein Parallelogram dargestellt. Jedes Tag kann auf höchstens ein Commit verweisen.
38 | Kapitel 4: Grundlegende Git-Konzepte
Der Zweig ist kein grundlegendes Git-Objekt, spielt aber trotzdem eine wesentliche Rolle beim Benennen von Commits. Dargestellt wird ein Zweig als abgerundetes Rechteck. Abbildung 4-1 zeigt, wie alle Teile zusammenpassen. Sie sehen in diesem Diagramm den Zustand eines Repository, nachdem ein einzelnes, erstes Commit zwei Dateien hinzugefügt hat. Beide Dateien befinden sich im obersten Verzeichnis. Sowohl der master-Zweig als auch ein Tag namens V1.0 verweisen auf das Commit mit der ID 8675309. commit 1492 V1.0
tag 2504624
author Jon L tree 8675309
master
Initial commit
branch name
tree 8675309 blob dead23 blob feeb1e
blob dead23
Four score and seven …
Mary had a little lamb …
blob feeb1e
Abbildung 4-1: Git-Objekte
Machen wir es nun ein wenig komplizierter. Wir lassen die beiden Originaldateien so, wie sie sind, und fügen ein neues Unterverzeichnis mit einer Datei darin hinzu. Der resultierende Objektspeicher sieht aus wie in Abbildung 4-2. Wie im vorangegangenen Bild hat das neue Commit ein eigenes Baumobjekt hinzugefügt, mit dem der Gesamtzustand der Verzeichnis- und Dateistruktur dargestellt wird. In diesem Fall handelt es sich um das Baumobjekt mit der ID cafed00d. Da das oberste Verzeichnis durch das Hinzufügen des neuen Unterverzeichnisses geändert wurde, hat sich auch der Inhalt des obersten Baumobjekts geändert. Git führt deshalb einen neuen Baum ein, cafed00d. Allerdings haben sich die Blobs dead23 und feeb1e vom ersten zum zweiten Commit nicht geändert. Git erkennt, dass die IDs gleich geblieben sind, sodass sie direkt von dem neuen Baum cafed00d referenziert und benutzt werden können.
Darstellungen des Objektspeichers
| 39
V1.0
commit 11235
tag 2504624 commit 1492
author Jon L tree cafed00d parent 1492 Add a limerick
author Jon L tree 8675309
master
branch name
tree cafed00d
Initial commit
tree 8675309
tree 1010220 blob dead23 blob feeb1e
blob dead23 blob feeb1e
blob dead23
Four score and seven …
Mary had a little lamb …
blob feeb1e
tree 1010220 blob 1010b
There once was a man …
blob 1010b
Abbildung 4-2: Git-Objekte nach dem zweiten Commit
Achten Sie auf die Richtung der Pfeile zwischen den Commits. Das oder die Eltern-Commits kommen zeitlich früher. Deshalb verweist jedes Commit in der Git-Implementierung zurück auf sein(e) Eltern-Commit(s). Manche Leute verwirrt das, weil der Zustand eines Repository herkömmlicherweise in entgegengesetzter Richtung dargestellt wird: als Datenfluss vom Eltern-Commit zu den Kind-Commits. In Kapitel 6 werden wir diese Bilder erweitern, um zu zeigen, wie der Verlauf eines Repository aufgebaut und von den verschiedenen Befehlen manipuliert wird.
40 | Kapitel 4: Grundlegende Git-Konzepte
Git-Konzepte am Werk Nachdem wir nun einige grundsätzliche Fragen geklärt haben, wollen wir uns anschauen, wie all diese Konzepte und Komponenten im Repository selbst zusammenpassen. Legen wir dazu ein neues Repository an und untersuchen die internen Dateien und den Objektspeicher ausführlicher.
Im .git-Verzeichnis Initialisieren Sie zu Beginn ein leeres Repository mit dem Befehl git init und führen Sie dann find aus, um festzustellen, was erzeugt wurde: $ mkdir /tmp/hello $ cd /tmp/hello $ git init Initialized empty Git repository in /tmp/hello/.git/ # Alle Dateien im aktuellen Verzeichnis auflisten $ find . . ./.git ./.git/hooks ./.git/hooks/commit-msg.sample ./.git/hooks/applypatch-msg.sample ./.git/hooks/pre-applypatch.sample ./.git/hooks/post-commit.sample ./.git/hooks/pre-rebase.sample ./.git/hooks/post-receive.sample ./.git/hooks/prepare-commit-msg.sample ./.git/hooks/post-update.sample ./.git/hooks/pre-commit.sample ./.git/hooks/update.sample ./.git/refs ./.git/refs/heads ./.git/refs/tags ./.git/config ./.git/objects ./.git/objects/pack ./.git/objects/info ./.git/description ./.git/HEAD ./.git/branches ./.git/info ./.git/info/exclude
Wie Sie sehen, enthält .git schon ziemlich viele Dinge. Alle Dateien basieren auf einem Template-Verzeichnis, das Sie gegebenenfalls anpassen können. Je nach Ihrer Git-Version sieht Ihre tatsächliche Liste ein wenig anders aus. Z.B. benutzen ältere Versionen von Git nicht das Suffix .sample bei den .git/hooks-Dateien.
Git-Konzepte am Werk | 41
Im Allgemeinen müssen Sie die Dateien in .git nicht anschauen oder manipulieren. Diese »verborgenen« Dateien werden als Teil des Plumbing (quasi des Innenlebens) oder der Konfiguration von Git betrachtet. Git besitzt einige Plumbing-Befehle zum Verändern dieser verborgenen Dateien, die Sie aber nur selten benutzen werden. Zu Anfang ist das .git/objects-Verzeichnis (das Verzeichnis für alle Git-Objekte) leer, abgesehen von einigen Platzhaltern: $ find .git/objects .git/objects .git/objects/pack .git/objects/info
Wir wollen nun sorgfältig ein einfaches Objekt erzeugen: $ echo "hello world" > hello.txt $ git add hello.txt
Wenn Sie »hello world« genau so eingegeben haben, wie es hier gezeigt wird (ohne die Abstände oder die Groß- und Kleinschreibung zu ändern), dann sollte Ihr Objektverzeichnis nun so aussehen: $ find .git/objects .git/objects .git/objects/pack .git/objects/3b .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad .git/objects/info
Das sieht alles mysteriös aus, ist es aber nicht, wie in den folgenden Abschnitten erläutert wird.
Objekte, Hashes und Blobs Wenn Git ein Objekt für hello.txt anlegt, ist es ihm egal, dass der Dateiname hello.txt lautet. Git kümmert sich nur um das, was sich innerhalb der Datei befindet: die Folge von zwölf Bytes, die »hello world« wiedergeben, sowie das abschließende Newline-Zeichen (der gleiche Blob, der bereits früher erzeugt wurde). Git führt auf diesem Blob einige Operationen aus, berechnet seinen SHA1-Hash und gibt diesen als Datei in den Objektspeicher ein, deren Name die Hexadezimalrepräsentation des Hash ist. Der Hash lautet in diesem Fall 3b18e512dba79e4c8300dd08aeb37f8e728b8dad. Die 160 Bits eines SHA1-Hash entsprechen 20 Bytes, die eine 40 Byte lange Hexadezimalzahl für die Anzeige benötigen, sodass der Inhalt als .git/objects/3b/18e512dba79e4c8300dd08aeb3 7f8e728b8dad gespeichert wird. Git fügt nach den ersten beiden Stellen ein / ein, um die Effizienz des Dateisystems zu verbessern. (Manche Dateisysteme werden langsamer, wenn man zu viele Dateien in dasselbe Verzeichnis packt; indem man das erste Byte des SHA1 in ein Verzeichnis umwandelt, schafft man ganz leicht eine feste, 256-fache Partitionierung des Namensraums für alle möglichen Objekte bei gleichmäßiger Verteilung.)
42 | Kapitel 4: Grundlegende Git-Konzepte
Woher wissen wir, dass ein SHA1-Hash eindeutig ist? Es besteht eine außerordentlich geringe Wahrscheinlichkeit, dass zwei unterschiedliche Blobs denselben SHA1-Hash ergeben. Dieser Fall wird als Kollision bezeichnet. Allerdings sind SHA1-Kollisionen derartig unwahrscheinlich, dass Sie sicher davon ausgehen können, dass Ihnen so etwas bei der Benutzung von Git niemals unterkommen wird. SHA1 ist ein kryptographisch sicherer Hash. Bisher ist keine Möglichkeit (abgesehen von schierem Glück) bekannt, mit der ein Benutzer absichtlich eine Kollision verursachen könnte. Aber was ist mit zufälligen Kollisionen? Schauen wir einmal. Bei 160 Bits haben Sie 2160 oder etwa 1048 (1 mit 48 Nullen dahinter) mögliche SHA1Hashes. Diese Zahl ist einfach unglaublich riesig. Selbst wenn Sie eine Billion Leute anheuern, die eine Billion Jahre lang pro Sekunde eine Billion neuer, eindeutiger Blobs erzeugen, hätten Sie immer noch erst 1043 Blobs. Falls Sie für 280 zufällige Blobs Hash-Werte erzeugen, könnten Sie eine Kollision finden. Vertrauen Sie nicht uns. Lesen Sie Bruce Schneier.
Um zu zeigen, dass Git wirklich nichts mit dem Inhalt der Datei angestellt hat (es ist immer noch das vertraute »hello world«), können Sie den Hash benutzen, um die Datei wieder aus dem Objektspeicher herauszuholen: $ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello world
Git weiß auch, dass es ein wenig riskant ist, 40 Zeichen von Hand einzutippen, und bietet deshalb einen Befehl an, mit dem man Objekte anhand eines eindeutigen Präfix des Objekt-Hash abfragen kann: $ git rev-parse 3b18e512d 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
Dateien und Bäume Was passiert mit dem Dateinamen des »hello world«-Blobs, nachdem wir ihn sicher im Objektspeicher verstaut haben? Git wäre nicht besonders nützlich, wenn es Dateien nicht anhand ihres Namens suchen könnte. Wie bereits erwähnt, überwacht Git die Pfadnamen der Dateien mithilfe einer anderen Art von Objekt namens Baum. Wenn man git add ausführt, erzeugt Git für den Inhalt aller Dateien, die man hinzufügt, jeweils ein Objekt; allerdings legt es für den Baum nicht gleich ein Objekt an. Stattdessen aktualisiert es den Index. Der Index befindet sich in .git/ index und verfolgt die Dateipfadnamen und die dazugehörenden Blobs. Immer wenn man Befehle wie git add, git rm oder git mv ausführt, aktualisiert Git den Index mit dem neuen Pfadnamen und den Blob-Informationen.
Git-Konzepte am Werk | 43
Sie können zu einem beliebigen Zeitpunkt ein Baumobjekt aus Ihrem aktuellen Index erzeugen, indem Sie mit dem einfachen Befehl git write-tree einen Schnappschuss seiner aktuellen Informationen aufnehmen. Im Moment enthält der Index exakt eine Datei, hello.txt: $ git ls-files -s 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0
hello.txt
Hier können Sie die Verknüpfung zwischen der Datei hello.txt und dem Blob 3b18e5... sehen. Als Nächstes wollen wir den Zustand des Index erfassen und in einem Baumobjekt speichern: $ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 $ find .git/objects .git/objects .git/objects/68 .git/objects/68/aba62e560c0ebc3396e8ae9335232cd93a3f60 .git/objects/pack .git/objects/3b .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad .git/objects/info
Jetzt gibt es hier zwei Objekte: das »hello world«-Objekt unter 3b18e5 und ein neues, das Baumobjekt, unter 68aba6. Wie Sie sehen, entspricht der SHA1-Objektname exakt dem Unterverzeichnis und dem Dateinamen in .git/objects. Aber wie sieht ein Baum aus? Da er genau wie der Blob ein Objekt ist, können Sie zum Anschauen den gleichen einfachen Befehl benutzen: $ git cat-file -p 68aba6 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hello.txt
Der Inhalt des Objekts sollte sich leicht interpretieren lassen. Die erste Zahl, 100644, repräsentiert die Dateiattribute in Oktalschreibweise, was jedem vertraut sein dürfte, der schon einmal den Unix-Befehl chmod verwendet hat. 3b18e5 ist hier der Objektname des »hello world«-Blobs, und hello.txt ist der Name, der mit diesem Blob verknüpft ist. Es lässt sich leicht feststellen, dass das Baumobjekt die Informationen erfasst hat, die sich im Index befanden, als Sie git ls-files -s ausführten.
Ein Hinweis zur Verwendung von SHA1 durch Git Bevor wir uns den Inhalt des Baumobjekts genauer anschauen, wollen wir eine wichtige Eigenschaft von SHA1-Hashes untersuchen: $ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
44 | Kapitel 4: Grundlegende Git-Konzepte
$ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60 $ git write-tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
Jedes Mal, wenn Sie ein anderes Baumobjekt für den gleichen Index berechnen, erhalten Sie den exakt gleichen SHA1-Hash. Git muss kein neues Baumobjekt erstellen. Falls Sie diese Schritte an Ihrem Computer nachvollziehen, sollten Ihre SHA1-Hash-Werte exakt mit den Werten in diesem Buch übereinstimmen. In dieser Hinsicht ist die Hash-Funktion eine echte Funktion in mathematischem Sinn: Sie erzeugt für eine bestimmte Eingabe immer dieselbe Ausgabe. So eine Hash-Funktion wird manchmal als Digest bezeichnet, um die Tatsache zu betonen, dass sie als eine Art Zusammenfassung für das als Hash berechnete Objekt dient. Natürlich besitzt jede Hash-Funktion, selbst das bescheidene Paritätsbit, diese Eigenschaft. Das ist außerordentlich wichtig. Falls Sie z.B. genau den gleichen Inhalt erschaffen wie ein anderer Entwickler, dann ist ein identischer Hash Beweis genug, dass der komplette Inhalt identisch ist, unabhängig davon, wo oder wann Sie beide gearbeitet haben. Zumindest behandelt Git sie als identisch. Aber warten Sie – sind SHA1-Hash-Werte denn nicht eindeutig? Was ist mit der Billion Leute und der Billion Blobs pro Sekunde geschehen, die niemals eine Kollision verursachen? Diese Frage sorgt üblicherweise für Verwirrung unter neuen Git-Benutzern. Lesen Sie deshalb aufmerksam weiter, denn wenn Sie diesen Unterschied verstehen, ist alles andere in diesem Kapitel einfach. Identische SHA1-Hash-Werte zählen in diesem Fall nicht als Kollision. Es wäre nur dann eine Kollision, wenn zwei unterschiedliche Objekte den gleichen Hash erzeugen würden. Hier haben Sie zwei separate Instanzen des gleichen Inhalts hergestellt, und gleicher Inhalt besitzt immer gleiche Hash-Werte. Git ist von einer weiteren Folge der SHA1-Hash-Funktion abhängig: Es spielt keine Rolle, wie Sie einen Baum namens 68aba62e560c0ebc3396e8ae9335232cd93a3f60 bekommen haben. Wenn Sie ihn haben, können Sie sich ganz sicher darauf verlassen, dass alle anderen Leser dieses Buches ihn auch haben. Bob hat ihn möglicherweise erzeugt, indem er die Commits A und B von Jennie mit dem Commit C von Sergey kombiniert hat, während Sie Commit A von Sue und ein Update von Lakshmi erhalten haben, die die Commits B und C kombiniert hat. Die Ergebnisse sind identisch, und das ermöglicht und erleichtert die verteilte Entwicklung. Wenn Sie Objekt 68aba62e560c0ebc3396e8ae9335232cd93a3f60 suchen und es finden können, dann können Sie sich sicher sein, dass Sie genau die Daten vor sich haben, aus denen der Hash erzeugt wurde (weil SHA1 ein kryptographischer Hash ist). Auch das Gegenteil gilt: Falls Sie ein Objekt mit einem bestimmten Hash nicht in Ihrem Objektspeicher finden, dann können Sie sicher sein, dass Sie keine Kopie von genau diesem Objekt besitzen.
Git-Konzepte am Werk | 45
Das bedeutet, dass Sie feststellen können, ob Ihr Objektspeicher ein bestimmtes Objekt enthält oder nicht, obwohl Sie nichts über seinen (potenziell sehr großen) Inhalt wissen. Der Hash dient daher als zuverlässiges »Label« oder als Name für das Objekt. Git verlässt sich allerdings auch noch auf etwas Stärkeres als diese Schlussfolgerung. Betrachten Sie das neueste Commit (oder sein verknüpftes Baumobjekt). Da es als Teil seines Inhalts den Hash seines Eltern-Commit und seines Baums enthält, und da dieser wiederum den Hash all seiner Teilbäume und Blobs enthält, und zwar rekursiv durch die gesamte Datenstruktur, folgt also durch Induktion, dass der Hash des ursprünglichen Commit eindeutig den Zustand der gesamten Datenstruktur identifiziert, die in diesem Commit wurzelt. Die Implikationen meiner Behauptung aus dem vorhergehenden Absatz führen schließlich zu einer leistungsstarken Anwendung der Hash-Funktion: Sie bietet eine effiziente Möglichkeit, zwei Objekte, selbst zwei sehr große und komplexe Datenstrukturen, miteinander zu vergleichen, ohne beide vollständig zu übertragen2.
Baumhierarchien Es ist ja ganz nett, wenn man Informationen hat, die eine einzige Datei betreffen, wie wir im vorangegangenen Abschnitt gezeigt haben, allerdings enthalten Projekte komplexe, tief verschachtelte Verzeichnisse, die im Laufe der Zeit umstrukturiert und verschoben werden. Wir wollen uns anschauen, wie Git damit zurechtkommt, indem wir ein neues Unterverzeichnis anlegen, das eine identische Kopie der Datei hello.txt enthält: $ pwd /tmp/hello $ mkdir subdir $ cp hello.txt subdir/ $ git add subdir/hello.txt $ git write-tree 492413269336d21fac079d4a4672e55d5d2147ac $ git cat-file -p 4924132693 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 040000 tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60
hello.txt subdir
Der neue oberste Baum enthält zwei Elemente: die ursprüngliche hello.txt-Datei sowie das neue subdir-Verzeichnis, das vom Typ tree und nicht vom Typ blob ist. Bemerken Sie etwas Ungewöhnliches? Schauen Sie sich den Objektnamen von subdir genauer an. Es ist unser alter Freund 68aba62e560c0ebc3396e8ae9335232cd93a3f60! Was ist da gerade passiert? Der neue Baum für subdir enthält nur eine Datei, hello.txt, und diese Datei enthält das gleiche alte »hello world«. Der subdir-Baum ist also identisch
2
Diese Datenstruktur wird in »Commit-Graphen« auf Seite 81 genauer behandelt.
46 | Kapitel 4: Grundlegende Git-Konzepte
mit dem älteren obersten Baum! Und natürlich trägt er denselben SHA1-Objektnamen wie zuvor. Schauen wir uns das Verzeichnis .git/objects an und stellen fest, was die neueste Änderung beeinflusst hat: $ find .git/objects .git/objects .git/objects/49 .git/objects/49/2413269336d21fac079d4a4672e55d5d2147ac .git/objects/68 .git/objects/68/aba62e560c0ebc3396e8ae9335232cd93a3f60 .git/objects/pack .git/objects/3b .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad .git/objects/info
Es gibt immer noch nur drei eindeutige Objekte: einen Blob, der »hello world« enthält, einen Baum, der hello.txt enthält, das wiederum den Text »hello world« sowie ein Newline-Zeichen enthält, und einen zweiten Baum, der eine weitere Referenz auf hello.txt zusammen mit dem ersten Baum enthält.
Commits Das nächste Objekt, das wir hier besprechen wollen, ist das Commit. Nachdem hello.txt mit git add hinzugefügt wurde und mit git write-tree das Baumobjekt erzeugt wurde, können Sie mit einfachen Befehlen ein Commit-Objekt herstellen: $ echo -n "Bestaetige eine Datei, die hello sagt\n" \ | git commit-tree 492413269336d21fac079d4a4672e55d5d2147ac 3ede4622cc241bcb09683af36360e7413b9ddf6c
Das sieht dann in etwa so aus: $ git cat-file -p 3ede462 author Jon Loeliger <[email protected]> 1220233277 -0500 committer Jon Loeliger <[email protected]> 1220233277 -0500 Bestaetige eine Datei, die hello sagt
Wenn Sie diesen Schritt auf Ihrem Computer ebenfalls vollzogen haben, dann haben Sie wahrscheinlich festgestellt, dass das generierte Commit-Objekt nicht den gleichen Namen trägt wie in diesem Buch. Falls Sie alles bisher verstanden haben, sollte Ihnen der Grund dafür klar sein: Es ist nicht das gleiche Commit. Das Commit enthält Ihren Namen sowie den Zeitpunkt, zu dem Sie das Commit ausgeführt haben, weshalb es sich natürlich unterscheidet, wenn auch nur leicht. Andererseits hat Ihr Commit den gleichen Baum. Deshalb sind Commit-Objekte getrennt von ihren Baumobjekten: Unterschiedliche Commits beziehen sich oft auf exakt denselben Baum. In diesem Fall ist Git schlau genug, um nur das neue Commit-Objekt zu übertragen – das winzig ist –, anstatt die Baum- und Blob-Objekte anzufassen, die wahrscheinlich viel größer sind.
Git-Konzepte am Werk | 47
Im echten Leben können (und sollten!) Sie die git write-tree- und git commit-treeSchritte auslassen und nur den Befehl git commit benutzen. Sie müssen sich diese ganzen Plumbing-Befehle nicht merken, um ein absolut zufriedener Git-Benutzer zu werden. Ein grundlegendes Commit-Objekt ist ziemlich einfach und stellt die letzte Zutat für ein echtes Revisionskontrollsystem dar. Das gerade gezeigte Commit-Objekt ist das einfachste Commit-Objekt, das möglich ist. Es enthält • den Namen eines Baumobjekts, das die damit verknüpften Dateien wirklich identifiziert, • den Namen der Person, die die neue Version zusammengestellt hat (den Autor), sowie den Zeitpunkt, zu dem dies geschehen ist, • den Namen der Person, die die neue Version in das Repository gesetzt hat (den Committer), sowie den Zeitpunkt, zu dem dies geschehen ist, und • eine Beschreibung des Grundes für diese Revision (die Commit-Nachricht). Autor und Committer sind standardmäßig identisch; es kommt nur gelegentlich vor, dass sie sich unterscheiden. Mit dem Befehl git show --pretty=fuller können Sie sich zusätzliche Details zu einem bestimmten Commit anschauen.
Commit-Objekte werden ebenfalls in einer Graphenstruktur gespeichert, auch wenn sich diese völlig von den Strukturen unterscheidet, die von den Baumobjekten verwendet werden. Wenn Sie ein neues Commit ausführen, können Sie ihm ein oder mehrere ElternCommits zuweisen. Indem Sie dann die Kette der Eltern zurückverfolgen, erschließt sich Ihnen der Verlauf Ihres Projekts. Nähere Informationen über Commits und den CommitGraphen erhalten Sie in Kapitel 6.
Tags Das letzte Objekt schließlich, das Git verwaltet, ist das Tag (Markierung). Git implementiert nur eine Art von Tag-Objekt, allerdings gibt es zwei grundlegende Tag-Typen, die normalerweise als leicht (lightweight) und kommentiert (annotated) bezeichnet werden. Leichte Tags sind einfach Referenzen auf ein Commit-Objekt. Normalerweise werden sie als Repository-eigen betrachtet. Diese Tags erzeugen kein dauerhaftes Objekt im Objektspeicher. Ein kommentiertes Tag ist umfassender und erzeugt ein Objekt. Es enthält eine Nachricht, die Sie angeben, und kann mit einem GnuPG-Schlüssel entsprechend RFC4880 digital unterzeichnet werden.
48 | Kapitel 4: Grundlegende Git-Konzepte
Git behandelt beim Benennen eines Commit leichte und kommentierte Tag-Namen äquivalent. Allerdings funktionieren viele Git-Befehle standardmäßig nur mit kommentierten Tags, da sie als »dauerhafte« Objekte angesehen werden. Sie erzeugen ein kommentiertes, unsigniertes Tag mit einer Nachricht auf einem Commit mit dem Befehl git tag: $ git tag -m"Tag version 1.0" V1.0 3ede462
Sie können das Tag-Objekt mithilfe des Befehls git cat-file -p sehen, aber wie lautet der SHA1 des Tag-Objekts? Um ihn herauszufinden, greifen Sie auf den Tipp aus »Objekte, Hashes und Blobs« auf Seite 42 zurück. $ git rev-parse V1.0 6b608c1093943939ae78348117dd18b1ba151c6a $ git cat-file -p 6b608c object 3ede4622cc241bcb09683af36360e7413b9ddf6c type commit tag V1.0 tagger Jon Loeliger <[email protected]> Sun Oct 26 17:07:15 2008 -0500 Tag version 1.0
Neben der Lognachricht und der Autoreninformation bezieht sich das Tag auf das Commit-Objekt 3ede462. Normalerweise markiert Git ein bestimmtes Commit mit einem Tag, das sich von einem Zweig herleitet. Dieses Verhalten unterscheidet sich deutlich von dem anderer VCS. Meist markiert Git ein Commit-Objekt, das auf ein Baumobjekt veweist, das den Gesamtzustand der kompletten Hierarchie der Dateien und Verzeichnisse in Ihrem Repository umfasst. Erinnern Sie sich an Abbildung 4-1: Das Tag V1.0 verweist auf das Commit namens 1492, das wiederum auf einen Baum (8675309) zeigt, der sich über mehrere Dateien erstreckt. Daher gilt das Tag gleichzeitig für alle Dateien dieses Baums. Das ist ganz anders als z.B. bei CVS, das ein Tag auf jede einzelne Datei anwendet und sich dann darauf verlässt, dass die Sammlung all dieser markierten Dateien eine komplette markierte Revision wiederherstellt. Und während CVS Ihnen erlaubt, das Tag auf einer Datei zu verschieben, verlangt Git ein neues Commit, das die Änderung des Dateizustands auslöst, woraufhin das Tag verschoben wird.
Git-Konzepte am Werk | 49
Kapitel 5
KAPITEL 5
Dateiverwaltung und der Index
Wenn Ihr Projekt dann unter dem Dach eines Versionskontrollsystems angekommen ist, arbeiten Sie in Ihrem Arbeitsverzeichnis und legen Ihre Änderungen zur Sicherung in das Repository. Git funktioniert ähnlich, schiebt aber noch eine weitere Ebene, nämlich den Index, zwischen das Arbeitsverzeichnis und das Repository, um die Änderungen bereitzustellen oder zu sammeln. Wenn Sie Ihren Code mit Git verwalten, arbeiten Sie im Arbeitsverzeichnis, sammeln die Änderungen im Index und übertragen dann mit einem Commit all das, was sich im Index angehäuft hat, in einem Schwung in das Repository. Stellen Sie sich den Index von Git als eine Menge geplanter oder voraussichtlicher Modifikationen vor. Sie fügen Dateien hinzu und entfernen, verschieben oder bearbeiten sie wiederholt, bis es schließlich zum Commit kommt, das die gesammelten Änderungen dann in das Repository überträgt. Der Großteil der wichtigen Arbeit geht dem eigentlichen Commit-Schritt voran. Merken Sie sich, dass ein Commit ein zweiteiliger Vorgang ist: Bereitstellen der Änderungen und Bestätigen der Änderungen. Eine Änderung, die zwar im Arbeitsverzeichnis zu finden ist, aber nicht im Index, ist nicht bereitgestellt und kann daher nicht bestätigt werden. Aus Gründen der Bequemlichkeit erlaubt Ihnen Git, die beiden Schritte zu kombinieren, wenn Sie eine Datei hinzufügen oder ändern: $ git commit index.html
Diesen Luxus genießen Sie allerdings nicht, wenn Sie eine Datei verschieben oder entfernen. Die beiden Schritte müssen dann getrennt werden: $ git rm index.html $ git commit
Dieses Kapitel1 erklärt, wie Sie den Index und Ihre Sammlung von Dateien verwalten. Es beschreibt, wie Sie eine Datei zu Ihrem Repository hinzufügen und aus ihm entfernen, 1
Ich weiß aus zuverlässiger Quelle, dass dieses Kapitel eigentlich Alles, was Bart Massey an Git hasst heißen sollte.
| 51
wie Sie eine Datei umbenennen und wie Sie den Zustand des Index katalogisieren. Am Ende dieses Kapitels wird Ihnen gezeigt, wie Sie Git dazu bringen, temporäre und andere irrelevante Dateien zu ignorieren, die nicht der Versionskontrolle unterstellt werden müssen.
Es geht um den Index Linus Torvalds hat auf der Git-Mailingliste angeführt, dass man die Stärke von Git nicht begreifen und richtig schätzen kann, wenn man vorher nicht den Sinn des Index verstanden hat. Gits Index enthält keinen Dateiinhalt; er verfolgt einfach, was Sie mit Commits bestätigen wollen. Wenn man git commit ausführt, überprüft Git nicht das Arbeitsverzeichnis, sondern den Index, um festzustellen, was bestätigt werden soll. (Commits werden in Kapitel 6 ausführlich behandelt.) Auch wenn viele der höheren Befehle von Git die Einzelheiten des Index vor Ihnen verbergen und Ihnen die Arbeit erleichtern sollen, müssen Sie den Index und seinen Zustand im Auge behalten. Sie können den Zustand des Index jederzeit mit git status abfragen. Der Befehl sucht ausdrücklich die Dateien heraus, die Git als bereitgestellt betrachtet. Mit »Plumbing«-Befehlen wie git ls-files können Sie außerdem den internen Zustand von Git untersuchen. Wahrscheinlich werden Sie während der Bereitstellung den Befehl git diff recht nützlich finden. (Diffs werden ausführlich in Kapitel 8 besprochen.) Dieser Befehl kann zwei unterschiedliche Gruppen von Änderungen anzeigen: git diff zeigt diejenigen Änderungen an, die in Ihrem Arbeitsverzeichnis verbleiben und nicht bereitgestellt werden, und git diff --cached zeigt diejenigen an, die bereitgestellt und daher beim nächsten Commit verarbeitet werden. Sie können sich von beiden git diff-Varianten durch den Prozess des Bereitstellens von Änderungen leiten lassen. Zu Anfang stellt git diff eine große Menge mit allen Modifikationen dar, --cached dagegen ist leer. Wenn Sie Änderungen für das Commit bereitstellen, verkleinert sich die erste Menge, und die zweite wächst an. Sobald alle Änderungen bereitgestellt sind und auf das Commit warten, ist --cached voll, und git diff zeigt nichts mehr an.
Dateiklassifikationen in Git Git teilt Ihre Dateien in drei Gruppen ein: Tracked (verfolgt) Verfolgte Dateien sind alle Dateien, die sich bereits im Repository befinden, oder alle Dateien, die im Index bereitgestellt wurden. Um eine neue Datei, eineDatei, zu dieser Gruppe hinzuzufügen, führen Sie git add eineDatei aus.
52 | Kapitel 5: Dateiverwaltung und der Index
Ignored (ignoriert) Eine ignorierte Datei muss im Repository ausdrücklich als »invisible« oder »ignored« deklariert werden, auch wenn sie in Ihrem Arbeitsverzeichnis vorhanden ist. In einem Softwareprojekt gibt es mit der Zeit meist eine ganze Menge an ignorierten Dateien. Dazu gehören u. a. temporäre Dateien, Arbeitsdateien, persönliche Notizen, Compiler-Ausgaben und die meisten Dateien, die beim Kompilieren automatisch generiert werden. Git pflegt eine vorgegebene Liste mit zu ignorierenden Dateien, und Sie können Ihr Repository dann so konfigurieren, dass es weitere erkennt. Ignorierte Dateien werden in »Die Datei .gitignoreT« auf Seite 63 ausführlich besprochen. Untracked (nicht verfolgt) Eine nicht verfolgte Datei ist jede Datei, die nicht in einer der zwei genannten Kategorien zu finden ist. Git betrachtet alle Dateien in Ihrem Arbeitsverzeichnis und zieht sowohl die verfolgten als auch die ignorierten Dateien ab, um die Dateien zu erhalten, die als nicht verfolgt gelten. Wir wollen die unterschiedlichen Kategorien von Dateien untersuchen, indem wir ein brandneues Arbeitsverzeichnis sowie ein Repository anlegen und dann mit einigen Dateien arbeiten: $ cd /tmp/my_stuff $ git init $ git status # On branch master # # Initial commit # nothing to commit (create/copy files and use "git add" to track) $ echo "Neue Daten" > daten $ git status # On branch master # # Initial commit # # Untracked files: # (use "git add ..." to include in what will be committed) # # daten nothing added to commit but untracked files present (use "git add" to track)
Zu Anfang gibt es noch keine Dateien, und die verfolgten, ignorierten und nicht verfolgten Mengen sind leer. Nachdem Sie daten erzeugt haben, zeigt git status eine einzelne, nicht verfolgte Datei an. Editoren und Build-Umgebungen hinterlassen oft temporäre oder flüchtige Dateien in Ihrem Quellcode. Solche Dateien sollen gewöhnlich nicht als »Quelldateien« in einem
Dateiklassifikationen in Git
| 53
Repository verfolgt werden. Damit Git eine Datei in einem Verzeichnis ignoriert, fügen Sie den Namen dieser Datei einfach der speziellen Datei .gitignore hinzu: # Manuelles Erzeugen einer Beispieldatei zum Ignorieren $ touch main.o $ # # # # # # # # #
git status On branch master Initial commit Untracked files: (use "git add ..." to include in what will be committed) daten main.o
$ echo main.o >> .gitignore $ # # # # # # # # #
git status On branch master Initial commit Untracked files: (use "git add ..." to include in what will be committed) .gitignore daten
main.o wird also ignoriert, git status zeigt nun aber eine neue, nicht verfolgte Datei namens .gitignore. Die Datei .gitignore hat zwar für Git eine besondere Bedeutung, wird aber wie jede andere normale Datei in Ihrem Repository verwaltet. Bis .gitignore hinzugefügt wird, betrachtet Git sie als nicht verfolgt. In den nächsten Abschnitten demonstrieren wir, wie sich der Verfolgungsstatus einer Datei ändern lässt und wie man eine Datei zum Index hinzufügt bzw. aus ihm entfernt.
git add benutzen Der Befehl git add stellt eine Datei bereit. Im Hinblick auf die Git-Dateiklassifikation ist es so, dass git add den Status der Datei auf verfolgt setzt, wenn sie bisher nicht den Status verfolgt hatte. Wird git add auf einem Verzeichnisnamen eingesetzt, dann werden alle Dateien und Unterverzeichnisse in diesem Verzeichnis rekursiv bereitgestellt. Betrachten wir wieder unser Beispiel aus dem vorangegangenen Abschnitt: $ git status # On branch master # # Initial commit #
54 | Kapitel 5: Dateiverwaltung und der Index
# Untracked files: # (use "git add ..." to include in what will be committed) # # .gitignore # daten # Beide neuen Dateien verfolgen. $ git add data .gitignore $ # # # # # # # # # #
git status On branch master Initial commit Changes to be committed: (use "git rm --cached ..." to unstage) new file: .gitignore new file: daten
Das erste git status zeigt Ihnen, dass zwei Dateien nicht verfolgt werden, und erinnert Sie daran, dass Sie nur git add ausführen müssen, um eine Datei zu verfolgen. Nach git add sind daten und .gitignore bereitgestellt und verfolgt und können mit dem nächsten Commit in das Repository eingefügt werden. In Bezug auf das Git-Objektmodell verhält es sich folgendermaßen: In dem Augenblick, in dem Sie git add ausführen, wird die Datei komplett in den Objektspeicher kopiert und mit ihrem resultierenden SHA1-Namen indiziert. Zum Bereitstellen einer Datei sagt man auch »Caching einer Datei«2 oder »eine Datei in den Index setzen«. Mit git ls-files können Sie dem Objektmodell unter die Haube schauen und die SHA1Werte für diese bereitgestellten Dateien ermitteln: $ git ls-files --stage 100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 100644 534469f67ae5ce72a7a274faf30dee3c2ea1746d 0
.gitignore daten
Bei den meisten der täglichen Änderungen innerhalb Ihres Repository wird es sich wahrscheinlich um einfache Bearbeitungen handeln. Nach dem Bearbeiten und vor dem Bestätigen der Änderungen mit Commit führen Sie git add aus, um den Index mit der absolut neuesten und tollsten Version Ihrer Datei zu aktualisieren. Wenn Sie das versäumen, werden Sie zwei unterschiedliche Versionen der Datei haben: eine, die im Objektspeicher erfasst und vom Index referenziert ist, und die andere in Ihrem Arbeitsverzeichnis. Um mit dem Beispiel fortzufahren, ändern wir nun die Datei daten, damit sie sich von der Version im Index unterscheidet, und benutzen den geheimnisvollen Befehl git hashobject Datei (den Sie nur äußerst selten direkt aufrufen werden), um den SHA1-Hash für die neue Version direkt zu berechnen und auszugeben: 2
Sie haben --cached in der git status-Ausgabe gesehen, oder?
git add benutzen
| 55
$ git ls-files --stage 100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 100644 534469f67ae5ce72a7a274faf30dee3c2ea1746d 0
.gitignore daten
# Bearbeiten von "daten" ... $ cat daten Neue Daten Und weitere Daten $ git hash-object daten e476983f39f6e4f453f0fe4a859410f63b58b500
Nachdem die Datei ergänzt wurde, besitzt die vorherige Version der Datei im Objektspeicher und im Index den SHA1-Wert 534469f67ae5ce72a7a274faf30dee3c2ea1746d. Die aktualisierte Version der Datei jedoch besitzt den SHA1-Wert e476983f39f6e4f453f0fe4a 859410f63b58b500. Wir ändern nun den Index, damit er ebenfalls die neue Version enthält: $ git add daten $ git ls-files --stage 100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 100644 e476983f39f6e4f453f0fe4a859410f63b58b500 0
.gitignore daten
Der Index enthält nun die aktualisierte Version der Datei. Das heißt, »Die Datei daten wurde bereitgestellt« oder lässig ausgedrückt: »Die Datei daten ist im Index«. Die zweite Wendung ist nicht ganz akkurat, weil die Datei sich eigentlich im Objektspeicher befindet und der Index lediglich darauf verweist. Das scheinbar nutzlose Spiel mit SHA1-Hashes und dem Index bringt uns auf einen wesentlichen Punkt: Stellen Sie sich git add nicht als »Füge diese Datei hinzu« vor, sondern eher als »Füge diesen Inhalt hinzu«. Auf jeden Fall müssen Sie sich merken, dass die Version der Datei in Ihrem Arbeitsverzeichnis nicht unbedingt synchron mit der Version sein muss, die im Index bereitgestellt wird. Wenn es Zeit für das Commit wird, greift Git auf die Version im Index zurück. Die Option --interactive für git add bzw. git commit kann Ihnen gute Dienste leisten, wenn Sie untersuchen wollen, welche Dateien Sie für ein Commit bereitstellen wollen.
Einige Hinweise zur Benutzung von git commit git commit --all benutzen Die Option -a oder --all für git commit sorgt dafür, dass automatisch alle nicht bereitgestellten, verfolgten Dateiänderungen bereitgestellt werden – einschließlich der Löschungen verfolgter Dateien aus der Arbeitskopie –, bevor das Commit ausführt wird.
56 | Kapitel 5: Dateiverwaltung und der Index
Wir wollen uns anschauen, wie das funktioniert, indem wir einige Dateien mit unterschiedlichen Bereitstellungseigenschaften einrichten: # Die Datei "fertig" bearbeiten und mit "git add" zum Index hinzufügen # edit fertig $ git add fertig # Die Datei "nochnicht" bearbeiten und nicht bereitstellen # edit nochnicht # Eine neue Datei in ein Unterverzeichnis einfügen, aber nicht in den Index einfügen $ mkdir subdir $ echo Nope >> subdir/neu
Schauen Sie sich mit git status an, was ein normales Commit (ohne Kommandozeilenoptionen) tun würde: $ # # # # # # # # # # # # # # #
git status On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) modified:
fertig
Changed but not updated: (use "git add ..." to update what will be committed) modified:
nochnicht
Untracked files: (use "git add ..." to include in what will be committed) subdir/
Der Index ist hier nur darauf vorbereitet, die Datei namens fertig mit dem Commit zu bestätigen, da es die einzige Datei ist, die bereitgestellt wurde. Falls Sie jedoch git commit --all ausführen, durchläuft Git rekursiv das gesamte Repository, stellt alle bekannten veränderten Dateien bereit und bestätigt sie. Wenn Ihnen in diesem Fall Ihr Editor das Template der Commit-Nachricht präsentiert, sollte zu erkennen sein, dass die geänderte und bekannte Datei nochnicht ebenfalls bestätigt wird: # # # # # # # # # # # # #
Please enter the commit message for your changes. (Comment lines starting with '#' will not be included) On branch master Changes to be committed: (use "git reset HEAD ..." to unstage) modified: modified:
nochnicht fertig
Untracked files: (use "git add ..." to include in what will be committed) subdir/
Einige Hinweise zur Benutzung von git commit | 57
Und da schließlich das Verzeichnis namens subdir neu ist und kein Dateiname oder Pfad darin verfolgt wird, kann nicht einmal die Option --all veranlassen, dass es bestätigt wird: Created commit db7de5f: Ein --all-Ding. 2 files changed, 2 insertions(+), 0 deletions(-)
Git durchläuft zwar rekursiv das Repository und sucht nach geänderten und gelöschten Dateien, das völlig neue Verzeichnis subdir/ mit all seinen Dateien wird jedoch nicht zu einem Bestandteil des Commit.
Commit-Lognachrichten schreiben Falls Sie nicht direkt auf der Kommandozeile eine Lognachricht angeben, startet Git einen Editor und fordert Sie auf, eine Nachricht zu schreiben. Welcher Editor gewählt wird, steht in Ihrer Konfiguration, wie in »Konfigurationsdateien« auf Seite 28 beschrieben ist. Wenn Sie gerade im Editor eine Commit-Lognachricht schreiben und aus irgendwelchen Gründen beschließen, die Operation abzubrechen, dann beenden Sie einfach den Editor, ohne zu speichern. Sie erhalten eine leere Lognachricht. Ist es dazu zu spät, weil Sie bereits gespeichert haben, dann löschen Sie einfach die gesamte Lognachricht und speichern Sie noch einmal. Git verarbeitet ein leeres Commit (ohne Text) nicht.
git rm benutzen Der Befehl git rm ist natürlich das Gegenteil von git add. Er löscht eine Datei sowohl aus dem Repository als auch aus dem Arbeitsverzeichnis. Da das Löschen einer Datei jedoch problematischer sein könnte (falls etwas schiefgeht) als das Hinzufügen einer Datei, geht Git beim Löschen einer Datei etwas sorgfältiger zu Werke. Git löscht eine Datei entweder aus dem Index oder aus dem Index und dem Arbeitsverzeichnis gleichzeitig. Git löscht eine Datei niemals nur aus dem Arbeitsverzeichnis; für diesen Zweck muss der normale rm-Befehl benutzt werden. Beim Löschen einer Datei aus Ihrem Verzeichnis sowie aus dem Index wird der Verlauf der Datei nicht aus dem Repository entfernt. Alle Versionen der Datei, die Teil des Verlaufs sind, der bereits im Repository bestätigt ist, bleiben im Objektspeicher und tragen zu diesem Verlauf bei. Wir wollen nun in Fortführung unseres Beispiels eine »versehentlich« hinzugefügte Datei einführen, die nicht bereitgestellt und irgendwie entfernt werden soll: $ echo "Kram" > oops # Can't "git rm" files Git considers "other" # This should be just "rm oops" $ git rm oops fatal: pathspec 'oops' did not match any files
58 | Kapitel 5: Dateiverwaltung und der Index
Da git rm ebenfalls eine Operation auf dem Index ist, funktioniert der Befehl nicht mit einer Datei, die nicht zuvor dem Repository oder dem Index hinzugefügt wurde; Git muss eine Datei zuerst erkennen. Wir wollen deshalb die Datei oops aus Versehen bereitstellen: # Versehentlich die Datei "oops" bereitstellen $ git add oops $ # # # # # # # # # # #
git status On branch master Initial commit Changes to be committed: (use "git rm --cached ..." to unstage) new file: .gitignore new file: daten new file: oops
Um eine bereitgestellte Datei in eine nicht bereitgestellte Datei umzuwandeln, benutzen Sie git rm --cached: $ git ls-files --stage 100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 100644 e476983f39f6e4f453f0fe4a859410f63b58b500 0 100644 fcd87b055f261557434fa9956e6ce29433a5cd1c 0
.gitignore daten oops
$ git rm --cached oops rm 'oops' $ git ls-files --stage 100644 0487f44090ad950f61955271cf0a2d6c6a83ad9a 0 100644 e476983f39f6e4f453f0fe4a859410f63b58b500 0
.gitignore daten
Während git rm --cached die Datei aus dem Index entfernt und im Arbeitsverzeichnis belässt, löscht git rm die Datei sowohl aus dem Index als auch aus dem Arbeitsverzeichnis. Es ist gefährlich, git rm --cached zu benutzen, um eine Datei zu einer nicht verfolgten Datei zu machen, und dabei eine Kopie im Arbeitsverzeichnis zu lassen, da man leicht vergessen kann, dass sie nicht mehr verfolgt wird. Diese Vorgehensweise umgeht außerdem Gits Test, ob der Inhalt der Arbeitsdatei aktuell ist. Seien Sie also vorsichtig.
Falls Sie eine Datei nach dem Bestätigen löschen wollen, stellen Sie die Löschanforderung durch ein einfaches git rm Dateiname bereit: $ git commit -m "Einige Dateien hinzufügen" Created initial commit 5b22108: Einige Dateien hinzufügen 2 files changed, 3 insertions(+), 0 deletions(-)
git rm benutzen
| 59
create mode 100644 .gitignore create mode 100644 daten $ git rm daten rm 'daten' $ git status # On branch master # Changes to be committed: # (use "git reset HEAD ..." to unstage) # # deleted: daten #
Bevor Git eine Datei löscht, überprüft es, ob die Version der Datei im Arbeitsverzeichnis der neuesten Version im aktuellen Zweig entspricht (der Version, die die Git-Befehle als HEAD bezeichnen). Diese Überprüfung soll einem versehentlichen Verlust der Änderungen (aufgrund Ihrer Bearbeitung) vorbeugen, die an der Datei vorgenommen wurden. Damit git rm funktioniert, muss die Datei im Arbeitsverzeichnis entweder dem HEAD oder dem Inhalt des Index entsprechen. Verwenden Sie git rm -f, um das Löschen Ihrer Datei zu erzwingen. Diese Option stellt einen ausdrücklichen Auftrag dar und löscht die Datei selbst dann, wenn Sie sie nach Ihrem letzten Commit geändert haben.
Und falls Sie eine Datei, die Sie versehentlich gelöscht haben, wirklich wiederhaben wollen, dann fügen Sie sie einfach wieder hinzu: $ git add daten fatal: pathspec 'daten' did not match any files
Verdammte Axt! Git hat auch die Arbeitskopie gelöscht! Aber keine Panik. Versionskontrollsysteme sind wirklich gut darin, alte Versionen von Dateien wiederherzustellen: $ git checkout HEAD -- daten $ cat daten Neue Daten Und weitere Daten $ git status # On branch master nothing to commit (working directory clean)
git mv benutzen Nehmen Sie einmal an, Sie müssen eine Datei verschieben oder umbenennen. Sie könnten eine Kombination aus git rm auf der alten Datei und git add auf der neuen Datei benutzen. Oder Sie setzen direkt git mv ein. Bei einem Repository mit der Datei Kram, die
60 | Kapitel 5: Dateiverwaltung und der Index
Sie in neuerKram umbenennen wollen, sind die folgenden Befehlsfolgen äquivalente GitOperationen: $ mv stuff neuerKram $ git rm Kram $ git add neuerKram
und $ git mv Kram neuerKram
In beiden Fällen entfernt Git den Pfadnamen Kram aus dem Index, fügt den neuen Pfadnamen neuerKram hinzu, behält den ursprünglichen Inhalt von Kram im Objektspeicher und verknüpft diesen Inhalt neu mit dem Pfadnamen neuerKram. Nun, da daten wieder zurück im Beispiel-Repository ist, wollen wir es umbenennen und die Änderung bestätigen: $ git mv daten meinedaten $ git status # On branch master # Changes to be committed: # (use "git reset HEAD ..." to unstage) # # renamed: daten -> meinedaten # $ git commit -m"daten nach meinedaten verschoben" Created commit ec7d888: daten nach meinedaten verschoben 1 files changed, 0 insertions(+), 0 deletions(-) rename daten => meinedaten (100%)
Wenn Sie irgendwann den Verlauf der Datei überprüfen, könnte es Sie irritieren, dass Git augenscheinlich den Verlauf der ursprünglichen daten-Datei verloren hat und sich nur noch erinnern kann, daten in den aktuellen Namen umbenannt zu haben: $ git log meinedaten commit ec7d888b6492370a8ef43f56162a2a4686aea3b4 Author: Jon Loeliger <[email protected]> Date: Sun Nov 2 19:01:20 2008 -0600 daten nach meinedaten verschoben
Git kennt den vollständigen Verlauf noch, allerdings beschränkt sich die Ausgabe auf den von Ihnen im Befehl angegebenen Dateinamen. Die Option --follow fordert Git auf, durch das Log zurückzugehen und den gesamten Verlauf zu ermitteln, der mit dem Inhalt verknüpft ist: $ git log --follow meinedaten commit ec7d888b6492370a8ef43f56162a2a4686aea3b4 Author: Jon Loeliger <[email protected]> Date: Sun Nov 2 19:01:20 2008 -0600
git mv benutzen | 61
daten nach meinedaten verschoben commit 5b22108820b6638a86bf57145a136f3a7ab71818 Author: Jon Loeliger <[email protected]> Date: Sun Nov 2 18:38:28 2008 -0600 Einige Dateien hinzufügen
Eines der klassischen Probleme von Versionskontrollsystemen besteht darin, dass das Umbenennen einer Datei dazu führen kann, dass sie den Verlauf der Datei aus den Augen verlieren. Git bewahrt diese Informationen selbst nach einem Umbenennen auf.
Ein Hinweis zum Überwachen des Umbenennens Lassen Sie uns ein wenig darüber reden, wie Git das Umbenennen von Dateinamen überwacht. Subversion, ein Beispiel für die traditionelle Revisionskontrolle, muss sich mächtig anstrengen, um den Überblick zu behalten, wenn eine Datei umbenannt und verschoben wird, da es eigentlich nur die Diffs, also die Unterschiede zwischen den Dateien verfolgt. Wenn Sie eine Datei verschieben, dann ist das im Prinzip genauso, als würden Sie alle Zeilen aus der alten Datei löschen und sie der neuen Datei hinzufügen. Es wäre allerdings ziemlich ineffizient, wenn man den gesamten Inhalt einer Datei immer wieder übertragen und speichern müsste, nur weil man der Datei einen neuen Namen verpassen möchte – stellen Sie sich einmal vor, Sie wollten ein ganzes Unterverzeichnis mit Tausenden von Dateien umbenennen. Um diese Lage zu erleichtern, verfolgt Subversion explizit jedes Umbenennen. Falls Sie hello.txt in subdir/hello.txt umbenennen wollen, müssen Sie svn mv anstelle von svn rm und svn add verwenden. Ansonsten hat Subversion keine Möglichkeit zu erkennen, dass es sich um ein Umbenennen handelt, und muss die gesamte ineffiziente Abfolge von Löschen und Hinzufügen durchlaufen, die wir gerade beschrieben haben. Angesichts der außergewöhnlichen Funktion des Verfolgens einer Umbenennung benötigt der Subversion-Server ein spezielles Protokoll, um seinen Clients zu sagen: »Bitte verschiebe hello.txt nach subdir/hello.txt.« Darüber hinaus muss jeder Subversion-Client dafür sorgen, dass er diese (relativ seltene) Operation korrekt ausführt. Git andererseits beobachtet das Umbenennen nicht. Sie können hello.txt an eine beliebge Stelle verschieben oder kopieren. Dabei werden jedoch nur die Baumobjekte beeinflusst. (Erinnern Sie sich, dass Baumobjekte die Beziehungen zwischen dem Inhalt speichern, während der Inhalt selbst in Blobs gespeichert ist.) Ein Blick auf die Unterschiede zwischen den beiden Bäumen macht klar, dass der Blob namens 3b18e5... an einen neuen Ort verschoben wurde. Und selbst wenn Sie die Unterschiede nicht explizit untersuchen, weiß jeder Teil des Systems bereits, dass er diesen Blob hat, sodass jeder Teil auch weiß, dass er keine weitere Kopie davon benötigt.
62 | Kapitel 5: Dateiverwaltung und der Index
Hier, wie an vielen anderen Stellen auch, erleichtert das einfache, Hash-basierte Speichersystem von Git Vieles, das andere Revisionskontrollsysteme verwirrt oder dort gar nicht funktioniert.
Probleme beim Verfolgen einer Umbenennung Das Verfolgen des Umbenennens einer Datei sorgt für eine anhaltende Debatte unter den Entwicklern von Versionskontrollsystemen. Ein einfaches Umbenennen reicht schon aus, um Missstimmung zu erzeugen. Der Streit spitzt sich jedoch noch zu, wenn erst der Name der Datei sich ändert und dann ihr Inhalt. Die Szenarien werden in diesem Fall zunehmend philosophisch: Ist diese »neue« Datei wirklich umbenannt oder ist sie der alten Datei einfach nur noch ähnlich? Wie ähnlich muss die neue Datei sein, damit sie als dieselbe Datei betrachtet wird? Wie wird damit umgegangen, wenn Sie einen Patch anwenden, der eine Datei löscht und irgendwo eine ähnliche Datei anlegt? Was passiert, wenn eine Datei in zwei unterschiedlichen Zweigen auf zwei verschiedene Weisen umbenannt wird? Ist es weniger fehleranfällig, wenn man in solchen Situationen Umbenennungen automatisch entdeckt, wie Git es tut, oder soll man vom Benutzer verlangen, Umbenennungen explizit zu kennzeichnen, wie in Subversion? In der Praxis scheint das Git-Verfahren für den Umgang mit Dateiumbenennungen überlegen zu sein, weil es einfach zu viele Möglichkeiten gibt, eine Datei umzubenennen, und Menschen kaum in der Lage sind abzusichern, dass Subversion sie alle kennt. Dennoch gibt es kein perfektes System für den Umgang mit Umbenennungen … bisher.
Die Datei .gitignore Sie haben in diesem Kapitel bereits erfahren, wie Sie die Datei .gitignore verwenden können, um main.o, eine irrelevante Datei, zu übergehen. Wie im Beispiel gezeigt, können Sie jede Datei überspringen, indem Sie ihren Namen im selben Verzeichnis in die Datei .gitignore setzen. Darüber hinaus haben Sie die Möglichkeit, die Datei überall zu ignorieren, indem Sie ihren Namen in die .gitignore-Datei im obersten Verzeichnis Ihres Repository aufnehmen. Git unterstützt aber auch einen viel stärkeren Mechanismus. Eine .gitignore-Datei kann eine Liste mit Dateinamenmustern enthalten, die festlegen, welche Dateien ignoriert werden sollen. Das Format von .gitignore sieht folgendermaßen aus: • Leere Zeilen werden ignoriert, und Zeilen, die mit einem Hash-Zeichen (#) beginnen, können für Kommentare benutzt werden. Das Hash-Zeichnen repräsentiert allerdings keinen Kommentar, wenn es anderem Text auf der Zeile folgt. • Ein einfacher, normaler Dateiname entspricht einer Datei in einem Verzeichnis dieses Namens.
Die Datei .gitignore | 63
• Ein Verzeichnis wird durch einen nachfolgenden Schrägstrich (/) gekennzeichnet. Das entspricht dem angegebenen Verzeichnis und allen Unterverzeichnissen, erfasst aber nicht eine Datei oder einen symbolischen Link. • Ein Muster, das Shell-Globbing-Zeichen, wie z.B. ein Asterisk (*) enthält, wird als Shell-Glob-Muster erweitert. Genau wie beim normalen Shell-Globbing kann der Treffer die Verzeichnisgrenzen nicht überschreiten und deshalb erfasst ein Asterisk immer nur einen einzigen Datei- oder Verzeichnisnamen. Ein Asterisk kann aber trotzdem Teil eines Musters sein, das Schrägstriche enthält, mit denen sich Verzeichnisnamen als Teil des Musters angeben lassen (z.B. debug/32bit/*.o). • Ein vorangestelltes Ausrufezeichen (!) kehrt den Sinn des Musters auf dem Rest der Zeile um. Darin ist jede Datei enthalten, die von einem vorherigen Muster ausgeschlossen wurde, von einer Umkehrungsregel aber erfasst wird. Ein umgekehrtes Muster setzt Regeln mit niedrigerer Priorität außer Kraft. Git erlaubt Ihnen außerdem, in jedem Verzeichnis Ihres Repository eine .gitignore-Datei zu haben. Jede Datei beeinflusst ihr Verzeichnis und alle Unterverzeichnisse. Die .gitignore-Regeln wirken hintereinander: Sie können die Regeln in einem höheren Verzeichnis außer Kraft setzen, indem Sie in eines der Unterverzeichnisse ein umgekehrtes Muster (mit dem vorangestellten !) aufnehmen. Um eine Hierarchie mit mehreren .gitignore-Verzeichnissen aufzulösen und Ergänzungen an der Liste der ignorierten Dateien auf der Kommandozeile zuzulassen, beachtet Git diese Rangfolge (von der höchsten zur niedrigsten): • Muster, die auf der Kommandozeile angegeben werden. • Muster, die aus .gitignore im selben Verzeichnis gelesen werden. • Muster in übergeordneten Verzeichnissen, nach oben verlaufend. Die Muster aus dem aktuellen Verzeichnis überstimmen also die Muster aus dem Elternverzeichnis. Elternverzeichnisse, die sich nahe beim aktuellen Verzeichnis befinden, haben eine höhere Priorität als Elternverzeichnisse, die weiter oben in der Verzeichnishierarchie liegen. • Muster aus der Datei .git/info/exclude. • Muster aus der Datei, die von der Konfigurationsvariablen core.excludefile angegeben wird. Da .gitignore als normale Datei in Ihrem Repository behandelt wird, wird es während der Klonoperationen kopiert und gilt für alle Kopien Ihres Repository. Im Allgemeinen sollten Sie nur dann Einträge in Ihre der Versionskontrolle unterliegenden .gitignore-Dateien setzen, wenn die Muster universell für alle abgeleiteten Repositories gelten. Wenn das Ausschlussmuster speziell für Ihr eigenes Repository gilt und nicht auf einen Klon Ihres Repository angewandt werden sollte (oder darf), dann sollten Sie das Muster stattdessen in die Datei .git/info/exclude setzen, weil diese Datei während der Klonopera-
64 | Kapitel 5: Dateiverwaltung und der Index
tionen nicht weiterverbreitet wird. Das Format und die Behandlung der Muster entsprechen denen von .gitignore-Dateien. Hier ist ein weiteres Szenario. Typischerweise werden .o-Dateien, die vom Compiler aus den Quellen generiert werden, ausgeschlossen. Um .o-Dateien zu ignorieren, setzen Sie *.o in Ihre oberste .gitignore-Datei. Was tun Sie jedoch, wenn Sie eine bestimmte *.oDatei hatten, die von jemand anderem bereitgestellt wurde und für die Sie selbst keinen Ersatz generieren konnten? Wahrscheinlich werden Sie diese spezielle Datei explizit überwachen und haben dann vermutlich eine solche Konfiguration: $ cd mein_paket $ cat .gitignore *.o $ cd mein_paket/herstellerdateien $ cat .gitignore !treiber.o
Die Kombination aus Regeln bedeutet, dass Git alle .o-Dateien innerhalb des Repository ignoriert, aber eine Ausnahme überwacht, nämlich die Datei treiber.o im Unterverzeichnis herstellerdateien.
Eine ausführliche Darstellung des Objektmodells und der Dateien von Git Sie haben inzwischen die grundlegenden Fähigkeiten zum Verwalten von Dateien erworben. Dennoch kann es verwirrend sein, den Überblick darüber zu behalten, welche Datei sich wo befindet – im Arbeitsverzeichnis, im Index oder im Repository. Betrachten Sie die folgenden vier Bilder, die den Fortgang einer Datei namens datei1 beim Bearbeiten, Bereitstellen im Index und abschließenden Bestätigen darstellen. Alle Abbildungen zeigen gleichzeitig Ihr Arbeitsverzeichnis, den Index und den Objektspeicher. Zur Vereinfachung wollen wir im Zweig master bleiben. Der Ausgangszustand ist in Abbildung 5-1 zu sehen. Das Arbeitsverzeichnis enthält hier zwei Dateien namens datei1 und datei2, mit dem Inhalt »foo« bzw. »bar«. Zusätzlich zu datei1 und datei2 im Arbeitsverzeichnis enthält der master-Zweig ein Commit, das einen Baum mit exakt dem gleichen Inhalt »foo« und »bar« für die Dateien datei1 und datei2 aufzeichnet. Darüber hinaus verzeichnet der Index die SHA1-Werte a23bf und 9d3a2 für diese Dateiinhalte. Arbeitsverzeichnis, Index und Objektspeicher sind synchron und im Einklang. Nichts ist unsauber. Abbildung 5-2 zeigt die Änderungen nach dem Bearbeiten von datei1 im Arbeitsverzeichnis. Der Inhalt dieser Datei besteht nun aus »quux«. Im Index oder im Objektspeicher hat sich nichts geändert, aber das Arbeitsverzeichnis wird nun als schmutzig betrachtet.
Eine ausführliche Darstellung des Objektmodells und der Dateien von Git | 65
Working directory project
file1
file2 foo
bar
Index
file1
file2
Object store master
a23bf
9d3a2 foo
bar
Abbildung 5-1: Ausgangssituation: Dateien und Objekte
Es kommt zu einigen interessanten Änderungen, wenn Sie die Bearbeitung von datei1 mit dem Befehl git add datei1 bereitstellen. Wie Abbildung 5-3 zeigt, nimmt Git zuerst die Version von datei1 aus dem Arbeitsverzeichnis, berechnet eine SHA1-Hash-ID (bd71363) für ihren Inhalt und legt diese ID im Objektspeicher ab. Als Nächstes zeichnet Git im Index auf, dass der Pfadname datei1 auf den neuen SHA1-Wert bd71363 aktualisiert wurde. Da sich der Inhalt von datei2 nicht geändert hat und datei2 auch noch nicht durch ein git add bereitgestellt wurde, verweist der Index weiterhin auf das ursprüngliche Blob-Objekt.
66 | Kapitel 5: Dateiverwaltung und der Index
Working directory project 1. Edit file1 file1 foo
file2 quux
bar
Index
file1
file2
Object store master
a23bf
9d3a2 foo
bar
Abbildung 5-2: Nach dem Bearbeiten von datei1
An dieser Stelle haben Sie datei1 im Index bereitgestellt, und das Arbeitsverzeichnis und der Index stimmen überein. Allerdings wird der Index im Hinblick auf HEAD als unsauber angesehen, weil er von dem Baum abweicht, der im Objektspeicher für das HEAD-Commit des master-Zweigs aufgezeichnet wurde.3 3
Sie können einen unsauberen Index auch in der anderen Richtung bekommen, ungeachtet des Zustands des Arbeitsverzeichnisses. Indem Sie ein Nicht-HEAD-Commit aus dem Objektspeicher in den Index auslesen und die entsprechenden Dateien nicht in das Arbeitsverzeichnis auschecken, erzeugen Sie eine Situation, in der Index und Arbeitsverzeichnis nicht in Übereinstimmung sind und der Index mit Blick auf den HEAD unsauber ist.
Eine ausführliche Darstellung des Objektmodells und der Dateien von Git | 67
Working directory project
file1
file2 quux
bar
Index
2a. Add file1 to Object store
file1
file2
Object store master
2b. Update index
a23bf
9d3a2 foo
bar
bd71363 quux
Abbildung 5-3: Nach git add
Nachdem schließlich alle Änderungen im Index bereitgestellt wurden, wendet ein Commit sie auf das Repository an. Die Auswirkungen von git commit werden in Abbildung 5-4 gezeigt. Wie Abbildung 5-4 zeigt, löst das commit drei Schritte aus. Erstens wird das virtuelle Baumobjekt, das sich im Index befindet, in ein echtes Baumobjekt umgewandelt und unter seinem SHA1-Namen im Objektspeicher abgelegt. Zweitens wird ein neues Commit-Objekt mit Ihrer Lognachricht erzeugt. Das neue Commit verweist auf das neu erzeugte Baumobjekt und außerdem auf das vorhergehende oder Eltern-Commit. Drittens wird die Referenz des master-Zweigs vom letzten Commit auf das neu angelegte Commit-Objekt verschoben, das damit zum neuen master-HEAD wird.
68 | Kapitel 5: Dateiverwaltung und der Index
Working directory project
file1
file2 quux
bar
Index
3a. Convert index into tree object file1
Object store
file2
3c. Update branch ref
master
3b. Make commit object
a23bf
9d3a2 foo
bar
bd71363 quux
Abbildung 5-4: Nach dem git commit
Interessanterweise sind nun das Arbeitsverzeichnis, der Index und der Objektspeicher (repräsentiert durch den HEAD des master-HEAD) wieder synchron und in Übereinstimmung – genau wie in Abbildung 5-1.
Eine ausführliche Darstellung des Objektmodells und der Dateien von Git | 69
Kapitel 6
KAPITEL 6
Commits
In Git dient ein Commit dazu, die Änderungen an einem Repository aufzuzeichnen. Auf den ersten Blick scheint ein Git-Commit sich nicht von einem Commit oder Einchecken in anderen Versionskontrollsystemen zu unterscheiden. Hinter den Kulissen jedoch operiert ein Git-Commit auf einzigartige Weise. Wenn ein Commit auftritt, zeichnet Git einen Schnappschuss des Index auf und legt diesen Schnappschuss im Objektspeicher ab. (Das Vorbereiten des Index für ein Commit wird in Kapitel 5 behandelt.) Dieser Schnappschuss enthält keine Kopie jeder Datei und jedes Verzeichnisses im Index, da eine solche Strategie enorme und unerschwingliche Mengen an Speicher erfordern würde. Stattdessen vergleicht Git den aktuellen Zustand des Index mit dem vorangegangenen Schnappschuss und leitet auf diese Weise eine Liste der betroffenen Dateien und Verzeichnisse ab. Git erzeugt neue Blobs für alle Dateien, die sich verändert haben, sowie neue Bäume für alle Verzeichnisse, die sich verändert haben. Alle Blob- oder Baumobjekte, die sich nicht geändert haben, werden wiederverwendet. Commit-Schnappschüsse werden aneinandergekettet, wobei jeder neue Schnappschuss auf seinen Vorgänger zeigt. Mit der Zeit werden Folgen aus Änderungen als eine Reihe aus Commits dargestellt. Es wirkt vielleicht aufwendig, wenn man den gesamten Index mit einem vorherigen Zustand vergleicht, allerdings verläuft der ganze Prozess bemerkenswert schnell, weil jedes Git-Objekt einen SHA1-Hash-Wert besitzt. Wenn zwei Objekte, und das betrifft sogar zwei Teilbäume, den gleichen SHA1-Hash haben, sind die Objekte identisch. Git kann eine Menge rekursiver Vergleiche vermeiden, indem es Teilbäume ausdünnt, die den gleichen Inhalt haben. Es gibt eine eineindeutige Beziehung zwischen einer Menge von Änderungen im Repository und einem Commit: Ein Commit ist die einzige Methode, mit der sich Änderungen in ein Repository einführen lassen, und jede Änderung im Repository muss durch ein Commit eingeführt werden. Diese Tatsache bringt Verantwortung mit sich. Unter keinen
| 71
Umständen dürfen sich Repository-Daten ändern, ohne dass die Änderung aufgezeichnet wird! Stellen Sie sich nur einmal das Chaos vor, das Sie erwartet, wenn sich der Inhalt des Master-Repository geändert hat und es keinen Vermerk darüber gibt, wie das geschehen ist oder wer das aus welchem Grund veranlasst hat. Meist werden Commits explizit von einem Entwickler eingeführt, Git selbst kann aber ebenfalls Commits auslösen. Wie Sie in Kapitel 9 sehen werden, verursacht eine MergeOperation ein Commit im Repository, und zwar zusätzlich zu den Commits, die von den Benutzern vor dem Merge vorgenommen wurden. Wie Sie über ein Commit entscheiden, hängt ganz von Ihnen und Ihren Vorlieben oder Ihrem Entwicklungsstil ab. Im Allgemeinen sollten Sie ein Commit zu wohldefinierten Zeitpunkten durchführen, wenn sich Ihre Entwicklung im Stillstand befindet, etwa wenn ein Testdurchlauf stattfindet, wenn Feierabend ist oder zu ähnlichen Anlässen. Zögern Sie jedoch nicht, Commits durchzuführen! Git eignet sich sehr gut für häufige Commits und bietet eine große Anzahl von Befehlen, mit denen Sie sie manipulieren können. Sie werden später feststellen, dass mehrere Commits – mit jeweils kleinen, wohldefinierten Änderungen – eine bessere Organisation der Änderungen und eine leichtere Manipulation der Patches nach sich ziehen.
Atomare Änderungsmengen (Changesets) Jedes Git-Commit repräsentiert eine einzige, atomare Änderungsmenge in Bezug auf den vorherigen Zustand. Ungeachtet der Anzahl der Verzeichnisse, Dateien, Zeilen oder Bytes, die sich mit einem Commit ändern,1 werden entweder alle oder keine Änderungen angewandt. Im Hinblick auf das zugrunde liegende Objektmodell ist die Atomarität durchaus sinnvoll: Ein Commit-Schnappschuss steht für die gesamte Menge an modifizierten Dateien und Verzeichnissen. Er muss den einen Baumzustand oder den anderen repräsentieren, und eine Änderungsmenge zwischen zwei Zustandsschnappschüssen stellt eine vollständige Baum-zu-Baum-Transformation dar. (Mehr über abgeleitete Unterschiede zwischen Commits erfahren Sie in Kapitel 8.) Betrachten Sie einmal das Vorgehen beim Verschieben einer Funktion von einer Datei in eine andere. Wenn Sie die Löschung mit einem Commit vornehmen und dann mit einem zweiten Commit folgen, um die Funktion wieder hinzuzufügen, bleibt eine kleine »semantische Lücke« im Verlauf Ihres Repository, während der die Funktion einfach nicht vorhanden ist. Zwei Commits in der anderen Reihenfolge sind ebenfalls problematisch. In beiden Fällen ist Ihr Code vor dem ersten und nach dem zweiten Commit semantisch konsistent, nach dem ersten Commit dagegen ist er fehlerhaft. 1
Git zeichnet außerdem ein Modus-Flag auf, das die Ausführbarkeit der einzelnen Dateien anzeigt. Änderungen an diesem Flag sind ebenfalls Teil einer Änderungsmenge.
72 | Kapitel 6: Commits
Mit einem atomaren Commit jedoch, das die Funktion gleichzeitig löscht und hinzufügt, bleibt Ihnen diese semantische Lücke im Verlauf erspart. In Kapitel 10 erfahren Sie, wie Sie Ihre Commits am besten konstruieren und organisieren. Git ist es egal, wieso sich die Dateien ändern. Das heißt, der Inhalt der Änderungen spielt keine Rolle. Als Entwickler könnten Sie eine Funktion von hier nach dort verschieben und erwarten, dass das als ein einheitlicher Schritt behandelt wird. Sie könnten aber auch alternativ das Löschen mit einem Commit bestätigen und dann später ein Commit für das Hinzufügen ausführen. Git kümmert das nicht. Es hat nichts mit der Semantik dessen zu tun, was sich in den Dateien befindet. Damit kommen wir aber zu einem der wesentlichen Gründe dafür, dass Git Atomarität implementiert: Sie erlaubt es Ihnen, Ihre Commits passender zu strukturieren, indem Sie einige bewährte Methoden befolgen. Letztlich können Sie damit rechnen, dass Git Ihr Repository nicht in irgendeinem Übergangszustand zwischen zwei Commit-Schnappschüssen zurücklässt.
Commits festlegen Unabhängig davon, ob Sie einzeln oder im Team an Ihrem Code arbeiten, ist das Festlegen der jeweiligen Commits eine wichtige Aufgabe. Um z.B. einen Zweig zu erzeugen, müssen Sie ein Commit wählen, von dem Sie abzweigen; um Codevariationen zu vergleichen, müssen Sie zwei Commits angeben, und um den Commit-Verlauf zu bearbeiten, müssen Sie eine Sammlung von Commits vorgeben. In Git können Sie auf jeden Commit über eine explizite oder implizite Referenz verweisen. Sie haben bereits explizite und auch einige implizite Referenzen gesehen. Die eindeutige, 40-stellige Hex-SHA1-Commit-ID ist eine explizite Referenz, während HEAD, das immer auf das neueste Commit verweist, eine implizite Referenz darstellt. Manchmal allerdings ist keine der beiden Referenzen geeignet. Zum Glück bietet Git viele unterschiedliche Mechanismen zum Benennen eines Commit, die jeweils ihre Vorteile haben und manchmal – je nach Kontext – sinnvoller sind als andere. Wenn man z.B. ein bestimmtes Commit mit einem Kollegen bespricht, der an denselben Daten, aber in einer verteilten Umgebung arbeitet, ist es am besten, wenn man einen Commit-Namen benutzt, der garantiert in beiden Repositories gleich ist. Falls Sie andererseits innerhalb Ihres eigenen Repository arbeiten und sich auf einen Zustand beziehen müssen, der vor einigen Commits in einem Zweig vorhanden war, funktioniert ein einfacher relativer Name hervorragend.
Absolute Commit-Namen Der strengste Name für ein Commit ist sein Hash-Identifikator. Bei der Hash-ID handelt es sich um einen absoluten Namen, was bedeutet, dass sie sich nur auf exakt ein Commit
Commits festlegen | 73
beziehen kann. Es spielt keine Rolle, wo sich das Commit im Verlauf des gesamten Repository befindet – die Hash-ID kennzeichnet immer dasselbe Commit. Jede Commit-ID ist global eindeutig – nicht nur für ein Repository, sondern für alle Repositories. Falls z.B. ein Entwickler Ihnen schreibt und sich auf eine bestimmte Commit-ID in seinem Repository bezieht und Sie das gleiche Commit in Ihrem Repository finden, dann können Sie sicher davon ausgehen, dass Sie dasselbe Commit mit demselben Inhalt haben. Da außerdem die Daten, die zu einer Commit-ID beitragen, den Zustand des kompletten Repository-Baums sowie den vorherigen Commit-Status enthalten, kann durch eine induktive Annahme noch eine viel stärkere Aussage getroffen werden: Sie können sich sicher sein, dass Sie beide über die gleiche Entwicklungslinie reden, die bis zu einschließlich diesem Commit geführt hat. Da eine 40-stellige SHA1-Zahl in Hexadezimalschreibweise einen lästigen und fehleranfälligen Eintrag ergibt, erlaubt Ihnen Git, diese Zahl innerhalb der Objektdatenbank eines Repository zu einem eindeutigen Präfix abzukürzen. Hier ist ein Beispiel aus dem Git-eigenen Repository: $ git log -1 --pretty=oneline HEAD 1fbb58b4153e90eda08c2b022ee32d90729582e6 Merge git://repo.or.cz/git-gui $ git log -1 --pretty=oneline 1fbb error: short SHA1 1fbb is ambiguous. fatal: ambiguous argument '1fbb': unknown revision or path not in the working tree. Use '--' to separate paths from revisions $ git log -1 --pretty=oneline 1fbb58 1fbb58b4153e90eda08c2b022ee32d90729582e6 Merge git://repo.or.cz/git-gui
Ein Tag-Name ist zwar nicht global eindeutig, stellt aber insofern einen absoluten Namen dar, als er auf ein eindeutiges Commit verweist und sich mit der Zeit nicht ändert (außer natürlich, Sie ändern ihn explizit).
Refs und Symrefs Eine Referenz oder Ref ist eine SHA1-Hash-ID, die auf ein Objekt innerhalb des GitObjektspeichers verweist. Sie kann sich zwar auf jedes beliebige Git-Objekt beziehen, meist aber verweist sie auf ein Commit-Objekt. Eine symbolische Referenz oder Symref ist ein Name, der indirekt auf ein Git-Objekt verweist. Es ist trotzdem nur ein Ref. Lokale Topic-Zweignamen, entfernte Tracking-Zweignamen und Tag-Namen sind Refs. Jede symbolische Referenz besitzt einen expliziten, vollständigen Namen, der mit refs/ beginnt. Diese Referenzen sind hierarchisch im Verzeichnis .git/refs/ des Repository gespeichert. Es werden im Prinzip drei unterschiedliche Namensräume in refs/ abgebildet: refs/heads/ref für Ihre lokalen Zweige, refs/remotes/ref für Ihre entfernten Tracking-Zweige und refs/tags/ref für Ihre Tags. (Zweige werden in Kapitel 7 sowie in Kapitel 11 ausführlich behandelt.)
74 | Kapitel 6: Commits
So ist z.B. der Name dev eines lokalen Topic-Zweigs in Wirklichkeit eine Kurzform von refs/heads/dev. Entfernte Tracking-Zweige befinden sich im Namensraum refs/remotes/, origin/master bezeichnet deshalb eigentlich refs/remotes/origin/master. Und ein Tag wie v2.6.23 ist die Kurzform von refs/tags/v2.6.23. Sie können entweder einen vollständigen Ref-Namen oder seine Abkürzung verwenden. Wenn Sie jedoch einen Zweig und ein Tag desselben Namens haben, wendet Git eine Heuristik zur Unterscheidung an und benutzt den ersten Treffer entsprechend dieser Liste von der git rev-parse-Manpage: .git/ref .git/refs/ref .git/refs/tags/ref .git/refs/heads/ref .git/refs/remotes/ref .git/refs/remotes/ref/HEAD
Die erste Regel gilt normalerweise nur für HEAD, ORIG_HEAD, FETCH_HEAD und MERGE_HEAD. Technisch gesehen kann der Name des Git-Verzeichnisses, .git, geändert werden. Deshalb benutzt die interne Git-Dokumentation die Variable $GIT_DIR anstelle des Literals .git.
Git pflegt für bestimmte Aufgaben automatisch mehrere spezielle Symrefs. Diese können überall eingesetzt werden, wo ein Commit ausgeführt wird: HEAD HEAD verweist immer auf das neueste Commit im aktuellen Zweig. Wenn Sie Zweige wechseln, wird HEAD angepasst, sodass es wieder auf das neueste Commit des neuen
Zweigs zeigt. ORIG_HEAD
Bestimmte Operationen, etwa Merge (Zusammenführen) und Reset (Zurücksetzen), zeichnen die vorhergehende Version von HEAD in ORIG_HEAD auf, bevor sie es auf einen neuen Wert setzen. Sie können ORIG_HEAD benutzen, um den vorhergegangenen Zustand wiederherzustellen oder zu ihm zurückzukehren oder um einen Vergleich anzustellen. FETCH_HEAD
Wenn entfernte Repositories verwendet werden, zeichnet git fetch die Köpfe aller Zweige, die abgerufen wurden, in der Datei .git/FETCH_HEAD auf. FETCH_HEAD ist eine Abkürzung für den Kopf des letzten abgerufenen Zweigs und gilt nur unmittelbar nach einer Fetch- (also Abruf-)Operation. Mithilfe dieses Symref können Sie den HEAD der Commits von git fetch auch dann finden, wenn ein anonymer Abruf verwendet wird, der nicht speziell einen Zweignamen angibt. Die fetch-Operation wird in Kapitel 11 behandelt.
Commits festlegen | 75
MERGE_HEAD
Wenn ein Merge im Gang ist, wird die Spitze des anderen Zweigs zeitweilig im Symref MERGE_HEAD aufgezeichnet. Mit anderen Worten: MERGE_HEAD ist das Commit, das zu HEAD zusammengeführt wird. Diese ganzen symbolischen Referenzen werden durch den Plumbing-Befehl git symbolic-ref verwaltet. Es ist zwar möglich, mit einem dieser symbolischen Namen (z.B. HEAD) einen eigenen Zweig zu erzeugen, aber das ist keine gute Idee.
Es gibt eine ganze Menge Sonderzeichenvarianten für Ref-Namen. Die gebräuchlichsten, das Dach (^) und die Tilde (~), werden im nächsten Abschnitt beschrieben. In einem anderen Trick mit Refs können Doppelpunkte benutzt werden, um auf alternative Versionen einer bekannten Datei zu verweisen, die in einen Merge-Konflikt verwickelt ist. Dieses Vorgehen wird in Kapitel 9 beschrieben.
Relative Commit-Namen Git bietet auch Mechanismen, um ein Commit relativ zu einer anderen Referenz zu kennzeichnen. Bei der anderen Referenz handelt es sich meist um die Spitze eines Zweiges. Einige dieser Namen haben Sie bereits gesehen, z.B. master und master^, wobei sich master^ immer auf das vorletzte Commit auf dem master-Zweig bezieht. Es gibt auch noch weitere: Sie können master^^, master~2 und sogar einen komplexen Namen wie master~10^2~2^2 verwenden. Mit Ausnahme des ersten Root-Commit2 wird jedes Commit von wenigstens einem früheren Commit, vielleicht sogar vielen Commits abgeleitet. Direkte Vorfahren werden dabei als Eltern-Commits bezeichnet. Damit ein Commit mehrere Eltern-Commits hat, muss es das Ergebnis einer Merge-Operation sein. Als Ergebnis gibt es ein Eltern-Commit für jeden Zweig, der zu einem Merge-Commit beigetragen hat. Innerhalb einer Generation dient das Dach dazu, ein anderes Eltern-Commit auszuwählen. Gehen wir von einem Commit C aus, dann ist C^1 das erste Eltern-Commit, C^2 das zweite Eltern-Commit, C^3 das dritte usw., wie Abbildung 6-1 zeigt. Die Tilde dient dazu, vor ein Eltern-Commit zu springen und eine der vorangegangenen Generationen auszuwählen. Wenn wir hier wieder vom Commit C ausgehen, dann bezeichnet C~1 das erste Eltern-Commit, C~2 das erste Großeltern-Commit und C~3 das erste Urgroßeltern-Commit. Wenn es mehrere Eltern in einer Generation gibt, dann wird 2
Ja, man kann tatsächlich mehrere Root-Commits in ein Repository einführen. Das kommt z.B. vor, wenn zwei unterschiedliche Projekte und ihre kompletten Repositories zusammengebracht und zu einem zusammengeführt werden.
76 | Kapitel 6: Commits
C^1
C^2
C
C^3 Abbildung 6-1: Mehrere Elternnamen
dem ersten Eltern-Commit des ersten Eltern-Commit gefolgt. Sie werden vielleicht auch bemerken, dass sowohl C^1 als auch C~1 sich auf das erste Eltern-Commit beziehen; beide Namen sind korrekt (Abbildung 6-2).
C~3
C~2
C~1
C^2
C
C^3 Abbildung 6-2: Mehrere Generationsnamen
Git unterstützt auch noch andere Abkürzungen und Kombinationen. Die abgekürzten Formen C^ und C~ sind identisch mit C^1 bzw. C~1. C^^ ist das Gleiche wie C^1^1, und da es »das erste Eltern-Commit des ersten Eltern-Commit von Commit C« bedeutet, verweist es auf das gleiche Commit wie C~2. Indem man eine Ref und Instanzen von Dach und Tilde kombiniert, können beliebige Commits aus dem Vorgänger-Commit-Graphen von Ref ausgewählt werden. Denken Sie jedoch daran, dass diese Namen relativ zum aktuellen Wert von Ref sind. Wird auf der Spitze von Ref ein neues Commit ausgeführt, wird der Commit-Graph durch eine neue Generation ergänzt und jeder »Eltern«-Name rückt im Verlauf und im Graphen weiter nach hinten.
Commits festlegen | 77
Hier ist ein Beispiel aus Gits eigener Geschichte, als der master-Zweig von Git bei Commit 1fbb58b4153e90eda08c2b022ee32d90729582e6 war. Mithilfe des Befehls git showbranch --more=35 und einer Beschränkung der Ausgabe auf die letzten zehn Zeilen können Sie den Verlauf des Graphen untersuchen und entdecken eine komplexe ZweigMerge-Struktur: $ git rev-parse master 1fbb58b4153e90eda08c2b022ee32d90729582e6 $ git show-branch --more=35 | tail -10 -- [master~15] Merge branch 'maint' -- [master~3^2^] Merge branch 'maint-1.5.4' into maint +* [master~3^2^2^] wt-status.h: declare global variables as extern -- [master~3^2~2] Merge branch 'maint-1.5.4' into maint -- [master~16] Merge branch 'lt/core-optim' +* [master~16^2] Optimize symlink/directory detection +* [master~17] rev-parse --verify: do not output anything on error +* [master~18] rev-parse: fix using "--default" with "--verify" +* [master~19] rev-parse: add test script for "--verify" +* [master~20] Add svn-compatible "blame" output format to git-svn $ git rev-parse master~3^2^2^ 32efcd91c6505ae28f87c0e9a3e2b3c0115017d8
Zwischen master~15 und master~16 fand ein Merge statt, der eine Reihe weiterer Merges sowie ein einfaches Commit namens master~3^2^2^ einführte. Dabei handelt es sich um Commit 32efcd91c6505ae28f87c0e9a3e2b3c0115017d8. Der Befehl git rev-parse ist die letzte Autorität beim Übersetzen jeder Form von Commit-Namen – tag, relativ, abgekürzt oder absolut – in eine tatsächliche, absolute Commit-Hash-ID innerhalb der Objektdatenbank.
Der Commit-Verlauf Alte Commits anschauen Der wichtigste Befehl zum Anzeigen des Verlaufs von Commits ist git log. Er besitzt mehr Optionen, Parameter, Selektoren, Formatierer und anderen Schnickenpfiffi als das sagenhafte ls. Aber keine Bange: Genau wie bei ls müssen Sie nicht sofort alle Einzelheiten auswendig lernen. In seiner parameterlosen Form funktioniert git log genau wie git log HEAD, d.h. es gibt die Lognachricht zu jedem einzelnen Commit in der History aus, der von HEAD aus erreichbar ist. Änderungen werden beginnend mit dem HEAD-Commit angezeigt, es arbeitet sich dann rückwärts durch den Graphen. Mit hoher Wahrscheinlichkeit werden sie dann in umgekehrter chronologischer Reihenfolge vorliegen; denken Sie aber daran, dass Git sich an den Commit-Graphen und nicht an die Zeit hält, wenn es im Verlauf zurückreist.
78 | Kapitel 6: Commits
Wenn Sie ein Commit à la git log commit angeben, beginnt das Log bei dem genannten Commit und arbeitet sich zurück. Diese Form des Befehls eignet sich zum Betrachten der Geschichte eines Zweiges: $ git log master commit 1fbb58b4153e90eda08c2b022ee32d90729582e6 Merge: 58949bb... 76bb40c... Author: Junio C Hamano Date: Thu May 15 01:31:15 2008 -0700 Merge git://repo.or.cz/git-gui * git://repo.or.cz/git-gui: git-gui: Delete branches with 'git branch -D' to clear config git-gui: Setup branch.remote,merge for shorthand git-pull git-gui: Update German translation git-gui: Don't use '$$cr master' with aspell earlier than 0.60 git-gui: Report less precise object estimates for database compression commit 58949bb18a1610d109e64e997c41696e0dfe97c3 Author: Chris Frey Date: Wed May 14 19:22:18 2008 -0400 Documentation/git-prune.txt: document unpacked logic Clarifies the git-prune manpage, documenting that it only prunes unpacked objects. Signed-off-by: Chris Frey Signed-off-by: Junio C Hamano commit c7ea453618e41e05a06f05e3ab63d555d0ddd7d9 ...
Die Logs sind zuverlässig, allerdings ist es mit Sicherheit weder praktisch noch besonders aussagekräftig, wenn man den gesamten Verlauf eines Repository noch einmal aufrollt. Meist ist ein eingeschränkter Verlauf deutlich informativer. Man kann z.B. einen Commit-Bereich zum Einschränken des Verlaufs angeben, und zwar mit der Form von..bis. Damit zeigt git log alle Commits von von bis einschließlich bis. Hier ist ein Beispiel: $ git log --pretty=short --abbrev-commit master~12..master~10 commit 6d9878c... Author: Jeff King clone: bsd shell portability fix commit 30684df... Author: Jeff King t5000: tar portability fix
Der Commit-Verlauf
| 79
Hier zeigt git log die Commits zwischen master~12 und master~10 oder die 10. und 11. vorhergehenden Commits auf dem Master-Zweig. Mehr über Bereiche erfahren Sie in »Commit-Bereiche« auf Seite 85. Das vorangegangene Beispiel führt darüber hinaus zwei Formatierungsoptionen ein, --pretty=short und --abbrev-commit. Das erste Flag ist für die Menge von Informationen über jedes Commit verantwortlich und tritt in mehreren Variationen auf, einschließlich oneline, short und full. Das zweite Flag fordert ganz einfach, dass die Hash-IDs abgekürzt werden. -p gibt den Patch oder die Änderungen aus, die durch das Commit ausgelöst werden: $ git log -1 -p 4fe86488 commit 4fe86488e1a550aa058c081c7e67644dd0f7c98e Author: Jon Loeliger <[email protected]> Date: Wed Apr 23 16:14:30 2008 -0500 Add otherwise missing --strict option to unpack-objects summary. Signed-off-by: Jon Loeliger <[email protected]> Signed-off-by: Junio C Hamano diff --git a/Documentation/git-unpack-objects.txt b/Documentation/git-unpack-objects.txt index 3697896..50947c5 100644 --- a/Documentation/git-unpack-objects.txt +++ b/Documentation/git-unpack-objects.txt @@ -8,7 +8,7 @@ git-unpack-objects - Unpack objects from a packed archive SYNOPSIS --------'git-unpack-objects' [-n] [-q] [-r] <pack-file +'git-unpack-objects' [-n] [-q] [-r] [--strict] <pack-file
Beachten Sie außerdem die Option -1: Sie beschränkt die Ausgabe auf ein einziges Commit. Man kann auch -n eingeben, um die Ausgabe auf höchstens n Commits zu beschränken. Die Option --stat benennt die Dateien, die bei einem Commit geändert wurden, und zählt, wie viele Zeilen in jeder einzelnen Datei modifiziert wurden: $ git log --pretty=short --stat master~12..master~10 commit 6d9878cc60ba97fc99aa92f40535644938cad907 Author: Jeff King clone: bsd shell portability fix git-clone.sh | 3 +-1 files changed, 1 insertions(+), 2 deletions(-)
80 | Kapitel 6: Commits
commit 30684dfaf8cf96e5afc01668acc01acc0ade59db Author: Jeff King t5000: tar portability fix t/t5000-tar-tree.sh | 8 ++++---1 files changed, 4 insertions(+), 4 deletions(-)
Vergleichen Sie die Ausgabe von git log --stat mit der Ausgabe von git diff --stat. Es gibt einen fundamentalen Unterschied zwischen den beiden Ausgaben: Die erste Variante erzeugt eine Zusammenfassung für jedes einzelne Commit, das in dem Bereich angegeben ist, während die zweite Ausgabe eine Zusammenfassung des Gesamtunterschieds zwischen zwei Repository-Zuständen ausgibt, die auf der Kommandozeile genannt wurden.
Ein weiterer Befehl zum Anzeigen von Objekten aus dem Objektspeicher ist git show. Mit seiner Hilfe können Sie ein Commit sehen: $ git show HEAD~2
Oder auch ein bestimmtes Blob-Objekt: $ git show origin/master:Makefile
Der im letzten git show gezeigte Blob ist das Makefile aus dem Zweig namens origin/master.
Commit-Graphen »Darstellungen des Objektspeichers« auf Seite 38 hat einige Bilder vorgestellt, die Ihnen helfen sollen, sich die Anordnung und die Zusammenhänge zwischen den Objekten in Gits Datenmodell vorzustellen. Solche Skizzen sind sehr erhellend, vor allem wenn Git Ihnen bisher völlig fremd war. Allerdings wird selbst ein kleines Repository mit nur einer Handvoll Commits, Merges und Patches schnell sehr sperrig und unhandlich, wenn man es in solchem Detail abbilden will. So zeigt Abbildung 6-3 einen vollständigeren, aber dennoch vereinfachten Commit-Graphen. Stellen Sie sich vor, wie er aussehen würde, wenn alle Commits und Datenstrukturen vorhanden wären. Eine Beobachtung über Commits kann den Plan ganz außerordentlich vereinfachen: Jedes Commit führt ein Baumobjekt ein, das das gesamte Repository repräsentiert. Deshalb kann ein Commit einfach als schlichter Name dargestellt werden. Abbildung 6-4 zeigt den gleichen Commit-Graphen wie Abbildung 6-3, ohne die Baumund Blob-Objekte wiederzugeben. Zum Zweck der Erörterung oder Veranschaulichung werden Zweignamen normalerweise ebenfalls in Commit-Graphen gezeigt. In der Informatik ist ein Graph eine Sammlung von Knoten und eine Menge von Kanten zwischen den Knoten. Es gibt mehrere Typen von Graphen mit unterschiedlichen Eigen-
Der Commit-Verlauf
| 81
pr-17
master
dev
Abbildung 6-3: Ein vollständiger Commit-Graph pr-17
master
dev
Abbildung 6-4: Vereinfachter Commit-Graph
82 | Kapitel 6: Commits
schaften. Git benutzt einen besonderen Graphen namens gerichteter azyklischer Graph. Ein solcher Graph besitzt zwei wichtige Eigenschaften. Erstens sind die Kanten innerhalb des Graphen alle zwischen zwei Knoten gerichtet. Zweitens gibt es, wenn man bei einem beliebigen Knoten im Graphen startet, keinen Pfad entlang der gerichteten Kanten, der zurück zum Startknoten führt. Git implementiert den Verlauf der Commits innerhalb eines Repository als gerichteten azyklischen Graphen. Im Commit-Graphen stellt jeder Knoten ein einzelnes Commit dar, alle Kanten sind von einem Nachfahrenknoten zu einem Elternknoten gerichtet und formen auf diese Weise eine Vorfahrenbeziehung. Die Graphen, die Sie in Abbildung 6-3 und in Abbildung 6-4 gesehen haben, sind solche gerichteten azyklischen Graphen. Wenn man vom Verlauf der Commits spricht und die Beziehung zwischen den Commits in einem Graphen diskutiert, werden die einzelnen Commit-Knoten oft beschriftet, wie in Abbildung 6-5 zu sehen ist. pr-17
E
F
G master
A
B
C
D
H
Abbildung 6-5: Beschrifteter Commit-Graph
In diesen Diagrammen verläuft die Zeit etwa von links nach rechts. A ist das erste Commit, da es kein Eltern-Commit besitzt, B ist nach A aufgetreten. Sowohl E als auch C kamen nach B, man kann aber keine Aussage über den relativen Zeitverlauf zwischen C und E treffen; beide hätten jeweils vor dem anderen auftreten können. Um genau zu sein, kümmert sich Git eigentlich nicht um die Zeit oder den Zeitablauf (absolut oder relativ) der Commits. Die tatsächliche Zeit in der realen Welt, zu der ein Commit stattgefunden hat, kann irreführend sein, da die Uhr des Computers möglicherweise falsch oder inkonsistent eingestellt ist. In einer verteilten Entwicklungsumgebung wird das Problem noch verschärft. Zeitstempeln kann man nicht vertrauen. Sicher ist jedoch, dass ungeachtet der Zeitstempel, die auf den Commits kleben, Commit X den Repository-Zustand vor dem Repository-Zustand von Commit Y erfasst, wenn Commit Y auf Eltern-Commit X zeigt. Die Commits E und C teilen sich ein Eltern-Commit, nämlich B. Das heißt, dass B der Ursprung eines Zweiges ist. Der Master-Zweig beginnt mit den Commits A, B, C und D.
Der Commit-Verlauf
| 83
Indes formt die Folge aus den Commits A, B, E, F und G den Zweig namens pr-17. Der Zweig pr-17 zeigt auf Commit G. (Mehr dazu erfahren Sie in Kapitel 7.) Da es ein Merge, d.h. eine Zusammenführung ist, besitzt H mehr als ein Eltern-Commit – in diesem Fall D und G. Obwohl H zwei Eltern hat, ist es nur auf dem Master-Zweig vorhanden, da pr-17 auf G verweist. (Die Merge-Operation wird in Kapitel 9 genauer besprochen.) In der Praxis werden die Feinheiten sich vermischender Commits als unwichtig erachtet. Auch die Implementierungsdetails eines Commit, das zurück auf sein Eltern-Commit verweist, werden häufig ignoriert, wie Abbildung 6-6 zeigt. Die Zeit verläuft immer noch ungefähr von links nach rechts, es sind zwei Zweige zu sehen und ein Merge-Commit (H) ist gekennzeichnet, aber die eigentlichen gerichteten Kanten sind vereinfacht, weil sie implizit verstanden werden. pr-17
master
H Abbildung 6-6: Commit-Graph ohne Pfeile
Diese Art von Commit-Graph wird oft benutzt, um über den Betrieb bestimmter GitBefehle zu reden und darüber, wie diese jeweils den Verlauf der Commits ändern. Graphen bilden eine relativ abstrakte Repräsentation des tatsächlichen Commit-Verlaufs und stehen damit im Gegensatz zu Werkzeugen (wie etwa gitk und git show-branch), die konkrete Repräsentationen der Commit-Verlaufsgraphen liefern. In diesen Werkzeugen wird die Zeit jedoch meist von unten nach oben, vom ältesten zum jüngsten Zeitpunkt gezeigt. Im Grunde handelt es sich um die gleichen Informationen.
Mit gitk den Commit-Graphen betrachten Ein Graph ist von Natur aus eine visuelle Hilfe, mit der Sie sich komplizierte Strukturen und Beziehungen verdeutlichen können. Der Befehl gitk3 kann ein Bild des gerichteten azyklischen Graphen eines Repository zeichnen, wann immer Sie es wollen. 3
Ja, das ist einer der wenigen Git-Befehle, die nicht als »Unterbefehle« betrachtet werden, daher wird er als gitk und nicht als git gitk angegeben.
84 | Kapitel 6: Commits
Schauen wir unsere Beispiel-Website an: $ cd public_html $ gitk
Das Programm gitk ist sehr vielseitig, wir wollen uns jedoch auf den Graphen beziehen. Die Graph-Ausgabe sieht ungefähr so aus wie in Abbildung 6-7.
Abbildung 6-7: Ein Merge mit gitk betrachtet
Folgendes müssen Sie wissen, um die gerichteten azyklischen Graphen der Commits zu verstehen. Zunächst einmal: Jedes Commit kann null oder mehr Eltern haben: • Normale Commits haben exakt ein Eltern-Commit, bei dem es sich um das vorhergehende Commit im Verlauf handelt. Wenn Sie eine Änderung vornehmen, ist Ihre Änderung der Unterschied zwischen Ihrem neuen Commit und dessen Eltern-Commit. • Es gibt normalerweise ein Commit ohne Eltern-Commits: das erste Commit, das ganz unten im Graphen auftaucht. • Ein Merge-Commit, wie etwa dasjenige ganz oben im Graphen, hat mehr als ein Eltern-Commit. Ein Commit mit mehr als einem Kind bildet die Stelle, an der der Verlauf sich geteilt und einen Zweig geformt hat. In Abbildung 6-7 bildet das Commit Mein Gedicht entfernen den Verzweigungspunkt. Es gibt keine permanente Aufzeichnung der Verzweigungsstartpunkte, allerdings kann Git sie mithilfe des Befehls git merge-base algorithmisch ermitteln.
Commit-Bereiche Viele Git-Befehle erlauben Ihnen, einen Commit-Bereich festzulegen. In seiner einfachsten Ausführung ist ein Commit-Bereich eine Kurzform für eine Reihe von Commits. Komplexere Formen erlauben Ihnen, Commits »einzuschließen« und »auszuschließen«. Ein Bereich wird durch doppelte Punkte angedeutet (..), wie in start..end, wobei start und end mithilfe der Formen angegeben werden können, die im Abschnitt »Commits
Der Commit-Verlauf
| 85
festlegen« auf Seite 73 beschrieben sind. Typischerweise dient ein Bereich dazu, einen Zweig oder einen Teil eines Zweiges zu untersuchen. Weiter vorn im Abschnitt »Alte Commits anschauen« auf Seite 78 haben Sie erfahren, wie Sie einen Commit-Bereich mit git log benutzen. In dem Beispiel wurde der Bereich master~12..master~10 dazu verwendet, das 11. und 10. vorangegangene Commit im Master-Zweig zu bezeichnen. Betrachten Sie zur Verdeutlichung den Commit-Graphen in Abbildung 6-8. Zweig M wird über einen Teil seines Commit-Verlaufs dargestellt, der linear ist. M~14
M~13
M~12
M~11
M~10
A
B
M~9 M
Abbildung 6-8: Linearer Commit-Verlauf
Erinnern Sie sich daran, dass die Zeit von links nach rechts verläuft, sodass M~14 das älteste gezeigte, M~9 das neueste gezeigte und A das elftletzte Commit ist. Der Bereich M~12..M~10 repräsentiert zwei Commits, die 11.- und 10.-ältesten Commits, die mit A und B bezeichnet sind. Der Bereich schließt nicht M~12 ein. Wieso nicht? Das ist eine Definitionsfrage. Ein Commit-Bereich, start..end, ist als die Menge der vom Ende end erreichbaren Commits definiert, die nicht von start erreichbar sind. Mit anderen Worten, »das Commit end wird einbezogen«, während »das Commit start ausgeschlossen wird«. Meist wird das durch die Wendung »in end, aber nicht in start« vereinfacht.
Erreichbarkeit in Graphen In der Graphentheorie sagt man, dass ein Knoten X von einem anderen Knoten A aus erreichbar ist, wenn Sie bei A starten, den Regeln entsprechend die Bögen des Graphen entlangreisen und bei X ankommen können. Die Menge der erreichbaren Knoten für einen Knoten A ist die Sammlung aller Knoten, die von A aus erreichbar sind. In einem Git-Commit-Graphen besteht die Menge der erreichbaren Commits aus all denjenigen Commits, die Sie von einem bestimmten Commit aus erreichen können, indem Sie die gerichteten Elternverbindungen durchlaufen. Konzeptuell und in Bezug auf den Datenfluss ist die Menge der erreichbaren Commits die Menge der Vorfahren-Commits, die in ein bestimmtes Anfangs-Commit einfließt und zu diesem beiträgt.
Indem Sie für git log ein Commit Y angeben, fordern Sie Git dazu auf, das Log für alle Commits anzuzeigen, die von Y aus erreichbar sind. Sie können ein Commit X sowie alle Commits, die von X aus erreichbar sind, mit dem Ausdruck ^X ausschließen.
86 | Kapitel 6: Commits
Kombiniert man die beiden Formen, dann ist git log ^X Y das Gleiche wie git log X..Y und könnte als »Gib mir alle Commits, die von Y aus erreichbar sind, und gib mir keine Commits, die bis zu einschließlich X führen« umschrieben werden. Der Commit-Bereich X..Y ist mathematisch äquivalent mit ^X Y. Sie können ihn sich auch als eine Mengensubtraktion vorstellen: Benutze alles, was bis zu Y führt, minus allem, was bis zu einschließlich X führt. Um wieder auf die Commit-Folge aus dem Beispiel zurückzukommen: Hier sehen Sie, wie M~12..M~10 nur zwei Commits festlegt, A und B. Beginnen Sie mit allem, was bis zu M~10 führt, wie in der ersten Zeile von Abbildung 6-9 gezeigt wird. Suchen Sie alles, was bis zu einschließlich M~12 führt, wie in der zweiten Zeile der Abbildung zu sehen ist. Und schließlich subtrahieren Sie M~12 von M~10, um die Commits zu erhalten, die in der dritten Zeile der Abbildung stehen. …
M~14
M~13
M~12
…
…
M~14
M~13
M~11
M~10
A
B
M~11
M~10
A
B
M~12
…
Abbildung 6-9: Hier werden Bereiche als Mengensubtraktion interpretiert
Wenn der Verlauf Ihres Repository eine einfache lineare Folge von Commits darstellt, dann ist es relativ leicht verständlich, wie ein Bereich funktioniert. Sind allerdings Zweige oder Merges an dem Graphen beteiligt, wird alles etwas kompliziert. Deshalb ist es wichtig, die entscheidende Definition zu verstehen. Schauen wir uns einige weitere Beispiele an. Im Fall eines master-Zweigs mit einem linearen Verlauf (wie in Abbildung 6-10) sind die Mengen B..E, ^B E sowie C, D und E äquivalent.
A
B
C
D
E
master
Abbildung 6-10: Ein einfacher linearer Verlauf
Der Commit-Verlauf
| 87
In Abbildung 6-11 wurde der master-Zweig in Commit V mit dem topic-Zweig unter B zusammengeführt.
T
U
A
B
C
D
V
W
X
Y
topic
Z
master
Abbildung 6-11: Master in Topic zusammengeführt
Der Bereich topic..master repräsentiert diese Commits in master, nicht aber in topic. Da jedes Commit auf dem master-Zweig bis einschließlich V (..., T, U, V) zu topic beiträgt, werden diese Commits ausgeschlossen, wodurch W, X, Y und Z übrigbleiben. Die Umkehrung des vorherigen Beispiels wird in Abbildung 6-12 gezeigt. Hier wurde topic in master zusammengeführt.
A
B
V
W
topic
X
Y
Z
master
Abbildung 6-12: Topic in Master zusammengeführt
In diesem Beispiel ist der Bereich topic..master, der wieder diese Commits in master, nicht aber topic repräsentiert, die Menge der Commits auf dem master-Zweig, die bis zu einschließlich V, W, X, Y und Z führt. Wir müssen jedoch ein bisschen vorsichtig sein und den vollständigen Verlauf des topicZweigs betrachten. Bedenken Sie den Fall, wo er ursprünglich als Zweig von master begann und dann wieder mit diesem Zweig zusammengeführt wurde (Abbildung 6-13).
U
A
B
C
D
V
W
X
Y
Abbildung 6-13: Verzweigung und Merge (Zusammenführung)
88 | Kapitel 6: Commits
topic
Z
master
In diesem Fall enthält topic..master nur die Commits W, X, Y und Z. Erinnern Sie sich: Der Bereich schließt alle Commits aus, die (durch Zurück- bzw. Nach-links-Gehen über den Graphen) von topic (d.h. die Commits D, C, B, A und früher) sowie V, U und früher vom Eltern-Commit von B aus erreichbar sind. Das Ergebnis ist einfach W bis Z. Es gibt zwei weitere mögliche Kombinationen: Wenn Sie entweder das start- oder das end-Commit aus dem Bereich herauslassen, wird HEAD angenommen. Das bedeutet, ..end ist äquivalent mit HEAD..end und start.. ist äquivalent mit start..HEAD. Und schließlich, genau wie start..end als Darstellung einer Mengensubtraktionsoperation betrachtet werden kann, repräsentiert die Notation A...B (mit drei Punkten) die symmetrische Differenz zwischen A und B oder die Menge von Commits, die entweder von A oder von B aus erreichbar sind, nicht jedoch von beiden. Wegen der Symmetrie der Funktion kann kein Commit wirklich als »Start« oder »Ende« angesehen werden. In diesem Sinn sind A und B »gleich«. Förmlicher wird die Menge der Revisionen in der symmetrischen Differenz zwischen A und B, A...B, angegeben durch $ git rev-list A B --not $(git merge-base --all A B)
Schauen wir uns das Beispiel in Abbildung 6-14 an.
A
B
C
D
E
F
G
H
U
V
W
X
Y
I
Z
master
dev
Abbildung 6-14: Symmetrische Differenz
Wir können die einzelnen Teile der Definition der symmetrischen Differenz berechnen: master...dev = (master OR dev) AND NOT (merge-base --all master dev)
Die Commits, die zu master beitragen, sind (I, H, ..., B, A, W, V, U). Die Commits, die zu dev beitragen, sind (Z, Y, ..., U, C, B, A). Die Vereinigung dieser beiden Mengen ist (A, ..., I, U, ..., Z). Die Merge-Basis zwischen master und dev ist Commit W. In komplexeren Fällen könnte es mehrere Merge-Basen geben, hier haben wir aber nur eine. Die Commits, die zu W beitragen, sind (W, V, U, C, B und A); das sind auch die Commits, die sowohl master als auch dev gemein haben, die also entfernt werden müssen, um die symmetrische Differenz zu formen: (I, H, Z, Y, X, G, F, E, D). Es hilft vielleicht, wenn Sie sich die symmetrische Differenz zwischen zwei Zweigen A und B so vorstellen: »Zeige alles in Zweig A oder in Zweig B, aber nur zurück bis zu der Stelle, an der sich die beiden Zweige geteilt haben«.
Der Commit-Verlauf
| 89
Nachdem wir uns nun damit befasst haben, was Commit-Bereiche sind, wie man sie schreibt und wie sie funktionieren, müssen wir deutlich machen, dass Git eigentlich keinen echten Bereichsoperator unterstützt. Es ist nur eine Bequemlichkeit in der Schreibweise, dass A..B die zugrunde liegende Form ^A B repräsentiert. Git erlaubt auf seiner Kommandozeile sogar noch eine viel stärkere Manipulation der Commit-Mengen. Befehle, die einen Bereich akzeptieren, nehmen sogar eine beliebige Folge von »eingeschlossenen« und »ausgeschlossenen« Commits entgegen. Sie könnten z.B. git log ^dev ^topic ^bugfix master verwenden, um Commits in master, aber nicht in einem der Zweige dev, topic oder bugfix auszuwählen. Diese Beispiele sind vielleicht ein bisschen abstrakt. Die Stärke der Bereichsdarstellung zeigt sich, wenn Sie bedenken, dass jeder Zweigname als Teil des Bereichs verwendet werden kann. Falls, wie in »Tracking-Zweige« auf Seite 197 beschrieben wird, einer Ihrer Zweige die Commits aus einem anderen Repository repräsentiert, können Sie schnell diejenige Menge der Commits ermitteln, die zwar in Ihrem Repository, nicht jedoch in einem anderen Repository sind!
Commits suchen (und finden) Ein Teil eines guten Revisionskontrollsystems ist die Unterstützung, die es für die »Archäologie« und die Untersuchung eines Repository bietet. Git kennt mehrere Mechanismen, mit deren Hilfe Sie in Ihrem Repository Commits suchen können, die bestimmten Kriterien entsprechen.
git bisect verwenden Der Befehl git bisect stellt ein leistungsfähiges Werkzeug zum Isolieren eines bestimmten fehlerhaften Commit auf der Grundlage praktisch beliebiger Suchkriterien dar. git bisect eignet sich ganz besonders für Gelegenheiten, in denen Sie entdecken, dass irgendetwas »Falsches« oder »Schlechtes« Ihr Repository beeinflusst, und Sie wissen, dass der Code eigentlich in Ordnung war. Nehmen wir z.B. an, dass Sie am Linux-Kernel arbeiten und ein Test-Booten fehlschlägt. Sie sind sich aber sicher, dass das Booten früher schon einmal funktioniert hat – vielleicht letzte Woche oder bei einem früheren Release-Tag. In diesem Fall ist Ihr Repository von einem bekannten »guten« in einen bekannten »schlechten« Zustand übergegangen. Aber wann? Welches Commit hat den Schaden verursacht? Das ist genau die Frage, bei deren Beantwortung Sie git bisect unterstützen soll. Die einzige echte Suchanforderung besteht darin, dass Sie bei ausgechecktem Repository in der Lage sein sollen festzustellen, ob es Ihrer Suchanforderung entspricht oder nicht. In diesem Fall müssen Sie diese Frage beantworten können: »Kompiliert und bootet die Version des Kernels, die ausgecheckt wurde?« Sie müssen außerdem eine »gute« und eine
90 | Kapitel 6: Commits
»schlechte« Version bzw. einen entsprechenden Commit kennen, bevor Sie anfangen, damit die Suche eingeschränkt werden kann. git bisect wird oft verwendet, um ein bestimmtes Commit zu isolieren, das einen Rück-
schritt oder einen Bug in das Repository getragen hat. Wenn Sie z.B. am Linux-Kernel arbeiten, dann kann Ihnen git bisect dabei helfen, Probleme und Bugs zu finden, wie etwa fehlschlagendes Kompilieren, fehlschlagendes Booten oder Boot-Vorgänge, die bestimmte Aufgaben nicht ausführen können oder denen irgendwelche gewünschten Leistungsmerkmale fehlen. In all diesen Fällen unterstützt Sie git bisect dabei, das genaue Commit zu isolieren und zu ermitteln, das das Problem verursacht hat. Der Befehl git bisect wählt systematisch ein neues Commit in einem kleiner werdenden Bereich, der durch das »gute« Verhalten am einen Ende und das »schlechte« Verhalten am anderen Ende eingegrenzt wird. Schließlich wird der Bereich exakt auf das eine Commit eingeengt, das für das fehlerhafte Verhalten verantwortlich ist. Sie müssen nicht mehr tun, als zu Anfang ein gutes und ein schlechtes Commit vorzugeben und dann wiederholt die Frage zu beantworten: »Funktioniert diese Version?« Zu Beginn müssen Sie ein gutes und ein schlechtes Commit ermitteln. In der Praxis handelt es sich bei der schlechten Version oft um Ihren aktuellen HEAD, da dies die Stelle ist, an der Sie arbeiteten, als Sie plötzlich bemerkten, dass etwas schiefläuft oder dass es einen Bug zu beheben gibt. Eine erste gute Version zu finden, kann etwas schwieriger sein, da diese üblicherweise irgendwo im Verlauf begraben ist. Vermutlich können Sie eine Version irgendwo im Verlauf des Repository benennen oder erraten, von der Sie wissen, dass sie richtig funktioniert. Dies könnte ein markiertes Release wie v2.6.25 oder ein Commit vor 100 Revisionen, master~100, auf Ihrem Master-Zweig sein. Idealerweise liegt das gute Commit »nah« bei Ihrem schlechten Commit (master~25 ist besser als master~100) und ist nicht zu weit in der Vergangenheit verschüttet. Auf jeden Fall müssen Sie wissen oder überprüfen können, dass es tatsächlich ein gutes Commit ist. Es ist sehr wichtig, dass Sie den git bisect-Prozess aus einem sauberen Arbeitsverzeichnis heraus starten. Der Prozess arrangiert Ihr Arbeitsverzeichnis notwendigerweise so, dass es mehrere unterschiedliche Versionen Ihres Repository enthält. Wenn Sie mit einem unsauberen Arbeitsverzeichnis beginnen, handeln Sie sich nur Ärger ein; die Änderungen im Arbeitsverzeichnis könnten leicht verloren gehen. Wir verwenden in unserem Beispiel einen Klon des Linux-Kernels und weisen Git nun an, eine Suche zu beginnen: $ cd linux-2.6 $ git bisect start
Nach dem Initiieren einer zweigeteilten Suche tritt Git in einen »Bisect-Modus« ein, in dem es einige Zustandsinformationen für sich selbst einrichtet. Git nutzt einen abgesonderten HEAD, um die aktuelle ausgecheckte Version des Repository zu verwalten. Dieser
Commits suchen (und finden)
| 91
abgesonderte HEAD ist im Prinzip ein anonymer Zweig, der verwendet werden kann, um innerhalb des Repository herumzuhüpfen und bei Bedarf auf unterschiedliche Revisionen zu zeigen. Teilen Sie nun Git mit, welches Commit schlecht ist. Da es sich hierbei typischerweise um Ihre aktuelle Version handelt, stellen Sie einfach die Revision auf Ihren aktuellen HEAD:4 # Sage git, dass die HEAD-Version nicht funktioniert $ git bisect bad
Sagen Sie Git nun noch, welche Version in Ordnung ist: $ git bisect good v2.6.27 Bisecting: 3857 revisions left to test after this [cf2fa66055d718ae13e62451bb546505f63906a2] Merge branch 'for_linus' of git://git.kernel.org/pub/scm/linux/kernel/git/mchehab/linux-2.6
Durch das Identifizieren einer guten und einer schlechten Version wird ein Bereich von Commits eingegrenzt, in dem der Übergang von Gut zu Schlecht auftritt. Git sagt Ihnen bei jedem Schritt entlang des Weges, wie viele Revisionen sich in diesem Bereich befinden. Darüber hinaus verändert Git Ihr Arbeitsverzeichnis, indem es eine Revision auscheckt, die sich ungefähr in der Mitte zwischen den guten und den schlechten Endpunkten befindet. Jetzt müssen Sie diese Frage beantworten: »Ist diese Version gut oder schlecht?« Immer wenn Sie die Frage beantworten, engt Git den Suchraum auf die Hälfte ein, identifiziert eine neue Revision, checkt sie aus und wiederholt die »Gut oder schlecht?«-Frage. Nehmen Sie an, dass diese Version »gut« ist: $ git bisect good Bisecting: 1939 revisions left to test after this [2be508d847392e431759e370d21cea9412848758] Merge git://git.infradead.org/mtd-2.6
Wie Sie sehen, sind von 3857 Revisionen nun noch 1939 im Rennen. Machen wir weiter: $ git bisect good Bisecting: 939 revisions left to test after this [b80de369aa5c7c8ce7ff7a691e86e1dcc89accc6] 8250: Add more OxSemi devices $ git bisect bad Bisecting: 508 revisions left to test after this [9301975ec251bab1ad7cfcb84a688b26187e4e4a] Merge branch 'genirq-v28-for-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/linux-2.6-tip
Bei einem perfekten Bisection-Durchlauf sind log2 der ursprünglichen Anzahl an Revisionsschritten nötig, um die Menge auf ein Commit einzugrenzen.
4
Für die neugierigen Leser, die dieses Beispiel nachvollziehen wollen: HEAD ist hier Commit 49fdf6785fd660e18a1eb4588928f47e9fa29a9a.
92 | Kapitel 6: Commits
Nach einer weiteren »Gut«- und »Schlecht«-Antwort: $ git bisect good Bisecting: 220 revisions left to test after this [7cf5244ce4a0ab3f043f2e9593e07516b0df5715] mfd: check for platform_get_irq() return value in sm501 $ git bisect bad Bisecting: 104 revisions left to test after this [e4c2ce82ca2710e17cb4df8eb2b249fa2eb5af30] ring_buffer: allocate buffer page pointer
Während des Bisection-Prozesses führt Git ein Log Ihrer Antworten zusammen mit deren Commit-IDs: $ git bisect log git bisect start # bad: [49fdf6785fd660e18a1eb4588928f47e9fa29a9a] Merge branch 'for-linus' of git://git.kernel.dk/linux-2.6-block git bisect bad 49fdf6785fd660e18a1eb4588928f47e9fa29a9a # good: [3fa8749e584b55f1180411ab1b51117190bac1e5] Linux 2.6.27 git bisect good 3fa8749e584b55f1180411ab1b51117190bac1e5 # good: [cf2fa66055d718ae13e62451bb546505f63906a2] Merge branch 'for_linus' of git://git.kernel.org/pub/scm/linux/kernel/git/mchehab/linux-2.6 git bisect good cf2fa66055d718ae13e62451bb546505f63906a2 # good: [2be508d847392e431759e370d21cea9412848758] Merge git://git.infradead.org/mtd-2.6 git bisect good 2be508d847392e431759e370d21cea9412848758 # bad: [b80de369aa5c7c8ce7ff7a691e86e1dcc89accc6] 8250: Add more OxSemi devices git bisect bad b80de369aa5c7c8ce7ff7a691e86e1dcc89accc6 # good: [9301975ec251bab1ad7cfcb84a688b26187e4e4a] Merge branch 'genirq-v28-for-linus' of git://git.kernel.org/pub/scm/linux/kernel/git/tip/linux-2.6-tip git bisect good 9301975ec251bab1ad7cfcb84a688b26187e4e4a # bad: [7cf5244ce4a0ab3f043f2e9593e07516b0df5715] mfd: check for platform_get_irq() return value in sm501 git bisect bad 7cf5244ce4a0ab3f043f2e9593e07516b0df5715
Falls Sie sich während dieses Vorgangs verirren oder aus einem anderen Grund neu anfangen wollen, geben Sie den Befehl git bisect replay mit der Logdatei als Eingabe ein. Das ist ein ausgezeichneter Mechanismus, um einen Schritt in diesem Pfad »zurückzugehen« und einen anderen Weg zu erkunden. Wir wollen uns dem Defekt mit fünf weiteren »Schlecht«-Antworten annähern: $ git bisect bad Bisecting: 51 revisions left to test after this [d3ee6d992821f471193a7ee7a00af9ebb4bf5d01] ftrace: make it depend on DEBUG_KERNEL $ git bisect bad Bisecting: 25 revisions left to test after this [3f5a54e371ca20b119b73704f6c01b71295c1714] ftrace: dump out
Commits suchen (und finden)
| 93
ftrace buffers to console on panic $ git bisect bad Bisecting: 12 revisions left to test after this [8da3821ba5634497da63d58a69e24a97697c4a2b] ftrace: create _mcount_loc section $ git bisect bad Bisecting: 6 revisions left to test after this [fa340d9c050e78fb21a142b617304214ae5e0c2d] tracing: disable tracepoints by default $ git bisect bad Bisecting: 2 revisions left to test after this [4a0897526bbc5c6ac0df80b16b8c60339e717ae2] tracing: tracepoints, samples
Sie könnten den Befehl git bisect visualize verwenden, um die Menge der Commits, die sich immer noch im Bereich unserer Überlegungen befinden, visuell zu untersuchen. Git benutzt das grafische Werkzeug gitk, wenn die Umgebungsvariable DISPLAY gesetzt ist. Falls nicht, nimmt Git stattdessen git log. In diesem Fall könnte auch --pretty=oneline ganz nützlich sein. $ git bisect visualize --pretty=oneline fa340d9c050e78fb21a142b617304214ae5e0c2d b07c3f193a8074aa4afe43cfa8ae38ec4c7ccfa9 0a16b6075843325dc402edf80c1662838b929aff instrumentation - scheduler 4a0897526bbc5c6ac0df80b16b8c60339e717ae2 24b8d831d56aac7907752d22d2aba5d8127db6f6 97e1c18e8d17bd87e1e383b2e9d9fc740332c8e2
tracing: disable tracepoints by default ftrace: port to tracepoints tracing, sched: LTTng tracing: tracepoints, samples tracing: tracepoints, documentation tracing: Kernel Tracepoints
Die aktuelle Revision, die infrage kommt, befindet sich ungefähr in der Mitte des Bereichs: $ git bisect good Bisecting: 1 revisions left to test after this [b07c3f193a8074aa4afe43cfa8ae38ec4c7ccfa9] ftrace: port to tracepoints
Wenn Sie schließlich die letzte Revision testen und Git die eine Revision isoliert hat, die das Problem verursacht hat,5 wird sie angezeigt: $ git bisect good fa340d9c050e78fb21a142b617304214ae5e0c2d is first bad commit commit fa340d9c050e78fb21a142b617304214ae5e0c2d Author: Ingo Molnar <[email protected]> Date: Wed Jul 23 13:38:00 2008 +0200 tracing: disable tracepoints by default
5
Nein, dieses Commit hat nicht unbedingt ein Problem verursacht. Die »Gut«- und »Schlecht«-Antworten wurden konstruiert, und deshalb sind wir hier gelandet.
94 | Kapitel 6: Commits
while it's arguably low overhead, we don't enable new features by default. Signed-off-by: Ingo Molnar <[email protected]> :040000 040000 4bf5c05869a67e184670315c181d76605c973931 fd15e1c4adbd37b819299a9f0d4a6ff589721f6c M init
Wenn schließlich Ihr Bisection-Durchlauf fertig ist und Sie das Bisection-Log und den gesicherten Zustand abgeschlossen haben, müssen Sie Git das unbedingt mitteilen. Wie Sie sich sicher erinnern, wird der gesamte Bisection-Vorgang, also die zweigeteilte Suche, auf einem abgesonderten HEAD durchgeführt: $ git branch * (no branch) master $ git bisect reset Switched to branch "master" $ git branch * master
Mit dem Befehl git bisect reset gelangen Sie wieder zurück in Ihren ursprünglichen Zweig.
git blame benutzen Ein weiteres Werkzeug zum Identifizieren eines bestimmten Commit ist git blame. Dieser Befehl teilt Ihnen mit, wer zuletzt die einzelnen Zeilen einer Datei modifiziert hat und bei welchem Commit die Änderung bestätigt wurde: $ git blame -L 35, init/version.c 4865ecf1 (Serge E. Hallyn 2006-10-02 02:18:14 -0700 35) }, ^1da177e (Linus Torvalds 2005-04-16 15:20:36 -0700 36) }; 4865ecf1 (Serge E. Hallyn 2006-10-02 02:18:14 -0700 37) EXPORT_SYMBOL_GPL(init_uts_ns); 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 38) c71551ad (Linus Torvalds 2007-01-11 18:18:04 -0800 39) /* FIXED STRINGS! Don't touch! */ c71551ad (Linus Torvalds 2007-01-11 18:18:04 -0800 40) const char linux_banner[] = 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 41) "Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@" 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 42) LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n"; 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 43) 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 44) const char linux_proc_banner[] = 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 45) "%s version %s" 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 46) " (" LINUX_COMPILE_BY "@" LINUX_COMPILE_HOST ")" 3eb3c740 (Roman Zippel 2007-01-10 14:45:28 +0100 47) " (" LINUX_COMPILER ") %s\n";
Commits suchen (und finden)
| 95
Mit der Spitzhacke suchen Während git blame Sie über den aktuellen Zustand einer Datei in Kenntnis setzt, durchsucht git log -Sstring den Verlauf der Diffs einer Datei nach einem vorgegebenen String. Indem er die eigentlichen Diffs, also die Unterschiede zwischen den Revisionen durchsucht, kann der Befehl Commits finden, die eine Änderung sowohl bei Ergänzungen als auch bei den Löschungen durchführen. $ git log -Sinclude --pretty=oneline --abbrev-commit init/version.c cd354f1... [PATCH] remove many unneeded #includes of sched.h 4865ecf... [PATCH] namespaces: utsname: implement utsname namespaces 63104ee... kbuild: introduce utsrelease.h 1da177e... Linux-2.6.12-rc2
Jedes der auf der linken Seite aufgeführten Commits (cd354f1… usw.) fügt entweder Zeilen hinzu oder löscht Zeilen, die das Wort include enthalten. Seien Sie jedoch vorsichtig: Wenn ein Commit exakt die gleiche Anzahl an Zeilen mit Ihrer Schlüsselphrase sowohl hinzufügt als auch löscht, wird das nicht angezeigt. Damit gezählt wird, muss das Commit eine Änderung in der Anzahl der Ergänzungen und der Löschungen aufweisen. Die Option -S von git log wird auch als Pickaxe (Spitzhacke) bezeichnet. Das ist sozusagen Archäologie mit roher Gewalt.
96 | Kapitel 6: Commits
Kapitel 7
KAPITEL 7
Zweige
Ein Zweig (auch englisch »branch« genannt) ist das grundlegende Mittel zum Starten einer separaten Entwicklungslinie in einem Softwareprojekt. Bei einem Zweig handelt es sich um eine Abspaltung aus einer Art vereinigtem Ausgangszustand, die es erlaubt, die Entwicklung gleichzeitig in mehrere Richtungen voranzutreiben und potenziell verschiedene Versionen des Projekts herzustellen. Oft wird ein Zweig an anderen Zweigen abgestimmt und wieder mit ihnen zusammengeführt, um getrennt durchgeführte Leistungen wieder zu vereinen. Git lässt viele Zweige und damit auch viele unterschiedliche Entwicklungslinien innerhalb eines Repository zu. Das Verzweigungssystem von Git ist leichtgewichtig und einfach. Darüber hinaus bietet Git eine erstklassige Unterstützung für Merges (Zusammenführungen). Aus diesem Grund verwenden die meisten Git-Benutzer Zweige gern und oft. In diesem Kapitel zeigen wir Ihnen, wie Sie Zweige auswählen, anlegen, betrachten und entfernen. Außerdem bieten wir Ihnen einige bewährte Methoden, damit Ihre Zweige nicht am Ende einem Manzanita-Baum ähneln.1
Gründe für den Einsatz von Zweigen Für die Erzeugung eines Zweiges gibt es eine schier endlose Zahl von technischen, philosophischen, betrieblichen und sogar sozialen Gründen. Hier sind einige verbreitete Erklärungen: • Ein Zweig repräsentiert oft eine individuelle Kundenversion. Falls Sie mit Version 1.1 Ihres Projekts beginnen wollen, aber wissen, dass einige Ihrer Kunden lieber bei Version 1.0 bleiben, dann pflegen Sie die alte Version als separaten Zweig weiter. 1
OK, OK. Das ist ein kleiner, buschartiger Baum, der sich sehr stark verzweigt. Er kommt vor allem in Nordamerika vor. Heimischer Verwandter sind die Bärentrauben. Eine bessere Analogie ist vielleicht der BanyanBaum.
| 97
• Ein Zweig kann eine Entwicklungsphase einkapseln, wie etwa den Prototyp, die Beta-Version, eine stabile oder noch nicht ausgereifte Ausgabe. Stellen Sie sich die Version »1.1« einfach ebenfalls als getrennte Phase vor – die gepflegte Version. • Ein Zweig kann die Entwicklung einer speziellen Funktion oder die Untersuchungen über einen besonders komplexen Bug isolieren. So könnten Sie z.B. einen Zweig einführen, um eine wohldefinierte und konzeptuell isolierte Aufgabe auszuführen oder einen Merge mehrerer Zweige vor der Freigabe einer Version zu erleichtern. Es mag Ihnen vielleicht übertrieben erscheinen, nur zum Ausmerzen eines Bugs einen neuen Zweig anzulegen, doch das Verzweigungssystem von Git eignet sich für solche kleinen Einsatzfälle ganz besonders gut. • Ein einzelner Zweig kann die Arbeit eines einzelnen Mitarbeiters repräsentieren. Ein weiterer Zweig – der »Integrations«-Zweig – dient dann speziell dazu, die Arbeiten zu vereinen. Git bezeichnet solche Zweige als Topic-Zweige oder Entwicklungszweige. Das Wort »Topic« (Thema) deutet darauf hin, dass jeder Zweig im Repository einen bestimmten Zweck verfolgt. Außerdem kennt Git das Konzept eines Tracking-Zweigs oder Zweigs, mit dem die Klone eines Repository synchron gehalten werden. In Kapitel 11 wird näher erläutert, wie man Tracking-Zweige benutzt.
Zweig oder Tag? Zweige und Tags scheinen ähnlich, vielleicht sogar austauschbar zu sein. Wann sollte man daher einen Tag-Namen und wann einen Zweignamen benutzen? Tags und Zweige dienen unterschiedlichen Zwecken. Ein Tag soll ein statischer Name sein, der sich über die Zeit nicht ändert oder verschiebt. Einmal angewandt, sollten Sie es in Ruhe lassen. Es dient als Stütze im Boden und als Referenzpunkt. Ein Zweig andererseits ist dynamisch und verschiebt sich mit jedem Commit, das Sie ausführen. Der Zweigname ist dazu gedacht, Ihrer laufenden Entwicklung zu folgen. Seltsamerweise können Sie einem Zweig und einem Tag den gleichen Namen geben. In diesem Fall müssen Sie ihre vollständige Ref-Namen verwenden, um sie zu unterscheiden. Z.B. könnten Sie refs/tags/v1.0 und refs/heads/v1.0 benutzen. Möglicherweise wollen Sie den gleichen Namen als Zweignamen während der Entwicklung verwenden und ihn dann beim Abschluss Ihrer Entwicklung in einen Tag-Namen umwandeln. Wie Sie Zweige und Tags letztendlich nennen, müssen Sie selbst festlegen, etwa in Ihren Projektrichtlinien. Sie müssen jedoch das wesentliche Unterscheidungsmerkmal beachten: Ist dieser Name statisch und unveränderbar oder ist er für die Entwicklung dynamisch? Die erste Variante sollte ein Tag werden, die zweite ein Zweig.
98 | Kapitel 7: Zweige
Zweignamen Der Name, den Sie einem Zweig zuweisen, ist prinzipiell beliebig, allerdings gibt es einige Einschränkungen. Der Standardzweig in einem Repository wird als master bezeichnet. In diesem Zweig bewahren die meisten Entwickler die robusteste und zuverlässigste Entwicklungslinie des Repository auf. An dem Namen master ist nichts Magisches, außer dass Git ihn während der Initialisierung eines Repository einführt. Falls Sie wollen, können Sie den master-Zweig umbenennen oder sogar löschen. Allerdings ist es wahrscheinlich am besten, wenn Sie ihn so lassen, wie er ist. Um die Skalierbarkeit und die grundsätzliche Organisation zu unterstützen, können Sie einen hierarchischen Zweignamen anlegen, der einem Unix-Pfadnamen ähnelt. Nehmen Sie z.B. an, dass Sie Teil eines Entwicklungsteams sind, das eine große Menge Bugs behebt. Es könnte sich als sinnvoll erweisen, die Entwicklung der einzelnen Reparaturen in eine hierarchische Struktur unter dem Zweignamen bug zu legen, und zwar in separate Zweige, die irgendwie bug/pr-1023 und bug/pr-17 heißen. Falls Sie feststellen, dass Sie sehr viele Zweige haben oder einfach nur unglaublich gut organisiert sind, können Sie diese Schrägstrichsyntax verwenden, um Ihre Zweignamen zu organisieren. Ein Grund für die Benutzung von hierarchischen Zweignamen besteht darin, dass Git, genau wie die Unix-Shell, Wildcards unterstützt. Z.B. könnten Sie bei einem Namensschema wie bug/pr-1023 und bug/pr-17 mit einem cleveren und vertrauten Kürzel alle bug-Zweige auf einmal auswählen. git show-branch 'bug/*'
Was man in Zweignamen tun und lassen sollte Zweignamen müssen einigen einfachen Regeln genügen: • Sie können den Schrägstrich (/) benutzen, um ein hierarchisches Namensschema zu erzeugen. Allerdings darf der Name nicht mit einem Schrägstrich enden. • Komponenten, die nicht durch Schrägstriche getrennt sind, dürfen mit einem Punkt (.) beginnen. Ein Zweigname wie feature/.new ist ungültig • Der Name darf nicht zwei aufeinanderfolgende Punkte (..) enthalten. • Außerdem darf der Name Folgendes nicht enthalten: • Ein Leerzeichen oder ein anderes Whitespace-Zeichen • Ein Zeichen, das für Git eine besondere Bedeutung besitzt, darunter die Tilde (~), das Dach (^), den Doppelpunkt (:), das Fragezeichen (?), den Asterisk (*) und die geöffnete eckige Klammer ([) • Ein ASCII-Steuerzeichen, d.h. ein Byte mit einem niedrigeren Wert als \040 oktal oder das DEL-Zeichen (\177 oktal)
Zweignamen
| 99
Diese Regeln für Zweignamen werden vom Plumbing-Befehl git check-ref-format durchgesetzt und sollen sicherstellen, dass die Zweignamen sowohl einfach eingegeben als auch als Dateinamen innerhalb des .git-Verzeichnisses und der Skripten benutzt werden können.
Zweige benutzen Es kann in einem Repository zu jedem Zeitpunkt viele unterschiedliche Zweige geben, allerdings ist immer nur höchstens ein Zweig »aktiv« oder »aktuell«. Der aktive Zweig bestimmt, welche Dateien in das Arbeitsverzeichnis ausgecheckt werden. Darüber hinaus ist der aktuelle Zweig oft ein impliziter Operand in Git-Befehlen, wie etwa das Ziel der Merge-Operation. Standardmäßig ist master der aktive Zweig; Sie können aber jeden Zweig zum aktuellen Zweig machen. In Kapitel 6 habe ich Commit-Graph-Diagramme vorgestellt, die mehrere Zweige enthalten. Denken Sie an diese Graphenstruktur, wenn Sie Zweige manipulieren, weil Ihnen das dabei hilft, das elegante und einfache Objektmodell zu etablieren, das den Zweigen in Git zugrunde liegt.
Ein Zweig erlaubt dem Inhalt des Repository, in viele Richtungen, nämlich eine pro Zweig, auseinanderzudriften. Sobald ein Repository wenigstens einen Zweig abgeteilt hat, wird ein Commit entweder auf den einen oder den anderen Zweig angewandt, je nachdem, welcher aktiv ist. Jeder Zweig in einem bestimmten Repository muss einen eindeutigen Namen tragen. Der Name bezieht sich immer auf die neueste Revision, die auf diesem Zweig durch ein Commit bestätigt wurde. Das neueste Commit auf einem Zweig wird Spitze oder Kopf des Zweiges genannt. Git zeichnet keine Informationen darüber auf, woher ein Zweig stammt. Stattdessen verschiebt sich der Zweigname schrittweise vorwärts, wenn auf dem Zweig neue Commits vorgenommen werden. Ältere Commits müssen deshalb mit ihrem Hash oder über einen relativen Namen wie dev~5 benannt werden. Falls Sie ein bestimmtes Commit im Auge behalten wollen – weil es z.B. einen stabilen Punkt im Projekt repräsentiert oder es sich um eine Version handelt, die Sie testen wollen –, können Sie ihm explizit einen TagNamen zuweisen. Da das Original-Commit, mit dem ein Zweig gestartet wurde, nicht ausdrücklich gekennzeichnet ist, kann man dieses Commit (oder sein Äquivalent) algorithmisch suchen, indem man den Namen des Originalzweigs verwendet, aus dem der neue Zweig abgespaltet wurde: $ git merge-base Originalzweig neuer-Zweig
Ein Merge (Zusammenführung) ist das Gegenteil eines Zweigs bzw. einer Verzweigung. Wenn Sie einen Merge ausführen, wird der Inhalt eines oder mehrerer Zweige mit einem
100 | Kapitel 7: Zweige
impliziten Zielzweig zusammengeführt. Allerdings eliminiert ein Merge weder irgendwelche Quellzweige noch deren Namen. Der ziemlich komplexe Vorgang des Zusammenführens von Zweigen steht im Zentrum von Kapitel 9. Sie können sich einen Zweignamen als einen Zeiger auf ein bestimmtes (wiewohl sich entwickelndes) Commit vorstellen. Ein Zweig enthält die Commits, die ausreichend sind, um den gesamten Verlauf des Projekts entlang des Zweigs, aus dem es gekommen ist, zu rekonstruieren, und zwar bis zurück an den Anfang des Projekts. In Abbildung 7-1 verweist der Zweigname dev auf das oberste Commit (den Kopf) Z. Wollten Sie den Repository-Zustand bei Z wiederherstellen, wären alle Commits erforderlich, die von Z zurück bis zum Original-Commit A erreichbar sind. Der erreichbare Teil des Graphen ist mit dicken Linien hervorgehoben und deckt alle Commits ab außer (S, G, H, J, K, L).
P
Q
R
U
A
B
C
D
V
S
W
E
X
F J
testing
Y
G K
H L
Z
dev
master
Stable
Abbildung 7-1: Commits, die von dev aus erreichbar sind
Alle Zweignamen sowie der durch Commits bestätigte Inhalt in den einzelnen Zweigen liegen lokal in Ihrem Repository. Wenn Sie Ihr Repository anderen zugänglich machen, können Sie diese veröffentlichen bzw. beschließen, einen oder mehrere Zweige mit den dazugehörigen Commits anderen zur Verfügung zu stellen. Das Veröffentlichen eines Zweigs muss explizit geschehen. Falls Ihr Repository geklont ist, werden Ihre Zweignamen und die Entwicklung auf diesen Zweigen ebenfalls Teil der neu geklonten Repository-Kopie.
Zweige erzeugen Ein neuer Zweig beruht auf einem vorhandenen Commit innerhalb des Repository. Es bleibt ganz Ihnen überlassen, das Commit zu ermitteln und festzulegen, das als Start des neuen Zweiges dienen soll. Git unterstützt eine beliebig komplexe Verzweigungsstruktur, einschließlich verzweigender Zweige und der Abspaltung mehrerer Zweige vom selben Commit.
Zweige erzeugen
| 101
Auch die Lebensdauer eines Zweigs unterliegt Ihrer Entscheidung. Ein Zweig kann kurzoder langlebig sein. Während der Lebenszeit des Repository darf ein bestimmter Zweigname mehrere Male hinzugefügt und gelöscht werden. Nachdem Sie das Commit identifiziert haben, von dem ein Zweig ausgehen soll, setzen Sie einfach den Befehl git branch. Das heißt, um einen neuen Zweig vom HEAD Ihres aktuellen Zweigs aus zu erzeugen, damit Problem Report #1138 erledigt wird, würden Sie folgenden Befehl benutzen: $ git branch prs/pr-1138
Die Grundform des Befehls sieht so aus: git branch Zweig [Start-Commit]
Wenn kein Start-Commit angegeben ist, wird standardmäßig die Revision benutzt, die zuletzt auf dem aktuellen Zweig durch ein Commit bestätigt wurde. Mit anderen Worten wird ein neuer Zweig standardmäßig dort gestartet, wo Sie gerade arbeiten. Beachten Sie, dass der Befehl git branch lediglich den Namen eines Zweigs in das Repository einführt. Er ändert nicht Ihr Arbeitsverzeichnis dahingehend, dass der neue Zweig benutzt wird. Es ändern sich keine Dateien des Arbeitsverzeichnisses, es kommt nicht zu impliziten Änderungen des Zweigkontexts, es werden keine neuen Commits ausgeführt. Der Befehl erzeugt lediglich einen benannten Zweig am angegebenen Commit. Sie können nicht einmal anfangen, auf dem neuen Zweig zu arbeiten, da Sie zuerst dorthin wechseln müssen, wie später in »Zweige auschecken« auf Seite 105 gezeigt wird. Manchmal wollen Sie ein anderes Commit als Anfang eines Zweigs angeben. Nehmen Sie z.B. an, dass Ihr Projekt für jeden gemeldeten Bug einen neuen Zweig anlegt und Sie von einem Bug in einer bestimmten Version hören. Es ist sicher bequemer, den Parameter Start-Commit zu benutzen, als das Arbeitsverzeichnis in den Zweig umzuschalten, der diese Version repräsentiert. Normalerweise gibt es in Ihrem Projekt Konventionen, die es Ihnen erlauben, mit Sicherheit ein Start-Commit festzulegen. Um z.B. ein Bugfix an der Version 2.3 Ihrer Software vorzunehmen, könnten Sie einen Zweig namens rel-2.3 als Start-Commit erzeugen: $ git branch prs/pr-1138 rel-2.3
Der einzige Commit-Name, der garantiert eindeutig ist, ist die Hash-ID. Falls Sie die Hash-ID kennen, können Sie sie direkt benutzen: $ git branch prs/pr-1138 db7de5feebef8bcd18c5356cb47c337236b50c13
Zweignamen auflisten Der Befehl git branch listet die Zweignamen auf, die er im Repository findet: $ git branch bug/pr-1 dev * master
102 | Kapitel 7: Zweige
In diesem Beispiel werden drei Topic-Zweige gezeigt. Der Zweig, der gerade in Ihren Arbeitsbaum ausgecheckt ist, wird durch den Asterisk gekennzeichnet. Dieses Beispiel zeigt außerdem zwei weitere Zweige, nämlich bug/pr-1 und dev. Ohne zusätzliche Parameter werden nur die Topic-Zweige im Repository aufgelistet. Wie Sie in Kapitel 11 sehen werden, könnte es zusätzliche Remote-Tracking-Zweige in Ihrem Repository geben. Diese listen Sie mit der Option -r auf. Sowohl Topic- als auch RemoteZweige listen Sie mit -a auf.
Zweige anschauen Der Befehl git show-branch liefert eine ausführlichere Ausgabe als git branch, indem er in ungefähr umgekehrt chronologischer Reihenfolge die Commits auflistet, die zu einem oder mehreren Zweigen beitragen. Wie bei git branch werden ohne Optionen die TopicZweige aufgelistet, -r zeigt Remote-Tracking-Zweige und -a zeigt alle Zweige. Schauen wir uns ein Beispiel an: $ git show-branch ! [bug/pr-1] Fix Problem Report 1 * [dev] Neue Entwicklung verbessern ! [master] Bobs Fixes hinzugefügt --* [dev] Neue Entwicklung verbessern * [dev^] Eine neue Entwicklung beginnen + [bug/pr-1] Fix Problem Report 1 +*+ [master] Bobs Fixes hinzugefügt
Die git show-branch-Ausgabe ist in zwei Abschnitte unterteilt, die durch eine Zeile mit Bindestrichen getrennt sind. Der Abschnitt über der Trennlinie zeigt die Namen der Zweige eingeschlossen in eckige Klammern, und zwar je einen pro Zeile. Jeder Zweigname ist mit einer einzelnen Ausgabespalte verknüpft, die entweder mit einem Ausrufezeichen oder – falls es sich außerdem um den aktuellen Zweig handelt – einem Asterisk gekennzeichnet ist. In dem gezeigten Beispiel starten die Commits im Zweig bug/pr-1 in der ersten Spalte, Commits im aktuellen Zweig dev starten in der zweiten Spalte und Commits im dritten Zweig master starten in der dritten Spalte. Für einen schnelleren Zugriff ist für jeden Zweig im oberen Abschnitt außerdem die erste Zeile der Lognachricht aus dem neuesten Commit auf diesem Zweig aufgeführt. Der untere Abschnitt der Ausgabe ist eine Matrix, die angibt, welche Commits in jedem Zweig vorhanden sind. Auch hier ist jedes Commit mit der ersten Zeile der Lognachricht für dieses Commit aufgeführt. In einem Zweig ist ein Commit vorhanden, wenn es ein Pluszeichen (+), einen Asterisk (*) oder ein Minuszeichen (–) in der Spalte dieses Zweigs gibt. Das Pluszeichen zeigt an, dass das Commit in einem Zweig ist, der Asterisk hebt hervor, dass das Commit im aktiven Zweig vorhanden ist. Das Minuszeichen weist auf ein Merge-Commit hin.
Zweige anschauen
| 103
Beide folgende Commits sind z.B. durch Asteriske gekennzeichnet und im dev-Zweig vorhanden: * *
[dev] Neue Entwicklung verbessern [dev^] Eine neue Entwicklung starten
Diese beiden Commits sind in einem anderen Zweig nicht vertreten. Sie sind in umgekehrter chronologischer Reihenfolge aufgelistet: Das neueste Commit steht oben, das älteste unten. Eingeschlossen in eckige Klammern auf jeder Commit-Zeile zeigt Git außerdem einen Namen für dieses Commit. Wie bereits erwähnt, weist Git den Zweignamen dem neuesten Commit zu. Vorangegangene Commits tragen den gleichen Namen mit einem abschließenden Dach (^). In Kapitel 6 haben Sie master als Namen des neuesten Commit und master^ als Namen für das vorletzte Commit kennengelernt. Entsprechend sind dev und dev^ die Namen der beiden neuesten Commits im Zweig dev. Die Commits innerhalb eines Zweig sind zwar sortiert, die Zweige selbst aber werden in beliebiger Reihenfolge aufgelistet. Das liegt daran, dass alle Zweige den gleichen Status haben; es gibt keine Regel, die besagt, dass ein Zweig wichtiger wäre als ein anderer. Falls ein Commit in mehreren Zweigen vorhanden ist, trägt es ein Pluszeichen oder einen Asterisk für jeden Zweig. Das heißt, dass das letzte Commit, das in der vorherigen Ausgabe zu sehen war, in allen drei Zweigen vorkommt: +*+ [master] Bobs Fixes hinzugefügt
Das erste Pluszeichen bedeutet, dass das Commit sich in bug/pr-1 befindet, der Asterisk heißt, dass das gleiche Commit auch im aktiven Zweig dev zu finden ist, und das letzte Pluszeichen bedeutet, dass das Commit auch im master-Zweig vorliegt. Wenn der Befehl git show-branch aufgerufen wird, durchläuft er alle Commits in allen Zweigen, die gezeigt werden, und stoppt das Auflisten auf dem neuesten Commit, das in allen Zweigen vorkommt. In diesem Fall hat Git vier Commits aufgeführt, bevor es eines gefunden hat, das in allen drei Zweigen vorhanden ist (Bobs Fixes hinzugefügt). Dort hat es dann angehalten. Das Stoppen beim ersten gemeinsamen Commit ist eine Standardheuristik für ein vernünftiges Verhalten. Es wird angenommen, dass das Erreichen eines solchen gemeinsamen Punkts genügend Kontext liefert, um zu verstehen, wie die Zweige miteinander zusammenhängen. Falls Sie aus irgendeinem Grund in der Geschichte der Commits weiter zurückgehen wollen, benutzen Sie die Option --more=Zahl, mit der Sie die Anzahl der zusätzlichen Commits angeben, die Sie sehen wollen, während Sie entlang des gemeinsamen Zweigs in der Zeit zurückwandern. Der Befehl git show-branch akzeptiert eine Menge von Zweignamen als Parameter, wodurch er Ihnen erlaubt, den Verlauf, der für diese Zweige gezeigt wird, zu beschränken. Falls z.B. ein neuer Zweig namens bug/pr-2 hinzugefügt wird, der beim masterCommit startet, würde das so aussehen:
104 | Kapitel 7: Zweige
$ git branch bug/pr-2 master $ git show-branch ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt * [dev] Neue Entwicklung verbessern ! [master] Bobs Fixes hinzugefügt ---* [dev] Neue Entwicklung verbessern * [dev^] Eine neue Entwicklung starten + [bug/pr-1] Fix Problem Report 1 ++*+ [bug/pr-2] Bobs Fixes hinzugefügt
Mit dem folgenden Befehl zeigen Sie den Commit-Verlauf nur für die Zweige bug/pr-1 und bug/pr-2: $ git show-branch bug/pr-1 bug/pr-2
Bei nur einigen wenigen Zweigen mag das ja in Ordnung sein, wenn es aber viele solche Zweige gäbe, würde es sehr schnell nerven, sie alle einzeln zu nennen. Zum Glück erlaubt Git die Wildcard-Filterung von Zweignamen. Mit dem einfacheren bug/*-WildcardNamen kann man das gleiche Ergebnis erzielen: $ git show-branch bug/pr-1 bug/pr-2 ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt -+ [bug/pr-1] Fix Problem Report 1 ++ [bug/pr-2] Bobs Fixes hinzugefügt $ git show-branch bug/* ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt -+ [bug/pr-1] Fix Problem Report 1 ++ [bug/pr-2] Bobs Fixes hinzugefügt
Zweige auschecken Wie bereits weiter vorn in diesem Kapitel erwähnt, kann Ihr Arbeitsverzeichnis nur einen Zweig gleichzeitig wiedergeben. Um das Arbeiten in einem anderen Zweig zu starten, führen Sie den Befehl git checkout aus. git checkout macht den angegebenen Zweig zum neuen, aktuellen Arbeitszweig. Es ändert die Datei- und Verzeichnisstruktur Ihres Arbeitsbaums so, dass diese dem Zustand des angegebenen Zweigs entspricht. Wie Sie allerdings sehen werden, baut Git Sicherungen ein, damit Sie keine Daten verlieren, die Sie noch nicht durch Commits bestätigt haben. Darüber hinaus bietet Ihnen git checkout Zugriff auf alle Zustände des Repository von der Spitze des Zweigs bis an den Anfang des Projekts. Wie Sie sich vielleicht aus Kapitel 6 erinnern, hat das seinen Grund darin, dass jedes Commit einen Schnappschuss des gesamten Repository-Zustands zu einem bestimmten Augenblick aufzeichnet.
Zweige auschecken
| 105
Ein einfaches Beispiel für das Auschecken eines Zweigs Nehmen Sie an, Sie wollen vom dev-Zweig im Beispiel aus dem vorigen Abschnitt wechseln und Ihre Aufmerksamkeit stattdessen der Lösung des Problems zuwenden, das mit dem Zweig bug/pr-1 verknüpft ist. Schauen wir uns den Zustand des Arbeitsverzeichnisses vor und nach git checkout an: $ git branch bug/pr-1 bug/pr-2 * dev master $ git checkout bug/pr-1 Switched to branch "bug/pr-1" $ git branch * bug/pr-1 bug/pr-2 dev master
Die Dateien und die Verzeichnisstruktur Ihres Arbeitsverzeichnisses wurden an den Zustand und den Inhalt des neuen Zweigs bug/pr-1 angepasst. Um jedoch die Dateien zu sehen, die Ihr Arbeitsverzeichnis gewechselt hat, um den Zustand an der Spitze dieses Zweigs widerzuspiegeln, müssen Sie einen normalen Unix-Befehl wie ls benutzen. Das Auswählen eines neuen Zweigs könnte dramatische Auswirkungen auf Ihre Arbeitsbaumdateien und die Verzeichnisstruktur haben. Natürlich hängt das Ausmaß dieser Änderung von den Unterschieden zwischen Ihrem aktuellen Zweig und dem neuen Zielzweig ab, den Sie auschecken wollen. Das Wechseln der Zweige hat folgende Auswirkungen: • Dateien und Verzeichnisse, die in dem Zweig vorliegen, der ausgecheckt wird, aber nicht im aktuellen Zweig, werden aus dem Objektspeicher ausgecheckt und in den Arbeitsbaum gelegt. • Dateien und Verzeichnisse, die im aktuellen Zweig vorhanden sind, in dem ausgecheckten Zweig jedoch fehlen, werden aus dem Arbeitsbaum entfernt. • Dateien, die in beiden Zweigen vorkommen, werden verändert und spiegeln damit den Inhalt wider, der im ausgecheckten Zweig vorhanden ist. Seien Sie nicht beunruhigt, wenn es so aussieht, als würde das Auschecken fast sofort geschehen. Ein verbreiteter Anfängerfehler besteht darin, zu denken, dass das Auschecken nicht funktioniert hat, weil das System sofort zurückkehrt, nachdem angeblich riesige Änderungen vorgenommen wurden. Das ist eine der Eigenschaften, in denen sich Git wirklich und stark von vielen anderen Versionskontrollsystemen unterscheidet. Git ist ausgesprochen gut darin, die paar Dateien und Verzeichnisse zu ermitteln, die tatsächlich beim Auschecken geändert werden mussten.
106 | Kapitel 7: Zweige
Auschecken trotz unbestätigter Änderungen Git verhindert das versehentliche Löschen oder Verändern von Daten in Ihrem lokalen Arbeitsbaum, ohne dass Sie das ausdrücklich anfordern müssen. Dateien und Verzeichnisse, die nicht überwacht werden, werden immer in Ruhe gelassen; Git entfernt oder modifiziert sie nicht. Falls Sie jedoch lokale Änderungen an einer Datei haben, die sich von den Änderungen im neuen Zweig unterscheiden, gibt Git eine solche Fehlermeldung aus und weigert sich, den Zielzweig auszuchecken: $ git branch bug/pr-1 bug/pr-2 dev * master $ git checkout dev error: Entry 'NeuerKram' not uptodate. Cannot merge.
In diesem Fall warnt eine Meldung Sie, dass irgendetwas Git dazu gebracht hat, die Anforderung zum Auschecken zu stoppen. Aber was? Sie können es herausfinden, indem Sie den Inhalt der Datei NeuerKram untersuchen, da diese lokal im aktuellen Arbeitsverzeichnis modifiziert wurde. Außerdem schauen Sie sich den Zielzweig dev an: # Zeigen, wie NeuerKram im Arbeitsverzeichnis aussieht $ cat NeuerKram Irgendetwas Irgendetwas anderes # Zeigen, dass die lokale Version der Datei eine zusätzliche Zeile enthält, die # im aktuellen Zweig des Arbeitsverzeichnisses (master) nicht bestätigt ist $ git diff NeuerKram diff --git a/NeuerKram b/NeuerKram index 0f2416e..5e79566 100644 --- a/NeuerKram +++ b/NeuerKram @@ -1 +1,2 @@ Irgendetwas +Irgendetwas anderes # Zeigen, wie die Datei im dev-Zweig aussieht $ git show dev:NeuerKram Irgendetwas Eine Änderung
Hätte Git frecherweise die Anforderung zum Auschecken des dev-Zweigs befolgt, wären Ihre lokalen Modifikationen von NeuerKram in Ihrem Arbeitsverzeichnis durch die Version von dev überschrieben worden. Git entdeckt standardmäßig diesen potenziellen Verlust und verhindert, dass er geschieht.
Zweige auschecken
| 107
Falls es Ihnen egal ist, ob Änderungen in Ihrem Arbeitsverzeichnis verloren gehen, können Sie Git mit der Option -f zwingen, das Auschecken durchzuführen.
Allerdings weist der Cannot merge-Teil der Fehlermeldung von Git darauf hin, wie Git Ihre Datei schützen kann, wenn Sie ihm die Möglichkeit dazu einräumen. Wir wollen untersuchen, was Sie tun müssen, um den Wechsel durchzuführen und dennoch Ihre Änderungen zu sichern. Die Fehlermeldung, die besagt, dass NeuerKram not uptodate (nicht auf dem neuesten Stand) ist, könnte darauf hindeuten, dass Sie die Datei innerhalb des Index aktualisieren müssen und dann mit dem Auschecken fortfahren können. Allerdings reicht das noch nicht. Der Einsatz von git add, beispielsweise zum Aktualisieren des neuen Inhalts von NeuerKram im Index, setzt nur den Inhalt dieser Datei in den Index; es wird kein Commit durchgeführt. Git kann den neuen Zweig immer noch nicht auschecken, ohne dass die Änderungen verloren gehen. Das funktioniert also auch nicht. $ git add NeuerKram $ git checkout dev error: Entry 'NeuerKram' would be overwritten by merge. Cannot merge.
Tatsächlich würde es immer noch überschrieben werden. Es reicht also nicht, die Änderung dem Index hinzuzufügen. Sie könnten an dieser Stelle auch einfach git commit aufrufen, um die Änderung im aktuellen Zweig (master) zu bestätigen. Nehmen Sie aber an, Sie wollen, dass die Änderung stattdessen im neuen Zweig dev geschieht. Es scheint ausweglos zu sein: Sie können Ihre Änderung erst in den Zweig dev packen, wenn sie ausgecheckt ist, und Git erlaubt Ihnen das Auschecken nicht, weil es eine Änderung gegeben hat. Zum Glück gibt es einen Weg aus dieser Zwickmühle.
Änderungen in einem anderen Zweig zusammenführen Im vorangegangenen Abschnitt widersprach der aktuelle Zustand Ihres Arbeitsverzeichnisses dem des Zweigs, in den Sie wechseln wollten. Daher ist ein Merge nötig: Die Änderungen in Ihrem Arbeitsverzeichnis müssen mit den Dateien zusammengeführt werden, die ausgecheckt werden. Falls möglich oder speziell mit der Option -m angefordert, versucht Git Ihre lokale Änderung in das neue Arbeitsverzeichnis zu überführen, indem es eine Merge-Operation zwischen Ihren lokalen Änderungen und dem Zielzweig ausführt. $ git checkout -m dev M NeuerKram Switched to branch "dev"
Git hat hier die Datei NeuerKram geändert und den Zweig dev erfolgreich ausgecheckt.
108 | Kapitel 7: Zweige
Diese Merge-Operation erfolgt vollständig in Ihrem Arbeitsverzeichnis. Sie führt auf keinem der Zweige ein Merge-Commit ein. In gewisser Weise ist sie analog zum Befehl cvs update, da die lokalen Änderungen mit den Zielzweig zusammengeführt werden und in Ihrem Arbeitsverzeichnis verbleiben. Sie müssen in diesen Szenarien allerdings vorsichtig sein. Es mag zwar so aussehen, als ob der Merge sauber durchgeführt worden wäre und alles in Ordnung sei, jedoch hat Git einfach nur die Datei modifiziert und die Merge-Konfliktindikatoren dringelassen; Sie müssen also vorhandene Konflikte immer noch auflösen: $ cat NeuerKram Irgendetwas <<<<<<< dev:NeuerKram Eine Änderung ======= Irgendetwas anderes >>>>>>> local:NeuerKram
In Kapitel 9 erfahren Sie mehr über Merges und lernen hilfreiche Techniken kennen, mit denen sich Merge-Konflikte lösen lassen. Falls Git einen Zweig auschecken, in ihn wechseln und Ihre lokalen Änderungen sauber und ohne Konflikte zusammenführen kann, ist die Anforderung zum Auschecken erfolgreich verlaufen. Nehmen Sie einmal an, Sie sind im master-Zweig in Ihrem Entwicklungs-Repository und haben einige Änderungen an der Datei NeuerKram vorgenommen. Außerdem merken Sie, dass die Änderungen, die Sie gemacht haben, eigentlich an einem anderen Zweig hätten geschehen sollen, vielleicht weil sie Problem Report #1 erledigen und im Zweig bug/ pr-1 mit einem Commit bestätigt werden sollten. Gehen Sie so vor: Starten Sie im master-Zweig. Nehmen Sie Änderungen an einigen Dateien vor; diese Änderungen werden hier durch das Hinzufügen des Textes Ein Bugfix zu der Datei NeuerKram repräsentiert: $ git show-branch ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt ! [dev] Mit dem Entwickeln von NeuerKram begonnen * [master] Bobs Fixes hinzugefügt ---+ [dev] Mit dem Entwickeln von NeuerKram begonnen + [dev^] Neue Entwicklung verbessern + [dev~2] Eine neue Entwicklung starten + [bug/pr-1] Fix Problem Report 1 +++* [bug/pr-2] Bobs Fixes hinzugefügt $ echo "Ein Bugfix" >> NeuerKram $ cat NeuerKram Irgendetwas Ein Bugfix
Zweige auschecken
| 109
An dieser Stelle merken Sie, dass die gesamte Arbeit im Zweig bug/pr-1 mit einem Commit bestätigt werden sollte und nicht im master-Zweig. Zum Vergleich, so sieht die Datei NeuerKram im Zweig bug/pr-1 vor dem Auschecken im nächsten Schritt aus: $ git show bug/pr-1:NeuerKram Irgendetwas
Um Ihre Änderungen in den gewünschten Zweig zu überführen, versuchen Sie einfach, ihn auszuchecken : $ git checkout bug/pr-1 M NeuerKram Switched to branch "bug/pr-1" $ cat NeuerKram Irgendetwas Ein Bugfix
Git war hier in der Lage, die Änderungen aus Ihren Arbeitsverzeichnissen und den Zielzweig korrekt zusammenzuführen und sie in Ihrer neuen Arbeitsverzeichnisstruktur zu lassen. Mit dem Befehl git diff könnten Sie überprüfen, ob der Merge tatsächlich erwartungsgemäß abgelaufen ist: $ git diff diff --git a/NeuerKram b/NeuerKram index 0f2416e..b4d8596 100644 --- a/NeuerKram +++ b/NeuerKram @@ -1 +1,2 @@ Irgendetwas +Ein Bugfix
Der einzeilige Zusatz ist korrekt.
Einen neuen Zweig anlegen und auschecken Ein anderes relativ häufig auftretendes Szenario tritt ein, wenn Sie einen neuen Zweig anlegen und gleichzeitig in ihn wechseln wollen. Git bietet mit der Option -b neuer-Zweig eine Abkürzung zu diesem Ziel. Beginnen wir an derselben Stelle wie im vorigen Beispiel. Allerdings müssen Sie nun einen neuen Zweig starten, anstatt Änderungen in einen bestehenden Zweig einzuchecken. Mit anderen Worten, Sie befinden sich im master-Zweig, bearbeiten Dateien und merken plötzlich, dass Sie alle Änderungen lieber in einem völlig neuen Zweig namens bug/pr-3 bestätigen wollen. Sie gehen so vor: $ git branch bug/pr-1 bug/pr-2 dev * master $ git checkout -b bug/pr-3
110 | Kapitel 7: Zweige
M NeuerKram Switched to a new branch "bug/pr-3" $ git show-branch ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt * [bug/pr-3] Bobs Fixes hinzugefügt ! [dev] Mit dem Entwickeln von NeuerKram begonnen ! [master] Bobs Fixes hinzugefügt ----+ [dev] Mit dem Entwickeln von NeuerKram begonnen + [dev^] Neue Entwicklung verbessern + [dev~2] Eine neue Entwicklung starten + [bug/pr-1] Fix Problem Report 1 ++*++ [bug/pr-2] Bobs Fixes hinzugefügt
Solange nicht irgendein Problem verhindert, dass ein Befehl zum Auschecken abgeschlossen wird, entspricht der Befehl $ git checkout -b neuer-Zweig Startpunkt
genau dieser Sequenz aus zwei Befehlen: $ git branch neuer-Zweig Startpunkt $ git checkout neuer-Zweig
Abgesonderte HEAD-Zweige Normalerweise ist es ratsam, nur die Spitze eines Zweigs auszuchecken, indem man den Zweig direkt benennt. Daher wechselt git checkout standardmäßig zur Spitze eines gewünschten Zweigs. Sie können jedoch ein beliebiges Commit auschecken. In so einem Fall erzeugt Git eine Art anonymen Zweig für Sie, der als abgesonderter HEAD bezeichnet wird. Git legt einen abgesonderten HEAD an, wenn Sie: • Ein Commit auschecken, das nicht der Kopf eines Zweigs ist. • Einen Tracking-Zweig auschecken. Sie könnten das tun, um Änderungen zu untersuchen, die zuvor aus einem entfernten Repository in Ihr Repository gebracht wurden. • Das Commit auschecken, das von einem Tag referenziert wird. Sie könnten das tun, um ein Release auf der Grundlage der markierten Versionen der Dateien zusammenzustellen. • Eine git bisect-Operation starten, wie in »git bisect verwenden« auf Seite 90 beschrieben. • Den Befehl git submodule update einsetzen. In diesen Fällen teilt Ihnen Git mit, dass Sie in einen abgesonderten HEAD gewechselt sind: # Ich habe eine Kopie der Git-Quellen bei der Hand! $ cd git.git
Zweige auschecken
| 111
$ git checkout v1.6.0 Note: moving to "v1.6.0" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b HEAD is now at ea02eef... GIT 1.6.0
Falls Sie, wenn Sie sich selbst auf einem abgesonderten HEAD wiederfinden, später beschließen, dass Sie an dieser Stelle neue Commits ausführen und diese behalten wollen, müssen Sie zuerst einen neuen Zweig erzeugen: $ git checkout -b neuer-Zweig
Dadurch erhalten Sie einen neuen, sauberen Zweig auf der Grundlage des Commit, auf dem der abgesonderte HEAD war. Sie können dann mit der normalen Entwicklung fortfahren. Im Prinzip haben Sie dem Zweig, der zuvor anonym war, einen Namen verpasst. Um herauszufinden, ob Sie auf einem abgesonderten HEAD sind, fragen Sie einfach: $ git branch * (no branch) master
Falls Sie andererseits mit dem abgesonderten HEAD fertig sind und diesen Zustand einfach aufgeben wollen, können Sie zu einem benannten Zweig konvertieren, indem Sie einfach git checkout Zweig eingeben: $ git checkout master Previous HEAD position was ea02eef... GIT 1.6.0 Checking out files: 100% (608/608), done. Switched to branch "master" $ git branch * master
Zweige löschen Der Befehl git branch -d Zweig löscht den benannten Zweig aus einem Repository. Git verhindert, dass Sie den aktuellen Zweig entfernen: $ git branch -d bug/pr-3 error: Cannot delete the branch 'bug/pr-3' which you are currently on.
Durch das Löschen des aktuellen Zweigs würde Git nicht mehr feststellen können, wie der resultierende Arbeitsverzeichnisbaum aussehen soll. Sie müssen stattdessen immer einen nicht aktuellen Zweig nennen. Es gibt allerdings noch ein anderes kleines Problem: Git erlaubt es Ihnen nicht, einen Zweig zu löschen, der Commits enthält, die nicht auch auf dem aktuellen Zweig vorhanden sind. Das bedeutet, dass Git verhindert, dass Sie versehentlich Entwicklungen in Commits löschen, die verloren wären, wenn der Zweig gelöscht werden würde.
112 | Kapitel 7: Zweige
$ git checkout master Switched to branch "master" $ git branch -d bug/pr-3 error: The branch 'bug/pr-3' is not an ancestor of your current HEAD. If you are sure you want to delete it, run 'git branch -D bug/pr-3'.
In der git show-branch-Ausgabe ist das Commit »Einen Bugfix für pr-3 hinzugefügt« nur im Zweig bug/pr-3 zu finden. Falls dieser Zweig gelöscht würde, gäbe es keine Möglichkeit mehr, auf dieses Commit zuzugreifen. Indem Git anmerkt, dass der Zweig bug/pr-3 kein Vorfahr Ihres aktuellen HEAD ist, teilt es Ihnen mit, dass die Entwicklungslinie, die durch den Zweig bug/pr-3 repräsentiert wird, nicht zu der Entwicklung des aktuellen Zweigs, master, beiträgt. Git verlangt nicht, dass alle Zweige in den master-Zweig eingefügt werden, bevor sie gelöscht werden können. Denken Sie daran: Ein Zweig ist einfach nur ein Name oder Zeiger auf ein Commit, das tatsächlichen Inhalt enthält. Stattdessen hält Git Sie davon ab, versehentlich Inhalt aus dem zu löschenden Zweig zu verlieren, der nicht in Ihren aktuellen Zweig eingefügt ist. Wenn der Inhalt aus dem gelöschten Zweig bereits in dem anderen Zweig vorhanden ist, würden das Auschecken dieses Zweigs und das anschließende Anfordern des Löschens des Zweigs aus diesem Kontext funktionieren. Ein anderer Ansatz besteht darin, den Inhalt aus dem Zweig, den Sie löschen wollen, mit dem aktuellen Zweig zusammenzuführen (siehe Kapitel 9). Danach kann der andere Zweig sicher gelöscht werden: $ git merge bug/pr-3 Updating 7933438..401b78d Fast forward NeuerKram | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) $ git show-branch ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt ! [bug/pr-3] Einen Bugfix für pr-3 hinzugefügt ! [dev] Mit dem Entwickeln von NeuerKram begonnen * [master] Einen Bugfix für pr-3 hinzugefügt ----+ * [bug/pr-3] Einen Bugfix für pr-3 hinzugefügt + [dev] Mit dem Entwickeln von NeuerKram begonnen + [dev^] Neue Entwicklung verbessern + [dev~2] Eine neue Entwicklung starten + [bug/pr-1] Fix Problem Report 1 ++++* [bug/pr-2] Bobs Fixes hinzugefügt $ git branch -d bug/pr-3 Deleted branch bug/pr-3.
Zweige löschen | 113
$ git show-branch ! [bug/pr-1] Fix Problem Report 1 ! [bug/pr-2] Bobs Fixes hinzugefügt ! [dev] Mit dem Entwickeln von NeuerKram begonnen * [master] Einen Bugfix für pr-3 hinzugefügt ---* [master] Einen Bugfix für pr-3 hinzugefügt + [dev] Mit dem Entwickeln von NeuerKram begonnen + [dev^] Neue Entwicklung verbessern + [dev~2] Eine neue Entwicklung starten + [bug/pr-1] Fix Problem Report 1 +++* [bug/pr-2] Bobs Fixes hinzugefügt
Wie die Fehlermeldung vorschlägt, können Sie schließlich die Sicherheitsüberprüfung von Git außer Kraft setzen, indem Sie -D anstelle von -d benutzen. Tun Sie das, wenn Sie sich sicher sind, dass Sie den zusätzlichen Inhalt in diesem Zweig nicht haben wollen. Git führt keinerlei historische Aufzeichnungen über die Zweignamen, die erzeugt, verschoben, manipuliert, zusammengeführt oder gelöscht werden. Sobald ein Zweigname entfernt wurde, ist er verschwunden. Der Verlauf der Commits in diesem Zweig ist jedoch eine andere Geschichte. Git entfernt irgendwann schließlich Commits, die nicht mehr referenziert werden und von einem benannten Ref, wie etwa einem Zweig- oder Tag-Namen, aus erreichbar sind. Falls Sie diese Commits behalten wollen, müssen Sie sie entweder mit einem anderen Zweig zusammenführen, einen Zweig für sie anlegen oder eine Tag-Referenz auf sie richten. Ohne einen Verweis auf sie sind Commits und Blobs ansonsten unerreichbar und werden schließlich vom Werkzeug git gc als Müll entsorgt. Falls Sie versehentlich Zweige oder andere Refs entfernt haben, stellen Sie sie wieder her. Dazu verwenden Sie den Befehl git reflog. Andere Befehle, wie etwa git fsck, und Konfigurationsoptionen, wie etwa gc.reflogExpire und gc.pruneExpire, helfen Ihnen ebenfalls dabei, verloren gegangene Commits, Dateien und Zweigköpfe wiederherzustellen.
114 | Kapitel 7: Zweige
Kapitel 8
KAPITEL 8
Diffs
Bei einem Diff handelt es sich um eine kompakte Zusammenfassung der Unterschiede (Differenzen, daher der Name »Diff«) zwischen zwei Elementen. Sind z.B. zwei Dateien gegeben, vergleicht der Unix- und Linux-diff-Befehl die Dateien zeilenweise und fasst die Abweichungen in einem Diff zusammen, wie im folgenden Code demonstriert wird. Im Beispiel ist initial eine Version mit einem Text, rewrite ist eine nachfolgende Überarbeitung. Die Option -u erzeugt ein vereinheitlichtes Diff, ein standardisiertes Format, das weit verbreitet ist, um Modifikationen weiterzugeben. $ cat initial Now is the time For all good men To come to the aid Of their country.
$ cat rewrite Today is the time For all good men And women To come to the aid Of their country.
$ diff -u initial rewrite --- initial 1867-01-02 11:22:33.000000000 -0500 +++ rewrite 2000-01-02 11:23:45.000000000 -0500 @@ -1,4 +1,5 @@ -Now is the time +Today is the time For all good men +And women To come to the aid Of their country.
Schauen wir uns das Diff genauer an. Im Header wird die ursprüngliche Datei durch --angedeutet und die neue Datei durch +++. Die @@-Zeile liefert den Zeilennummerkontext für beide Dateiversionen. Eine Zeile, die mit einem Minuszeichen (-) eingeleitet wird, muss aus der Originaldatei gelöscht werden, um die neue Datei herzustellen. Umgekehrt muss eine Zeile mit einem Pluszeichen (+) am Anfang zum Original hinzugefügt werden, um die neue Datei zu erzeugen. Eine Zeile, die mit einem Leerzeichen beginnt, ist in beiden Dateien gleich und wird von der Option -u als Kontext mitgeliefert.
| 115
An sich bietet ein Diff keinen Grund oder keine Begründung für eine Änderung, noch rechtfertigt es den Ausgangs- oder Endzustand. Dennoch liefert ein Diff mehr als nur eine Kurzfassung der Unterschiede zweier Dateien. Es bietet eine förmliche Beschreibung, wie man eine Datei in eine andere überführen kann. (Solche Anweisungen sind sinnvoll, wenn man Änderungen durchführt oder rückgängig macht.) Darüber hinaus kann ein Diff sogar noch erweitert werden, um die Unterschiede zwischen mehreren Dateien und ganzen Verzeichnishierarchien anzuzeigen. Das Unix-diff kann die Unterschiede für alle Dateipaare berechnen, die in zwei Verzeichnishierarchien zu finden sind. Der Befehl diff -r durchläuft alle Hierarchien hintereinander, paart die Dateien anhand der Pfadnamen (z. B. original/src/main.c und neu/src/main.c) und fasst die Unterschiede zwischen den einzelnen Paaren zusammen. diff -r -u erzeugt eine Menge von vereinheitlichten Diffs, mit der zwei Hierarchien verglichen werden. Git besitzt seinen eigenen Diff-Mechanismus und kann daher gleichfalls eine Zusammenfassung von Unterschieden ausgeben. Der Befehl git diff kann Dateien vergleichen, ähnlich dem Unix-diff-Befehl. Außerdem kann Git wie bei diff -r zwei Baumobjekte durchlaufen und eine Darstellung der Abweichungen generieren. git diff weist aber seine eigenen Feinheiten und leistungsfähigen Eigenschaften auf, die auf die speziellen Bedürfnisse von Git-Benutzern zugeschnitten sind. Technisch gesehen, repräsentiert ein Baumobjekt nur eine Verzeichnisebene im Repository. Es enthält Informationen über die unmittelbaren Dateien des Verzeichnisses sowie die direkten Unterverzeichnisse, katalogisiert aber nicht den kompletten Inhalt aller Unterverzeichnisse. Da jedoch ein Baumobjekt auf die Baumobjekte der einzelnen Unterverzeichnisse verweist, repräsentiert das Baumobjekt an der Wurzel des Projekts im Prinzip das gesamte Projekt zu einem bestimmten Zeitpunkt. Daher können wir durchaus davon sprechen, dass git diff »zwei« Bäume durchläuft.
In diesem Kapitel behandeln wir einige der Grundlagen von git diff sowie einige seiner besonderen Fähigkeiten. Sie werden lernen, wie Sie Git dazu bringen, redaktionelle Änderungen in Ihrem Arbeitsverzeichnis ebenso anzuzeigen wie beliebige Änderungen zwischen zwei Commits innerhalb Ihres Projektverlaufs. Sie erfahren, wie das Git-Diff Ihnen dabei hilft, wohlstrukturierte Commits während Ihres normalen Entwicklungsprozesses durchzuführen, und erlernen außerdem die Herstellung von Git-Patches, die in Kapitel 13 ausführlicher beschrieben werden.
Formen des git diff-Befehls Wenn Sie zwei unterschiedliche Baumobjekte auf der Wurzelebene zum Vergleich herausgreifen, liefert git diff alle Abweichungen zwischen den beiden Projektzuständen. Das ist wirkungsstark. Sie könnten ein solches Diff einsetzen, um in großem Maßstab
116 | Kapitel 8: Diffs
von einem Projektzustand in einen anderen zu konvertieren. Falls z.B. Sie und Ihr Mitarbeiter Code für dasselbe Projekt entwickeln, könnte ein Diff auf Wurzelebene im Prinzip jederzeit die Repositories synchronisieren. Es gibt prinzipiell drei Quellen für Baum- oder baumartige Objekte, die mit git diff benutzt werden, nämlich • ein Baumobjekt irgendwo innerhalb des gesamten Commit-Graphen, • Ihr Arbeitsverzeichnis und • den Index. Typischerweise werden die Bäume, die in einem git diff-Befehl verglichen werden, über Commits, Zweignamen oder Tags angesprochen. Es reicht jedoch ein Commit-Name, wie in »Commits festlegen« auf Seite 73 besprochen wird. Außerdem können sowohl die Datei- und Verzeichnishierarchie Ihres Arbeitsverzeichnisses als auch die komplette Hierarchie der Dateien, die im Index bereitgestellt werden, als Bäume behandelt werden. Der Befehl git diff kann vier grundlegende Vergleiche mit verschiedenen Kombinationen der genannten drei Quellen durchführen: git diff git diff zeigt den Unterschied zwischen Ihrem Arbeitsverzeichnis und dem Index.
Es stellt heraus, was in Ihrem Arbeitsverzeichnis unsauber ist und daher ein Kandidat, um für das nächste Commit bereitgestellt zu werden. Dieser Befehl enthüllt nicht die Unterschiede zwischen dem, was sich in Ihrem Index befindet, und dem, was permanent im Repository gespeichert ist (ganz zu schweigen von den entfernten Repositories, mit denen Sie möglicherweise noch arbeiten). git diff Commit
Diese Form fasst die Unterschiede zwischen Ihrem Arbeitsverzeichnis und dem angegebenen Commit zusammen. Gebräuchliche Varianten dieses Befehls nennen HEAD oder einen bestimmten Zweignamen als Commit. git diff --cached Commit
Dieser Befehl zeigt die Unterschiede zwischen den bereitgestellten Änderungen im Index und dem angegebenen Commit. Ein gebräuchliches Commit für den Vergleich – und die Vorgabe, falls kein Commit genannt wurde – ist HEAD. Mit HEAD zeigt Ihnen dieser Befehl, wie Ihr nächstes Commit den aktuellen Zweig verändert. Falls Ihnen die Option --cached komisch vorkommt, dann denken Sie vielleicht lieber an das Synonym --staged. Es ist seit Git-Version 1.6.1 verfügbar. git diff Commit1 Commit2
Wenn Sie zwei beliebige Commits angeben, zeigt dieser Befehl die Unterschiede zwischen den beiden. Er ignoriert den Index und das Arbeitsverzeichnis. Er ist die Waffe der Wahl für beliebige Vergleiche zwischen zwei Bäumen, die sich bereits in Ihrem Objektspeicher befinden.
Formen des git diff-Befehls
| 117
Die Anzahl der Parameter auf der Kommandozeile bestimmt, welche grundlegende Form benutzt und was verglichen wird. Sie können zwei Commits oder Bäume vergleichen. Die zu vergleichenden Elemente müssen nicht in einer direkten oder indirekten Eltern-KindBeziehung zueinander stehen. Wenn Sie keine Baumobjekte angeben, vergleicht git diff implizite Quellen, etwa Ihren Index oder das Arbeitsverzeichnis. Wir wollen untersuchen, wie sich diese unterschiedlichen Formen in das Git-Objektmodell einfügen. Das Beispiel in Abbildung 8-1 zeigt ein Projektverzeichnis mit zwei Dateien. Die Datei datei1 wurde im Arbeitsverzeichnis geändert; Ihr Inhalt lautet jetzt nicht mehr »foo«, sondern »quux«. Diese Änderung wurde mit git add datei1 im Index bereitgestellt, aber noch nicht mit einem Commit bestätigt. Working directory project
file1 This is the working directory version
file2 quux
bar
git diff
Index git diff HEAD file1
file2
Object store
git diff --cached master HEAD
a23bf This is the HEAD version
9d3a2 foo
bar
bd71363 quux This is the version “in the index”
Abbildung 8-1: Verschiedene Dateiversionen, die miteinander verglichen werden können
118 | Kapitel 8: Diffs
Es wurde jeweils eine Version der Datei datei1 im Arbeitsverzeichnis, im Index und im HEAD erkannt. Auch wenn die Version von datei1, die »im Index« ist, bd71363, eigentlich als Blob-Objekt im Objektspeicher gespeichert wurde, wird sie doch indirekt über das virtuelle Baumobjekt, das der Index ist, referenziert. Ebenso wird die HEAD-Version der Datei, a23bf, durch mehrere Schritte indirekt referenziert. Dieses Beispiel demonstriert die Änderungen in datei1 nur dem Namen nach. Die fetten Pfeile in der Abbildung zeigen auf die Baum- oder virtuellen Baumobjekte, um Sie daran zu erinnern, dass der Vergleich tatsächlich auf den kompletten Bäumen basiert und nicht nur auf einzelnen Dateien. Anhand von Abbildung 8-1 können Sie nun sehen, dass die Benutzung von git diff ohne Argumente eine gute Technik zum Überprüfen der Bereitschaft des nächsten Commit ist. Solange dieser Befehl Ausgaben von sich gibt, haben Sie Änderungen oder Bearbeitungen in Ihrem Arbeitsverzeichnis, die noch nicht bereitgestellt wurden. Prüfen Sie die Bearbeitungen an den einzelnen Dateien. Wenn Sie mit Ihrer Arbeit zufrieden sind, stellen Sie die Datei mit git add bereit. Nachdem Sie das getan haben, ergibt das nächste git diff keine Diff-Ausgabe mehr für diese Datei. Auf diese Weise können Sie sich schrittweise durch alle unsauberen Dateien in Ihrem Arbeitsverzeichnis bewegen, bis es keine Diffs mehr gibt, was bedeuten würde, dass alle Dateien im Index bereitgestellt sind. Vergessen Sie nicht, außerdem nach neuen oder gelöschten Dateien zu suchen. Der Befehl git diff --cached zeigt jederzeit während der Bereitstellung die ergänzenden Änderungen und/ oder solche Änderungen, die bereits im Index bereitsgestellt wurden und im nächsten Commit verarbeitet werden. Wenn Sie fertig sind, erfasst git commit alle Änderungen in Ihrem Arbeitsverzeichnis für ein neues Commit. Sie sind nicht gezwungen, alle Änderungen aus Ihrem Arbeitsverzeichnis für ein einzelnes Commit bereitzustellen. Um genau zu sein: Falls Sie feststellen, dass Sie konzeptuell unterschiedliche Änderungen in Ihrem Arbeitsverzeichnis haben, die in unterschiedliche Commits gelangen sollten, können Sie eine Gruppe zu einem Zeitpunkt bereitstellen und die anderen Änderungen weiterhin im Arbeitsverzeichnis lassen. Ein Commit erfasst nur Ihre bereitgestellten Änderungen. Wiederholen Sie dann den Vorgang, indem Sie die nächste Gruppe von Dateien für ein passendes nachfolgendes Commit bereitstellen. Der scharfsinnige Leser wird sicher bemerkt haben, dass wir zwar vier grundlegende Formen des git diff-Befehls vorgestellt haben, aber nur drei mit fetten Pfeilen in Abbildung 8-1 hervorgehoben wurden. Welches ist also die vierte? Es gibt nur ein Baumobjekt, das von Ihrem Arbeitsverzeichnis repräsentiert wird, und es gibt nur ein Baumobjekt, das vom Index repräsentiert wird. Im Beispiel gibt es ein Commit im Objektspeicher mit seinem Baum. Allerdings besitzt der Objektspeicher wahrscheinlich viele Commits, die nach unterschiedlichen Zweigen und Tags benannt sind, die alle Bäume haben, die mit git diff verglichen werden können. Daher vergleicht die vierte Form von git diff einfach zwei beliebige Commits (Bäume), die bereits im Objektspeicher gespeichert wurden.
Formen des git diff-Befehls
| 119
Zusätzlich zu den vier Grundformen von git diff gibt es noch eine Unzahl von Optionen. Hier sehen Sie einige der nützlicheren: --M Die Option --M entdeckt Umbenennungen und generiert eine vereinfachte Ausgabe, die anstelle des kompletten Entfernens und anschließenden Hinzufügens der Quelldatei lediglich die Dateiumbenennung aufzeichnet. Wurde beim Umbenennen nicht nur der Name gewechselt, sondern auch der Inhalt geändert, gibt Git das bekannt. -w oder --ignore-all-space Sowohl -w als auch --ignore-all-space vergleichen Zeilen, ohne Whitespace-Änderungen gesondert zu beachten. --stat Die Option --stat fügt Statistiken über den Unterschied zwischen zwei Baumzuständen hinzu. Ihre Ausgabe besteht aus einer kompakten Syntax, die aussagt, wie viele Zeilen sich geändert haben, wie viele hinzugefügt und wie viele ignoriert wurden. --color Die Option --color koloriert die Ausgabe; die unterschiedlichen Arten von Änderungen in dem Diff werden jeweils durch eigene Farben repräsentiert. Man kann den Befehl git diff schließlich noch so einschränken, dass er nur die Diffs für bestimmte Dateien oder Verzeichnisse zeigt. Die Option -a für git diff ist nicht einmal entfernt der Option -a für git commit ähnlich. Um sowohl bereitgestellte als auch nicht bereitgestellte Änderungen zu erhalten, verwendet man git diff HEAD. Dieser Mangel an Symmetrie ist nicht nur bedauerlich, sondern auch wenig intuitiv.
Ein einfaches git diff-Beispiel Wir konstruieren hier das Szenario aus Abbildung 8-1, gehen es durch und schauen uns die verschiedenen Formen von git diff in Aktion an. Zuerst wollen wir ein einfaches Repository mit zwei Dateien einrichten: $ mkdir /tmp/diff_beispiel $ cd /tmp/diff_beispiel $ git init Initialized empty Git repository in /tmp/diff_example/.git/ $ echo "foo" > datei1 $ echo "bar" > datei2 $ git add datei1 datei2 $ git commit -m"datei1 und datei2 hinzufügen" [master (root-commit)]: created fec5ba5: "datei1 und datei2 hinzufügen"
120 | Kapitel 8: Diffs
2 files changed, 2 insertions(+), 0 deletions(-) create mode 100644 datei1 create mode 100644 datei2
Nun bearbeiten wir datei1, indem wir das Wort »foo« durch »quux« ersetzen: $ echo "quux" > datei1
Die datei1 wurde im Arbeitsverzeichnis verändert, aber noch nicht bereitgestellt. Dieser Zustand entspricht nicht der Situation, die in Abbildung 8-1 dargestellt wird, aber Sie können trotzdem einen Vergleich vornehmen. Sie sollten eine Ausgabe erwarten, wenn Sie das Arbeitsverzeichnis mit dem Index oder den vorhandenen HEAD-Versionen vergleichen. Allerdings dürfte es keinen Unterschied zwischen dem Index und dem HEAD geben, da nichts bereitgestellt wurde. (Mit anderen Worten: Das, was bereitgestellt wurde, ist immer noch der aktuelle HEAD-Baum.) # Vergleich von Arbeitsverzeichnis und Index $ git diff diff --git a/datei1 b/datei1 index 257cc56..d90bda0 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +quux # Vergleich von Arbeitsverzeichnis und HEAD $ git diff HEAD diff --git a/datei1 b/datei1 index 257cc56..d90bda0 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +quux # Vergleich von Index und HEAD, immer noch identisch $ git diff --cached $
git diff hat also eine Ausgabe erzeugt, und datei1 kann bereitgestellt werden. Also tun
wir das jetzt: $ git add datei1 $ git status # On branch master # Changes to be committed: # (use "git reset HEAD ..." to unstage) # # modified: datei1
Ein einfaches git diff-Beispiel
| 121
Hier wurde exakt die Situation kopiert, die in Abbildung 8-1 beschrieben ist. Da datei1 jetzt bereitgestellt ist, sind das Arbeitsverzeichnis und der Index synchron und sollten keine Unterschiede zeigen. Es gibt nun jedoch Unterschiede zwischen der HEAD-Version und sowohl dem Arbeitsverzeichnis als auch der bereitgestellten Version im Index: # Vergleich von Arbeitsverzeichnis und Index $ git diff # Vergleich von Arbeitsverzeichnis und HEAD $ git diff HEAD diff --git a/datei1 b/datei1 index 257cc56..d90bda0 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +quux # Vergleich von Index und HEAD $ git diff --cached diff --git a/datei1 b/datei1 index 257cc56..d90bda0 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +quux
Führten Sie nun git commit aus, würde das neue Commit die bereitgestellten Änderungen erfassen, die vom letzten Befehl gezeigt wurden, nämlich git diff --cached (der, wie bereits erwähnt, das neue Synonym git diff --staged besitzt). Um nun einmal alles durcheinanderzubringen: Was würde passieren, wenn Sie datei1 bearbeitet hätten, bevor Sie ein Commit durchgeführt hätten? Schauen wir einmal! $ echo "baz" > datei1 # Vergleich von Arbeitsverzeichnis und Index $ git diff diff --git a/datei1 b/datei1 index d90bda0..7601807 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -quux +baz # Vergleich von Arbeitsverzeichnis und HEAD $ git diff HEAD diff --git a/datei1 b/datei1 index 257cc56..7601807 100644
122 | Kapitel 8: Diffs
--- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +baz # Vergleich von Index und HEAD $ git diff --cached diff --git a/datei1 b/datei1 index 257cc56..d90bda0 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +quux
Alle drei Diff-Operationen zeigen nun irgendeine Form von Unterschied! Welche Version wird jedoch bestätigt werden? Erinnern Sie sich: git commit erfasst den Zustand, der im Index vorherrscht. Und was ist im Index? Es ist der Inhalt, der durch den Befehl git diff --cached oder git diff --staged aufgezeigt wird, oder die Version von datei1, die das Wort »quux« enthält! $ git commit -m"quux für alle" [master]: created f8ae1ec: "quux für alle" 1 files changed, 1 insertions(+), 1 deletions(-)
Der Objektspeicher enthält nun zwei Commits; wir wollen daher die allgemeine Form des git diff-Befehls ausprobieren: # Vergleich von vorhergehender HEAD-Version mit aktueller HEAD-Version $ git diff HEAD^ HEAD diff --git a/datei1 b/datei1 index 257cc56..d90bda0 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -foo +quux
Dieses Diff bestätigt, dass das vorhergehende Commit datei1 geändert hat, indem es »foo« durch »quux« ersetzt hat. Ist nun alles synchronisiert? Nein. Die Arbeitsverzeichniskopie von datei1 enthält »baz«: $ git diff diff --git a/datei1 b/datei1 index d90bda0..7601807 100644 --- a/datei1 +++ b/datei1 @@ -1 +1 @@ -quux +baz
Ein einfaches git diff-Beispiel
| 123
git diff und Commit-Bereiche Es gibt zwei zusätzliche Formen von git diff, die einiger Erläuterung bedürfen, vor allem im Vergleich zu git log. Der Befehl git diff unterstützt eine Syntax mit zwei Punkten, um den Unterschied zwischen zwei Commits darzustellen. Die folgenden beiden Befehle sind deshalb äquivalent: git diff master bug/pr-1 git diff master..bug/pr-1
Leider bedeutet die Syntax mit den zwei Punkten in git diff etwas völlig anderes als die in git log, die Sie in Kapitel 6 kennengelernt haben. Es lohnt sich, git diff und git log einmal miteinander zu vergleichen, weil auf diese Weise die Beziehungen der beiden Befehle zu den Änderungen, die an Repositories geschehen, hervorgehoben werden. Merken Sie sich diese Punkte, um das folgende Beispiel besser zu verstehen: • git diff kümmert sich nicht um den Verlauf und die Geschichte der Dateien, die es vergleicht, oder um irgendwelche Zweige. • git log achtet ganz genau darauf, wie eine Datei geändert wurde, um zu einer anderen Datei zu werden – etwa, wenn zwei Zweige voneinander abweichen –, und was in den einzelnen Zweigen geschehen ist. Die Befehle log und diff führen zwei grundlegend verschiedene Operationen aus. Während log auf einer Menge von Commits tätig wird, arbeitet diff auf zwei unterschiedlichen Endpunkten. Stellen Sie sich die folgende Abfolge von Ereignissen vor: 1. Jemand erzeugt einen neuen Zweig aus dem master-Zweig heraus, um den Bug pr-1 zu reparieren, und nennt ihn bug/pr-1. 2. Derselbe Entwickler fügt die Zeile »Fix Problem Report 1« in eine Datei im Zweig bug/pr-1 ein. 3. Währenddessen repariert ein anderer Entwickler Bug pr-3 im master-Zweig und fügt die Zeile »Fix Problem Report 3« in dieselbe Datei im master-Zweig ein. Kurz, in je eine Datei in jedem Zweig wurde eine Zeile eingefügt. Wenn Sie sich auf einer hohen Ebene die Änderungen an den Zweigen anschauen, können Sie sehen, wann der Zweig bug/pr-1 gestartet wurde und wann die einzelnen Änderungen erfolgt sind: $ git show-branch master bug/pr-1 * [master] Bugfix für pr-3 hinzugefügt ! [bug/pr-1] Fix Problem Report 1 -* [master] Bugfix für pr-3 hinzugefügt + [bug/pr-1] Fix Problem Report 1 *+ [master^] Bobs Fixes hinzugefügt
124 | Kapitel 8: Diffs
Wenn Sie git log -p master..bug/pr-1 eintippen, sehen Sie ein Commit, weil die Syntax master..bug/pr-1 alle Commits in bug/pr-1 repräsentiert, die nicht auch noch in master sind. Der Befehl geht zurück an die Stelle, an der bug/pr-1 von master abgezweigt ist, schaut sich aber nichts an, was seitdem im Zweig master passiert ist: $ git log -p master..bug/pr-1 commit 8f4cf5757a3a83b0b3dbecd26244593c5fc820ea Author: Jon Loeliger <[email protected]> Date: Wed May 14 17:53:54 2008 -0500 Fix Problem Report 1 diff --git a/fertig b/fertig index f3b6f0e..abbf9c5 100644 --- a/fertig +++ b/fertig @@ -1,3 +1,4 @@ stupid znill frot-less +Fix Problem Report 1
Im Gegensatz dazu zeigt git diff master..bug/pr-1 die Gesamtmenge der Unterschiede zwischen den beiden Bäumen, die durch die Köpfe der Zweige master und bug/pr-1 repräsentiert werden. Der Verlauf spielt keine Rolle, nur der aktuelle Zustand der Dateien ist wichtig: $ git diff master..bug/pr-1 diff --git a/NeuerKram b/NeuerKram index b4d8596..0f2416e 100644 --- a/NeuerKram +++ b/NeuerKram @@ -1,2 +1 @@ Irgendetwas -Fix Problem Report 3 diff --git a/fertig b/fertig index f3b6f0e..abbf9c5 100644 --- a/fertig +++ b/fertig @@ -1,3 +1,4 @@ stupid znill frot-less +Fix Problem Report 1
Die git diff-Ausgabe bedeutet also, dass Sie von der Datei im master-Zweig zu der Version im bug/pr-1-Zweig kommen können, indem Sie die Zeile »Fix Problem Report 3« aus der Datei entfernen und dann die Zeile »Fix Problem Report 1« hinzufügen. Wie Sie sehen, enthält dieses Diff Commits aus beiden Zweigen. Bei diesem kleinen Beispiel scheint das nicht so wichtig zu sein, betrachten Sie jedoch einmal das Beispiel in Abbildung 8-2 mit umfassenderen Entwicklungslinien in zwei Zweigen.
git diff und Commit-Bereiche | 125
A
B
C
D
E
F
G
V
W
X
Y
Z
H
master
maint
Abbildung 8-2: git diff bei längerem Verlauf
In diesem Fall stellt git log master..maint die fünf einzelnen Commits V, W, ..., Z dar. Auf der anderen Seite repräsentiert git diff master..maint die Unterschiede in den Bäumen bei H und Z, also eine Menge von elf Commits: C, D, ..., H und V, ..., Z. Sowohl git log als auch git diff akzeptieren die Form Commit1...Commit2, um eine symmetrische Differenz zu erzeugen. Allerdings liefern wie zuvor git log Commit1...Commit2 und git diff Commit1...Commit2 unterschiedliche Ergebnisse. Wie in »Commit-Bereiche« auf Seite 85 besprochen, zeigt der Befehl git log Commit1... Commit2 die Commits an, die von einem der beiden Commits, nicht jedoch von beiden aus erreichbar sind. Deshalb würde git log master...maint im vorigen Beispiel C, D, ..., H und V, ..., Z ergeben. Die Reihenfolge dieser Commits ist wichtig. git diff A B ist nicht identisch mit git diff B A.
Die symmetrische Differenz in git diff zeigt die Unterschiede zwischen Commit2 und einem Commit, das ein gemeinsamer Vorfahr (oder die Merge-Basis) von Commit1 und Commit2 ist. Bei der Genealogie aus Abbildung 8-2 kombiniert git diff master...maint die Änderungen in den Commits V, W, ..., Z.
git diff mit Pfadbegrenzung Der Befehl git diff operiert standardmäßig auf der gesamten Verzeichnisstruktur, die an einem bestimmten Baumobjekt hängt. Sie können jedoch mit derselben Pfadbegrenzungstechnik arbeiten, die auch git log einsetzt, um die Ausgabe von git diff auf eine Teilmenge des Repository zu beschränken. So zeigt z.B. git diff --stat an einer Stelle1 in der Entwicklung des Git-eigenen Repository Folgendes an: $ git diff --stat master~5 master Documentation/git-add.txt Documentation/git-cherry.txt 1
| |
d2b3691b61d516a0ad2bf700a2a5d9113ceff0b1
126 | Kapitel 8: Diffs
2 +6 +++++
Documentation/git-commit-tree.txt | 2 +Documentation/git-format-patch.txt | 2 +Documentation/git-gc.txt | 2 +Documentation/git-gui.txt | 4 +Documentation/git-ls-files.txt | 2 +Documentation/git-pack-objects.txt | 2 +Documentation/git-pack-redundant.txt | 2 +Documentation/git-prune-packed.txt | 2 +Documentation/git-prune.txt | 2 +Documentation/git-read-tree.txt | 2 +Documentation/git-remote.txt | 2 +Documentation/git-repack.txt | 2 +Documentation/git-rm.txt | 2 +Documentation/git-status.txt | 2 +Documentation/git-update-index.txt | 2 +Documentation/git-var.txt | 2 +Documentation/gitk.txt | 2 +builtin-checkout.c | 7 ++++builtin-fetch.c | 6 ++-git-bisect.sh | 29 ++++++++++++-------------t/t5518-fetch-exit-status.sh | 37 ++++++++++++++++++++++++++++++++++ 23 files changed, 83 insertions(+), 40 deletions(-)
Um die Ausgabe auf Änderungen in Documentation zu beschränken, könnten Sie stattdessen git diff --stat master~5 master Documentation eingeben: $ git diff --stat master~5 master Documentation Documentation/git-add.txt | 2 +Documentation/git-cherry.txt | 6 ++++++ Documentation/git-commit-tree.txt | 2 +Documentation/git-format-patch.txt | 2 +Documentation/git-gc.txt | 2 +Documentation/git-gui.txt | 4 ++-Documentation/git-ls-files.txt | 2 +Documentation/git-pack-objects.txt | 2 +Documentation/git-pack-redundant.txt | 2 +Documentation/git-prune-packed.txt | 2 +Documentation/git-prune.txt | 2 +Documentation/git-read-tree.txt | 2 +Documentation/git-remote.txt | 2 +Documentation/git-repack.txt | 2 +Documentation/git-rm.txt | 2 +Documentation/git-status.txt | 2 +Documentation/git-update-index.txt | 2 +Documentation/git-var.txt | 2 +Documentation/gitk.txt | 2 +19 files changed, 25 insertions(+), 19 deletions(-)
Natürlich können Sie die Diffs auch nach einer einzigen Datei filtern: $ git diff master~5 master Documentation/git-add.txt diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index bb4abe2..1afd0c6 100644 --- a/Documentation/git-add.txt
git diff mit Pfadbegrenzung | 127
+++ b/Documentation/git-add.txt @@ -246,7 +246,7 @@ characters that need C-quoting. `core.quotepath` configuration can be used to work this limitation around to some degree, but backslash, double-quote and control characters will still have problems. -See Also +SEE ALSO -------linkgit:git-status[1] linkgit:git-rm[1]
Im folgenden Beispiel, das ebenfalls aus dem Git-eigenen Repository stammt, durchsucht die Option -S"String" die letzten 50 Commits im master-Zweig nach Änderungen, die String enthalten: $ git diff -S"octopus" master~50 diff --git a/Documentation/RelNotes-1.5.5.3.txt b/Documentation/RelNotes-1.5.5.3.txt new file mode 100644 index 0000000..f22f98b --- /dev/null +++ b/Documentation/RelNotes-1.5.5.3.txt @@ -0,0 +1,12 @@ +GIT v1.5.5.3 Release Notes +========================== + +Fixes since v1.5.5.2 +-------------------+ + * "git send-email --compose" did not notice that non-ascii contents + needed some MIME magic. + + * "git fast-export" did not export octopus merges correctly. + +Also comes with various documentation updates.
Wird Git mit -S benutzt (die Option wird oft als Pickaxe oder Spitzhacke bezeichnet), listet es die Diffs auf, bei denen sich ändert, wie oft der angegebene String in dem Diff benutzt wird. Sie können sich das konzeptuell so vorstellen: »Wo wird der angegebene String entweder eingeführt oder entfernt?« Ein Beispiel für diese Option im Zusammenhang mit git log finden Sie in »Mit der Spitzhacke suchen« auf Seite 96.
Wie Subversion und Git Diffs ableiten – ein Vergleich Die meisten Systeme, wie CVS oder Subversion, überwachen eine Reihe von Revisionen und speichern einfach die Änderungen zwischen den einzelnen Dateipaaren. Diese Technik soll Speicherplatz und Aufwand sparen. Intern wenden solche Systeme eine Menge Zeit auf, um über Sachen nachzudenken, wie die Folge der Änderungen zwischen A und B. Wenn Sie z.B. Ihre Dateien aus dem zentralen Repository aktualisieren, erinnert sich Subversion daran, dass Sie beim letzten Aktua-
128 | Kapitel 8: Diffs
lisieren der Datei bei Revision r1095 waren. Nun ist das Repository aber bei Revision r1123. Das bedeutet, dass der Server Ihnen das Diff zwischen r1095 und r1123 schicken muss. Sobald Ihr Subversion-Client diese Diffs hat, kann er sie in Ihre Arbeitskopie einbauen und r1123 herstellen. (Auf diese Weise vermeidet es Subversion, Ihnen bei jeder Aktualisierung den Inhalt aller Dateien zu schicken.) Um Festplattenplatz zu sparen, speichert Subversion außerdem sein eigenes Repository als eine Folge von Diffs auf dem Server. Wenn Sie nach den Diffs zwischen r1095 und r1123 fragen, sucht es die einzelnen Diffs für jede Version zwischen diesen beiden Versionen, fügt sie zu einem großen Diff zusammen und schickt Ihnen das Ergebnis. Git dagegen funktioniert anders. Wie Sie gesehen haben, enthält jedes Commit in Git einen Baum, der alle Dateien auflistet, die von diesem Commit aufgenommen wurden. Die Bäume sind unabhängig voneinander. Git-Benutzer reden natürlich immer noch von Diffs und Patches, weil diese außerordentlich nützlich sind. In Git handelt es sich jedoch bei einem Diff und einem Patch um abgeleitete Daten, nicht um die grundlegenden Daten, die sie in CVS oder Subversion darstellen. Wenn Sie einen Blick in das .git-Verzeichnis werfen, werden Sie kein einziges Diff finden; schauen Sie dagegen in ein Subversion-Repository, dann merken Sie, dass es hauptsächlich aus Diffs besteht. So wie Subversion in der Lage ist, die komplette Menge der Unterschiede zwischen r1095 und r1123 abzuleiten, kann Git die Unterschiede zwischen zwei beliebigen Zuständen beschaffen und ableiten. Dabei muss Subversion allerdings jede Version zwischen r1095 und r1123 anschauen, Git hingegen kümmert sich nicht um die Zwischenschritte. Jede Revision besitzt ihren eigenen Baum, allerdings benötigt Git diese nicht, um das Diff zu generieren; Git kann direkt auf den Schnappschüssen des kompletten Zustands der jeweiligen Versionen operieren. Dieser einfache Unterschied in den Speichersystemen ist einer der wichtigsten Gründe dafür, dass Git so viel schneller ist als andere Revisionskontrollsysteme.
Wie Subversion und Git Diffs ableiten – ein Vergleich | 129
Kapitel 9
KAPITEL 9
Merges
Git ist ein verteiltes Versionskontrollsystem (distributed version control system, DVCS). Es erlaubt einem Entwickler, der etwa in Japan sitzt, und einem anderen Entwickler in New Jersey, Änderungen unabhängig voneinander vorzunehmen und aufzuzeichnen. Diese beiden Entwickler können ihre Änderungen jederzeit kombinieren – ganz ohne ein zentrales Repository. In diesem Kapitel erfahren Sie, wie man zwei oder mehr unterschiedliche Entwicklungslinien miteinander kombiniert. Ein Merge vereinigt zwei oder mehr Zweige mit Commit-Verläufen. Am häufigsten werden bei einem Merge nur zwei Zweige verbunden, obwohl Git auch Merges von drei, vier oder noch mehr Zweigen gleichzeitig unterstützt. In Git muss ein Merge innerhalb eines Repository auftreten – das heißt, alle Zweige, die zusammengeführt werden sollen, müssen im selben Repository vorliegen. Wie die Zweige in das Repository gelangen, ist nicht wichtig. (Wie Sie in Kapitel 11 sehen werden, bietet Git Mechanismen, mit denen auf andere Repositories verwiesen werden kann und mit denen man entfernte Zweige in das aktuelle Arbeits-Repository holt.) Wenn die Modifikationen in einem Zweig nicht im Konflikt mit den Modifikationen stehen, die in einem anderen Zweig gefunden werden, berechnet Git ein Merge-Ergebnis und erzeugt ein neues Commit, das den neuen, vereinten Zustand repräsentiert. Sollten die Zweige jedoch einander widersprechen, was vorkommt, wenn Änderungen versuchen, dieselbe Zeile in derselben Datei zu modifizieren, löst Git den Konflikt nicht auf, sondern markiert diese umstrittenen Änderungen als »unmerged« (nicht zusammengeführt) im Index und überlässt den Ausgleich Ihnen, dem Entwickler. Kann Git den Merge nicht automatisch ausführen, haben ebenfalls Sie das letzte Wort, indem Sie das abschließende Commit ausführen, nachdem alle Konflikte aufgelöst wurden.
| 131
Merge-Beispiele Um anderer_Zweig mit Zweig zusammenzuführen, müssen Sie den Zielzweig auschecken und die anderen Zweige mit ihm verbinden: $ git checkout Zweig $ git merge anderer_Zweig
Wir wollen zwei Beispiel-Merges durcharbeiten, einen ohne Konflikt und einen mit wesentlichen Überschneidungen. Um die Beispiele in diesem Kapitel zu vereinfachen, wollen wir mehrere Zweige verwenden, die wir mithilfe der Techniken aus Kapitel 7 erzeugen.
Für einen Merge vorbereiten Bevor Sie einen Merge beginnen, sollten Sie am besten erst einmal Ihr Arbeitsverzeichnis aufräumen. Während eines normalen Merge erzeugt Git neue Versionen von Dateien und legt diese in Ihrem Arbeitsverzeichnis ab, wenn es fertig ist. Darüber hinaus benutzt Git den Index, um temporäre und Zwischenversionen von Dateien während der Operation zu speichern. Falls Sie modifizierte Dateien in Ihrem Arbeitsverzeichnis haben oder den Index über git add oder git rm geändert haben, besitzt Ihr Repository ein unsauberes Arbeitsverzeichnis bzw. einen unsauberen Index. Wenn Sie einen Merge in einem unsauberen Zustand starten, ist Git möglicherweise nicht in der Lage, die Änderungen aus all den Zweigen und aus Ihrem Arbeitsverzeichnis oder Index in einem Durchgang zu kombinieren. Sie müssen nicht mit einem sauberen Verzeichnis beginnen. Git führt den Merge z.B. aus, wenn die Dateien, die von der Merge-Operation betroffen sind, und die unsauberen Dateien in Ihrem Arbeitsverzeichnis disjunkt sind. Ganz allgemein jedoch erleichtern Sie sich Ihr Leben mit Git ganz ungemein, wenn Sie alle Merges mit sauberem Arbeitsverzeichnis und Index starten.
Zwei Zweige zusammenführen Für das einfachste Szenario wollen wir ein Repository mit einer einzigen Datei einrichten, zwei Zweige anlegen und dann die beiden Zweige wieder zusammenführen: $ git init Initialized empty Git repository in /tmp/conflict/.git/ $ git config user.email "[email protected]" $ git config user.name "Jon Loeliger" $ cat Zeile Zeile Zeile
> 1 2 3
datei Kram Kram Kram
132 | Kapitel 9: Merges
^D $ git add datei $ git commit -m"Anfängliche dreizeilige Datei" Created initial commit 8f4d2d5: Anfängliche dreizeilige Datei 1 files changed, 3 insertions(+), 0 deletions(-) create mode 100644 datei
Erzeugen wir ein weiteres Commit auf dem master-Zweig: $ cat > andere_datei Hier ist Kram in einer anderen Datei! ^D $ git add andere_datei $ git commit -m"Eine andere Datei" Created commit 761d917: Eine andere Datei 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 andere_datei
Bisher enthält das Repository einen Zweig mit zwei Commits, wobei jedes Commit eine neue Datei eingeführt hat. Nun wollen wir in einen anderen Zweig wechseln und die erste Datei verändern: $ git checkout -b alternativ master^ Switched to a new branch "alternativ" $ git show-branch * [alternativ] Anfängliche dreizeilige Datei ! [master] Eine andere Datei -+ [master] Eine andere Datei *+ [alternativ] Anfängliche dreizeilige Datei
Der alternativ-Zweig wurde hier zunächst vom master^-Commit abgezweigt, und zwar ein Commit hinter dem aktuellen Kopf. Nehmen Sie an der Datei eine einfache Änderung vor, damit Sie etwas zum Zusammenführen haben, und bestätigen Sie sie dann mit einem Commit. Denken Sie daran: Am besten ist es, wenn Sie ausstehende Änderungen bestätigen und dann den Merge mit einem sauberen Arbeitsverzeichnis starten. $ cat >> datei Zeile 4 alternativer Kram ^D $ git commit -a -m"Zeile 4 in alternativ hinzugefügt" Created commit b384721: Zeile 4 in alternativ hinzugefügt 1 files changed, 1 insertions(+), 0 deletions(-)
Jetzt gibt es zwei Zweige mit unterschiedlichen Entwicklungsarbeiten. Zum master-Zweig wurde eine zweite Datei hinzugefügt, am alternativ-Zweig wurde eine Modifikation vorgenommen. Da die beiden Änderungen nicht die gleichen Teile einer gemeinsamen Datei betreffen, sollte ein Merge ganz geschmeidig und ohne Probleme vonstatten gehen. Die git merge-Operation ist kontextsensitiv. Ihr aktueller Zweig ist immer der Zielzweig, der oder die anderen Zweige werden in den aktuellen Zweig überführt. In diesem Fall
Merge-Beispiele
| 133
sollte der alternate-Zweig in den master-Zweig eingebracht werden, sodass der letztgenannte Zweig ausgecheckt werden muss, bevor Sie weitermachen können: $ git checkout master Switched to branch "master" $ git status # On branch master nothing to commit (working directory clean) # Jo, bereit für einen Merge! $ git merge alternativ Merge made by recursive. datei | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
Mit einem speziellen Werkzeug, das Teil von git log ist, können Sie sich den CommitGraphen anschauen: $ git log --graph --pretty=oneline --abbrev-commit * 1d51b93... Merge branch 'alternativ' |\ | * b384721... Zeile 4 in alternativ hinzugefügt * | 761d917... Eine andere Datei |/ * 8f4d2d5... Anfänglich dreizeilige Datei
Das ist genau der gleiche Commit-Graph wie in »Commit-Graphen« auf Seite 81, allerdings auf die Seite gedreht mit den neuesten Commits oben und nicht rechts. Die beiden Zweige haben sich beim ersten Commit, 8f4d2d5, geteilt; jeder Zweig zeigt je ein Commit (761d917 und b384721). Bei Commit 1d51b93 werden die beiden Zweige wieder zusammengeführt. git log --graph stellt eine ausgezeichnete Alternative zu grafischen Werkzeugen wie gitk dar. Die Visualisierung, die von git log --graph geboten
wird, ist sogar für dumme Terminals geeignet.
Technisch gesehen, führt Git die einzelnen Merges symmetrisch aus, um ein identisches, kombiniertes Commit herzustellen, das zu Ihrem aktuellen Zweig hinzugefügt wird. Der andere Zweig wird von dem Merge nicht beeinflusst. Da das Merge-Commit nur zum aktuellen Zweig hinzugefügt wird, können Sie sagen: »Ich habe einen anderen Zweig in diesen eingebracht.«
Ein Merge mit einem Konflikt Die Merge-Operation ist von Natur aus problematisch, weil sie notwendigerweise potenziell wechselnde und einander widersprechende Änderungen aus unterschiedlichen Ent-
134 | Kapitel 9: Merges
wicklungslinien zusammenbringt. Die Änderungen in einem Zweig könnten den Änderungen aus einem anderen Zweig ähneln oder sich auch radikal unterscheiden. Modifikationen könnten dieselben Dateien oder eine disjunkte Menge von Dateien verändern. Git kann mit diesen verschiedenen Möglichkeiten zwar umgehen, benötigt aber oft Ihre Führung, um Konflikte aufzulösen. Wir wollen ein Szenario durchgehen, in dem ein Merge zu einem Konflikt führt. Wir beginnen mit den Ergebnissen des Merge aus dem vorigen Abschnitt und führen unabhängige und widersprüchliche Änderungen auf den Zweigen master und alternate ein. Anschließend fügen wir den alternate-Zweig in den master-Zweig ein, schauen uns den Konflikt an, lösen ihn auf und bestätigen das Ergebnis mit einem Commit. Erzeugen Sie auf dem master-Zweig eine neue Version von datei mit einigen zusätzlichen Zeilen und bestätigen Sie sie: $ git checkout master $ cat >> datei Zeile 5 Kram Zeile 6 Kram ^D $ git commit -a -m"Zeilen 5 und 6 hinzufügen" Created commit 4d8b599: Zeilen 5 und 6 hinzufügen 1 files changed, 2 insertions(+), 0 deletions(-)
Verändern Sie dieselbe Datei nun auf dem alternate-Zweig auf andere Weise. Während Sie auf dem master-Zweig neue Commits durchgeführt haben, ist der alternate-Zweig noch nicht weitergekommen: $ git checkout alternativ Switched branch "alternativ" $ git show-branch * [alternativ] Zeile 4 in alternativ hinzugefügt ! [master] Zeilen 5 und 6 hinzufügen -+ [master] Zeilen 5 und 6 hinzufügen *+ [alternativ] Zeile 4 in alternativ hinzugefügt # In diesem Zweig verbleibt "datei" mit "Zeile 4 alternativer Kram" $ cat >> datei Zeile 5 alternativer Kram Zeile 6 alternativer Kram ^D $ cat Zeile Zeile Zeile
Datei 1 Kram 2 Kram 3 Kram
Merge-Beispiele
| 135
Zeile 4 alternativer Kram Zeile 5 alternativer Kram Zeile 6 alternativer Kram $ git diff diff --git a/datei b/datei index a29c52b..802acf8 100644 --- a/datei +++ b/datei @@ -2,3 +2,5 @@ Zeile 1 Kram Zeile 2 Kram Zeile 3 Kram Zeile 4 alternativer Kram +Zeile 5 alternativer Kram +Zeile 6 alternativer Kram $ git commit -a -m"alternative Zeilen 5 und 6 hinzugefügt" Created commit e306e1d: alternative Zeilen 5 und 6 hinzugefügt 1 files changed, 2 insertions(+), 0 deletions(-)
Überprüfen wir das Szenario. Der Verlauf des aktuellen Zweigs sieht so aus: $ git show-branch * [alternativ] alternative Zeilen 5 und 6 hinzugefügt ! [master] Zeilen 5 und 6 hinzufügen -* [alternativ] alternative Zeilen 5 und 6 hinzugefügt + [master] Zeilen 5 und 6 hinzufügen *+ [alternativ^] Zeile 4 in alternativ hinzugefügt
Checken Sie nun den master-Zweig aus und versuchen Sie, den Merge durchzuführen: $ git checkout master Switched to branch "master" $ git merge alternativ Auto-merged datei CONFLICT (content): Merge conflict in datei Automatic merge failed; fix conflicts and then commit the result.
Wenn ein solcher Merge-Konflikt auftritt, sollten Sie immer das Ausmaß des Konflikts mit dem Befehl git diff untersuchen. Hier trägt die Datei datei einen Konflikt in ihrem Inhalt: $ git diff diff --cc datei index 4d77dd1,802acf8..0000000 --- a/datei +++ b/datei @@@ -2,5 -2,5 +2,10 @@@ Zeile 1 Kram Zeile 2 Kram Zeile 3 Kram Zeile 4 alternativer Kram
136 | Kapitel 9: Merges
++<<<<<<< +Zeile 5 +Zeile 6 ++======= + Zeile 5 + Zeile 6 ++>>>>>>>
HEAD:datei Kram Kram alternativer Kram alternativer Kram alternativ:datei
Der Befehl git diff zeigt die Unterschiede zwischen der Datei in Ihrem Arbeitsverzeichnis und dem Index. Im traditionellen Ausgabestil des diff-Befehls wird der geänderte Inhalt zwischen <<<<<<< und ======= präsentiert, mit der Alternativ-Version zwischen ======= und >>>>>>>. Allerdings werden im kombinierten Diff-Format zusätzliche Plus- und Minuszeichen benutzt, um Änderungen aus mehreren Quellen relativ zur endgültigen resultierenden Version anzuzeigen. Die vorherige Ausgabe zeigt, dass der Konflikt sich über die Zeilen 5 und 6 erstreckt, wo wir in den beiden Zweigen absichtlich unterschiedliche Änderungen vorgenommen haben. Nun müssen Sie den Konflikt lösen. Bearbeiten Sie erst einmal die Datei so, dass sie diesen Inhalt widerspiegelt: $ cat Zeile Zeile Zeile Zeile Zeile Zeile
datei 1 Kram 2 Kram 3 Kram 4 alternativer Kram 5 Kram 6 alternativer Kram
Falls Sie mit der Konfliktlösung zufrieden sind, sollten Sie die Datei mit git add in den Index einfügen und für das Merge-Commit bereitstellen: $ git add datei
Nachdem Sie die Konflikte aufgelöst und die fertigen Versionen der einzelnen Dateien mit git add im Index bereitgestellt haben, wird es schließlich Zeit, den Merge mit dem Befehl git commit zu bestätigen. Git bringt Sie zu Ihrem Lieblingseditor, wo Sie ein solches Template vorfinden: Merge branch 'alternativ' Conflicts: datei # # It looks like you may be committing a MERGE. # If this is not correct, please remove the file # .git/MERGE_HEAD # and try again. # # Please enter the commit message for your changes. # (Comment lines starting with '#' will not be included) # On branch master
Merge-Beispiele
| 137
# Changes to be committed: # (use "git reset HEAD ..." to unstage) # # modified: datei #
Wie üblich handelt es sich bei den Zeilen, die mit der Raute (#) beginnen, um Kommentare, die nur zu Ihrer Information gedacht sind, wenn Sie eine Nachricht schreiben. Alle Kommentarzeilen werden dann in der fertigen Commit-Logmeldung ignoriert. Sie können die Commit-Meldung ganz nach Belieben verändern oder erweitern, indem Sie z.B. einen Hinweis dazu hinzufügen, wie Sie den Konflikt aufgelöst haben. Wenn Sie den Editor verlassen, müsste Git die erfolgreiche Erzeugung eines neuen Merge-Commit anzeigen: $ git commit # Bearbeiten der Merge-Commit-Nachricht Created commit 7015896: Merge branch 'alternativ' $ git show-branch ! [alternativ] alternative Zeilen 5 und 6 hinzugefügt * [master] Merge von Zweig 'alternativ' -- [master] Merge von Zweig 'alternativ' +* [alternativ] alternative Zeilen 5 und 6 hinzugefügt
Mit diesem Befehl sehen Sie das resultierende Merge-Commit: $ git log
Mit Merge-Konflikten arbeiten Wie am vorangegangenen Beispiel zu sehen war, gibt es Situationen, in denen widersprüchliche Änderungen nicht automatisch zusammengeführt werden können. Wir wollen ein weiteres Szenario mit einem Merge-Konflikt schaffen, um eine Gelegenheit zu erhalten, die Werkzeuge zu untersuchen, die Git anbietet, um Missverhältnisse aufzulösen. Beginnend mit dem typischen hallo, das nur den Inhalt »hallo« enthält, erzeugen wir zwei unterschiedliche Zweige mit zwei verschiedenen Varianten der Datei: $ git init Initialized empty Git repository in /tmp/conflict/.git/ $ echo hallo > hallo $ git add hallo $ git commit -m"Erste hallo-Datei" Created initial commit b8725ac: Erste hallo-Datei 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hallo
138 | Kapitel 9: Merges
$ git checkout -b alt Switched to a new branch "alt" $ echo Welt >> hallo $ echo 'Yay!' >> hallo $ git commit -a -m"Eine Welt" Created commit d03e77f: Eine Welt 1 files changed, 2 insertions(+), 0 deletions(-) $ git checkout master $ echo Welten >> hallo $ echo 'Yay!' >> hallo $ git commit -a -m"Alle Welten" Created commit eddcb7d: Alle Welten 1 files changed, 2 insertions(+), 0 deletions(-)
In einem Zweig heißt es Welt, während es in dem anderen Welten heißt – ein bewusster Unterschied. Wenn Sie nun master auschecken und versuchen, den alt-Zweig in diesen Zweig einzubringen, tritt ein Konflikt auf: $ git merge alt Auto-merged hallo CONFLICT (content): Merge conflict in hallo Automatic merge failed; fix conflicts and then commit the result.
Wie erwartet, warnt Git Sie vor dem Konflikt, den es in der Datei hallo gefunden hat.
Widersprüchliche Dateien finden Was tun Sie jedoch, wenn Gits hilfreiche Anweisungen schon vom Bildschirm verschwunden sind oder es zu viele Dateien mit Konflikten gab? Zum Glück behält Git die problematischen Dateien im Auge, indem es sie jeweils im Index als conflicted (konfliktbehaftet) oder unmerged (nicht zusammengeführt) kennzeichnet. Mit den Befehlen git status oder git ls-files -u lassen sich die Dateien anzeigen, die konfliktbehaftet und damit nicht zusammengeführt im Arbeitsbaum herumliegen: $ git status hallo: needs merge # On branch master # Changed but not updated: # (use "git add ..." to update what will be committed) # # unmerged: hallo # no changes added to commit (use "git add" and/or "git commit -a") $ git ls-files -u
Mit Merge-Konflikten arbeiten
| 139
100644 ce013625030ba8dba906f756967f9e9ca394464a 1 100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2 100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3
hallo hallo hallo
Natürlich können Sie auch git diff benutzen, allerdings zeigt dieser Befehl außerdem alle lästigen Details!
Konflikte untersuchen Wenn ein Konflikt eintritt, werden Arbeitsverzeichniskopien aller konfliktbehafteten Dateien um dreifache Diff- oder Merge-Markierungen erweitert. Ausgehend von unserem Beispiel, würde die resultierende konfliktbehaftete Datei jetzt so aussehen: $ cat hallo hallo <<<<<<< HEAD:hallo Welten ======= Welt >>>>>>> 6ab5ed10d942878015e38e4bab333daff614b46e:hallo Yay!
Die Merge-Markierungen trennen die beiden möglichen Versionen des konfliktbehafteten Teils der Datei. In der ersten Version steht dort »Welten«, in der anderen Version »Welt«. Sie könnten sich einfach für eine der Phrasen entscheiden, die Konfliktmarkierungen entfernen und dann git add und git commit ausführen. Wir wollen jedoch jetzt einige der anderen Funktionen ausprobieren, die Git anbietet, um Konflikte zu lösen. Die dreifachen Merge-Markierungslinien (<<<<<<<<, ======== und >>>>>>>>) werden automatisch generiert und sind eigentlich nur für Sie gedacht, nicht um von einem Programm gelesen zu werden. Sie sollten sie mit einem Texteditor löschen, wenn Sie den Konflikt gelöst haben.
git diff mit Konflikten Git enthält eine besondere, Merge-spezifische Variante von git diff, mit der sich gleichzeitig die Änderungen anzeigen lassen, die an beiden Eltern vorgenommen wurden. Im Beispiel sieht das so aus: $ git diff diff --cc hallo index e63164d,562080a..0000000 --- a/hallo +++ b/hallo @@@ -1,3 -1,3 +1,7 @@@ hallo ++<<<<<<< HEAD:hallo +Welten ++=======
140 | Kapitel 9: Merges
+ Welt ++>>>>>>> alt:hallo Yay!
Was heißt das alles? Es ist die einfache Kombination von zwei Diffs: eines im Vergleich mit dem ersten Elternelement, genannt HEAD, und eines im Vergleich mit dem zweiten Elternelement oder alt. (Seien Sie nicht überrascht, wenn das zweite Elternelement ein absoluter SHA1-Name ist, der irgendein unbenanntes Commit aus einem anderen Repository repräsentiert!) Der Einfachheit halber gibt Git dem zweiten Elternelement den Spezialnamen MERGE_HEAD. Sie können sowohl die HEAD- als auch die MERGE_HEAD-Version mit der Version aus dem Arbeitsverzeichnis vergleichen: $ git diff HEAD diff --git a/hallo b/hallo index e63164d..4e4bc4e 100644 --- a/hallo +++ b/hallo @@ -1,3 +1,7 @@ hallo +<<<<<<< HEAD:hallo Welten +======= +Welt +>>>>>>> alt:hallo Yay!
Und dann das hier: $ git diff MERGE_HEAD diff --git a/hallo b/hallo index 562080a..4e4bc4e 100644 --- a/hallo +++ b/hallo @@ -1,3 +1,7 @@ hallo +<<<<<<< HEAD:hallo +Welten +======= Welt +>>>>>>> alt:hallo Yay!
In neueren Versionen von Git ist git diff --ours ein Synonym für git diff HEAD, weil es die Unterschiede zwischen unserer (»our«) Version und der zusammengeführten Version zeigt. Genauso kann git diff MERGE_HEAD als git diff --theirs geschrieben werden. Mit git diff --base können Sie die kombinierte Menge der Änderungen seit der Merge-Basis sehen, was ansonsten sehr umständlich so geschrieben werden müsste: git diff $(git merge-basis HEAD MERGE_HEAD)
Mit Merge-Konflikten arbeiten
| 141
Wenn Sie die beiden Diffs vergleichen, sehen Sie, dass der Text bis auf die +-Spalten identisch ist, sodass Git den Haupttext nur einmal ausgibt und die +-Spalten nebeneinanderstellt. Bei dem von git diff gefundenen Konflikt sind jeder Ausgabezeile zwei Spalten mit Informationen vorangestellt. Ein Pluszeichen in einer Spalte kennzeichnet eine hinzugefügte Zeile, ein Minuszeichen steht entsprechend für eine entfernte Zeile, und eine Leerstelle bedeutet, dass keine Änderung in dieser Zeile vorliegt. Die erste Spalte gibt an, was sich im Vergleich zu Ihrer Version geändert hat, und die zweite Spalte zeigt, was sich im Vergleich zur anderen Version geändert hat. Die Konfliktmarkierungszeilen sind in beiden Versionen neu und erhalten deshalb ein ++. Die Zeilen Welt und Welten sind jeweils nur in einer der Versionen neu und tragen deshalb nur in der entsprechenden Spalte ein einzelnes +. Wenn Sie in die Datei eine dritte Möglichkeit einbauen $ cat hallo hallo Weltliche Yay!
sieht die neue git diff-Ausgabe so aus: $ git diff diff --cc hallo index e63164d,562080a..0000000 --- a/hallo +++ b/hallo @@@ -1,3 -1,3 +1,3 @@@ hallo - Welten -Welt ++Weltliche Yay!
Alternativ könnten Sie die eine oder andere Originalversion wählen: $ cat hallo hallo Welt Yay!
Die git diff-Ausgabe wäre dann $ git diff diff --cc hallo index e63164d,562080a..0000000 --- a/hallo +++ b/hallo
Warten Sie! Hier ist gerade etwas Seltsames passiert. Wo ist die Diff-Zeile über Welt, die anzeigt, dass es zur zweiten Version hinzugefügt wurde, und Welten, die zeigt, das es aus
142 | Kapitel 9: Merges
der ersten Version entfernt wurde? Git hat sie absichtlich weggelassen, weil es glaubt, dass dieser Abschnitt Sie wahrscheinlich nicht mehr interessiert. Wenn Sie git diff auf einer widersprüchlichen Datei einsetzen, zeigt es Ihnen nur die Abschnitte, die tatsächlich einen Konflikt haben. In einer großen Datei mit zahlreichen Änderungen sind die meisten Änderungen konfliktfrei abgelaufen; entweder hat die eine Seite des Merge einen bestimmten Abschnitt geändert oder die andere Seite. Wenn Sie versuchen, einen Konflikt aufzulösen, dann sind Ihnen diese Abschnitte meist egal, sodass git diff uninteressante Abschnitte mit einer einfachen Heuristik wegschneidet: Wenn ein Abschnitt sich nur zu einer Seite hin ändert, wird er nicht gezeigt. Diese Optimierung hat eine etwas verwirrende Nebenwirkung: Nachdem Sie etwas aufgelöst haben, das ein Konflikt war, indem Sie sich einfach für die eine oder andere Seite entschieden haben, wird es nicht mehr angezeigt. Sie haben schließlich den Abschnitt so modifiziert, dass er sich nur auf der einen oder der anderen Seite ändert (nämlich auf der Seite, die Sie nicht ausgewählt haben), sodass er für Git wie ein Abschnitt aussieht, der niemals konfliktbehaftet war. Das ist wirklich mehr eine Nebenwirkung der Implementierung als eine beabsichtigte Eigenschaft, aber vielleicht hilft sie Ihnen ja trotzdem: git diff zeigt nur die Abschnitte der Datei an, die immer noch widersprüchlich sind, sodass Sie die Konflikte vor sich haben, die noch nicht behoben sind.
git log mit Konflikten Während Sie dabei sind, einen Konflikt aufzulösen, können Sie mithilfe einiger spezieller git log-Optionen exakt feststellen, woher die Änderungen kamen und wieso sie aufgetreten sind. Probieren Sie das hier: $ git log --merge --left-right -p commit <eddcb7dfe63258ae4695eb38d2bc22e726791227 Author: Jon Loeliger <[email protected]> Date: Wed Oct 22 21:29:08 2008 -0500 Alle Welten diff --git a/hallo b/hallo index ce01362..e63164d 100644 --- a/hallo +++ b/hallo @@ -1 +1,3 @@ hallo +Welten +Yay! commit >d03e77f7183cde5659bbaeef4cb51281a9ecfc79 Author: Jon Loeliger <[email protected]> Date: Wed Oct 22 21:27:38 2008 -0500
Mit Merge-Konflikten arbeiten
| 143
Eine Welt diff --git a/hallo b/hallo index ce01362..562080a 100644 --- a/hallo +++ b/hallo @@ -1 +1,3 @@ hallo +Welt +Yay!
Dieser Befehl zeigt alle Commits in beiden Teilen des Verlaufs, die widersprüchliche Dateien in Ihrem Merge beeinflussen, sowie die eigentlichen Änderungen, die von den einzelnen Commits vorgenommen wurden. Falls Sie sich gefragt haben, wann, wieso, wie und von wem die Zeile Welten in die Datei gekommen ist, dann finden Sie hier genau die passende Stelle. Folgende Optionen gibt es für git log: • --merge zeigt nur solche Commits, die mit widersprüchlichen Dateien zu tun haben. • --left-right zeigt <, wenn das Commit von der »linken« Seite des Merge stammt (»unserer« Version, derjenigen, von der Sie gestartet sind), oder >, falls das Commit von der »rechten« Seite des Merge kommt (»ihrer« Version, derjenigen, in die Sie Ihre Version einbringen). • -p zeigt die Commit-Nachrichten und die Patches, die mit den einzelnen Commits verknüpft sind. Wäre Ihr Repository komplizierter und hätten mehrere Dateien Konflikte, dann könnten Sie als Kommandozeilenoption den oder die Dateinamen angeben, an denen Sie interessiert sind: $ git log --merge --left-right -p hallo
Die Beispiele, die wir hier geliefert haben, sind absichtlich klein gehalten. Reale Situationen sind natürlich in den meisten Fällen deutlich umfangreicher und komplexer. Um großen Merges mit hässlichen, ausufernden Konflikten aus dem Weg zu gehen, verwenden Sie mehrere kleine Commits mit wohldefinierten Auswirkungen, die jeweils einzelnen Konzepten gewidmet sind. Git kommt mit kleinen Commits gut zurecht, es gibt also keinen Grund, bis zum letzten Augenblick zu warten, um dann riesige, weit verteilte Änderungen mit einem Commit zu bestätigen. Kleinere Commits und häufigere MergeZyklen verringern den Aufwand beim Auflösen von Konflikten.
Wie Git Konflikte überwacht Wie genau behält Git die ganzen Informationen über einen konfliktbehafteten Merge im Blick? Es gibt verschiedene Teile:
144 | Kapitel 9: Merges
• .git/MERGE_HEAD enthält den SHA1-Hash des Commit, in den Sie sich integrieren. Sie müssen den SHA1 nicht selbst benutzen, denn Git weiß, wie es in diese Datei schauen kann, wenn Sie von MERGE_HEAD reden. • .git/MERGE_MSG enthält die vorgegebene Merge-Nachricht, die verwendet wird, wenn Sie nach dem Auflösen der Konflikte git commit ausführen. • Der Git-Index enthält je drei Kopien aller konfliktbehafteten Kopien: die MergeBasis, »unsere« Version und »ihre« Version. Diesen drei Kopien werden die StageNummern 1, 2 bzw. 3 zugewiesen. • Die konfliktbehaftete Version (mit Merge-Markern usw.) wird nicht im Index gespeichert. Stattdessen wird sie in einer Datei in Ihrem Arbeitsverzeichnis abgelegt. Wenn Sie git diff ohne Parameter ausführen, findet der Vergleich immer zwischen dem statt, was im Index steht, und dem, was sich in Ihrem Arbeitsverzeichnis befindet. Um festzustellen, wie die Indexeinträge gespeichert werden, können Sie den PlumbingBefehl git ls-files einsetzen: $ git ls-files -s 100644 ce013625030ba8dba906f756967f9e9ca394464a 1 100644 e63164d9518b1e6caf28f455ac86c8246f78ab70 2 100644 562080a4c6518e1bf67a9f58a32a67bff72d4f00 3
hallo hallo hallo
Die Option -s für git ls-files zeigt alle Dateien mit allen Stages. Falls Sie nur die widersprüchlichen Dateien sehen wollen, benutzen Sie stattdessen die Option -u. Mit anderen Worten: Die Datei hallo wird dreimal gespeichert, und jede Version besitzt einen anderen Hash, entsprechend den drei unterschiedlichen Versionen. Mit git catfile können Sie sich eine bestimmte Variante anschauen: $ git cat-file -p e63164d951 hallo Welten Yay!
Es gibt sogar eine besondere Syntax für git diff, mit deren Hilfe sich unterschiedliche Versionen der Datei vergleichen lassen. Falls Sie z.B. feststellen wollen, was sich zwischen der Merge-Basis und der Version, in die Sie den Merge ausführen, geändert hat, benutzen Sie $ git diff :1:hallo :3:hallo diff --git a/:1:hallo b/:3:hallo index ce01362..562080a 100644 --- a/:1:hallo +++ b/:3:hallo @@ -1 +1,3 @@ hallo +Welt +Yay!
Mit Merge-Konflikten arbeiten
| 145
Seit der Git-Version 1.6.1 akzeptiert der Befehl git checkout die Option --ours oder --theirs als Kürzel für das einfache Auschecken einer Datei von der einen oder anderen Seite eines widersprüchlichen Merge; durch Ihre Wahl wird der Konflikt aufgelöst. Diese beiden Optionen können nur während der Auflösung eines Konflikts benutzt werden.
Die Verwendung der Stage-Nummern zum Benennen einer Version unterscheidet sich von git diff --theirs, was die Unterschiede zwischen »ihrer« Version und der resultierenden zusammengeführten (oder weiterhin widersprüchlichen) Version in Ihrem Arbeitsverzeichnis zeigt. Die zusammengeführte Version ist gar nicht im Index, besitzt also noch nicht einmal eine Nummer. Da Sie die Arbeitskopieversion zugunsten »ihrer« Version vollständig bearbeitet und aufgelöst haben, sollte es nun keinen Unterschied mehr geben: $ cat hallo hallo Welt Yay! $ git diff --theirs * Unmerged path hallo
Alles, was bleibt, ist der Hinweis »unmerged path«, der daran erinnert, sie in den Index aufzunehmen.
Eine Konfliktauflösung abschließen Wir wollen eine letzte Änderung an der Datei hallo vornehmen, bevor wie sie für zusammengeführt erklären: $ cat hallo hallo allerseits Yay!
Nachdem die Datei nun vollständig zusammengeführt und aufgelöst ist, verkleinert git add den Index wieder auf nur eine einzige Kopie der Datei hallo: $ git add hallo $ git ls-files -s 100644 ebc56522386c504db37db907882c9dbd0d05a0f0 0
hallo
Die einsame 0 zwischen dem SHA1-Hash und dem Pfadnamen sagt uns, dass die StageNummer für eine nicht widersprüchliche Datei null ist. Sie müssen alle widersprüchlichen Dateien durcharbeiten, die im Index aufgezeichnet sind. Solange es noch einen nicht aufgelösten Konflikt gibt, können Sie kein Commit ausführen. Wenn Sie daher die Konflikte in einer Datei behoben haben, führen Sie git add (oder git rm, git update-index usw.) auf der Datei aus, um ihren Konfliktstatus zu klären.
146 | Kapitel 9: Merges
Achten Sie darauf, dass Sie nicht versehentlich Dateien mit git add hinzufügen, in denen sich noch Konfliktmarker befinden. Das beseitigt zwar den Konflikt im Index und erlaubt Ihnen, die Datei mit Commit zu bestätigen, allerdings ist die Datei dann nicht korrekt.
Schließlich können Sie das Ergebnis mit git commit bestätigen und das Merge-Commit mit git show anzeigen lassen: $ cat .git/MERGE_MSG Merge branch 'alt' Conflicts: hallo $ git commit $ git show commit a274b3003fc705ad22445308bdfb172ff583f8ad Merge: eddcb7d... d03e77f... Author: Jon Loeliger <@example.com> Date: Wed Oct 22 23:04:18 2008 -0500 Merge branch 'alt' Conflicts: hallo diff --cc hallo index e63164d,562080a..ebc5652 --- a/hallo +++ b/hallo @@@ -1,3 -1,3 +1,3 @@@ hallo - Welten -Welt ++allerseits Yay!
Wenn Sie sich ein Merge-Commit anschauen, sollten Sie drei interessante Dinge bemerken: • Es gibt im Header eine neue, zweite Zeile, die Merge: lautet. Normalerweise ist es nicht nötig, das Eltern-Commit eines Commit in git log oder git show zu zeigen, da es nur ein Eltern-Commit gibt und es sich dabei typischerweise um dasjenige handelt, das direkt nach dem Commit im Log kommt. Merge-Commits haben jedoch typischerweise zwei (und manchmal mehr) Eltern-Commits, und diese Eltern sind wichtig, um den Merge zu verstehen. Daher geben git log und git show immer den SHA1 der einzelnen Vorfahren aus.
Mit Merge-Konflikten arbeiten
| 147
• Die automatisch generierte Commit-Lognachricht gibt dankenswerterweise die Liste der konfliktbehafteten Dateien an. Das kann sich später als nützlich erweisen, falls sich herausstellt, dass Ihr Merge Probleme verursacht hat. Normalerweise werden Probleme, die bei einem Merge auftreten, von den Dateien verursacht, die von Hand zusammengeführt werden mussten. • Das Diff eines Merge-Commit ist kein normales Diff. Es liegt immer im Format kombiniertes Diff oder »konfliktbehafteter Merge« vor. Ein Merge gilt in Git als erfolgreich, wenn überhaupt keine Änderung vorlag; er ist ganz einfach eine Kombination anderer Änderungen, die im Verlauf bereits aufgetreten sind. Beim Anzeigen des Inhalts eines Merge-Commit werden daher nur die Teile angezeigt, die sich von einem der zusammengeführten Zweige unterscheiden, und nicht die gesamte Gruppe der Änderungen.
Einen Merge abbrechen oder neustarten Falls Sie eine Merge-Operation starten, dann aber aus irgendeinem Grund beschließen, sie nicht abzuschließen, bietet Git Ihnen die Möglichkeit, die Operation abzubrechen. Geben Sie vor dem Ausführen des letzten git commit beim Merge-Commit Folgendes ein: $ git reset --hard HEAD
Dieser Befehl bringt sowohl Ihr Arbeitsverzeichnis als auch den Index in den Zustand zurück, der vor dem git merge-Befehl bestand. Wenn Sie den Merge abbrechen oder verwerfen wollen, nachdem er abgeschlossen wurde (das heißt, nachdem ein neues Merge-Commit eingeführt wurde), nehmen Sie diesen Befehl: $ git reset --hard ORIG_HEAD
Vor dem Beginn der Merge-Operation speichert Git genau zu diesem Zweck den HEAD Ihres Originalzweigs im Ref ORIG_HEAD. Seien Sie dennoch vorsichtig: Falls Sie den Merge nicht mit einem sauberen Arbeitsverzeichnis und Index gestartet haben, könnten Sie Probleme bekommen und alle nicht bestätigten Änderungen verlieren, die in Ihrem Verzeichnis vorhanden sind. Sie können eine git merge-Anforderung mit einem unsauberen Arbeitsverzeichnis initiieren, allerdings wird der unsaubere Zustand vor dem Merge bei einem git reset --hard nicht vollständig wiederhergestellt. Stattdessen verliert der Reset Ihren unsauberen Zustand im Arbeitsverzeichnisbereich. Sie haben mit anderen Worten einen --hard-Reset in den HEAD-Zustand angefordert! Seit Git-Version 1.6.1 haben Sie eine andere Möglichkeit: Wenn Sie eine Konfliktauflösung vermasselt haben und in den ursprünglichen Konfliktzustand vor dem Auflösungsversuch zurückkehren wollen, bevor Sie es erneut probieren, benutzen Sie den Befehl git checkout -m.
148 | Kapitel 9: Merges
Merge-Strategien Bisher waren unsere Beispiele noch ganz leicht zu bewältigen, weil es nur zwei Zweige gab. Vielleicht kommt es Ihnen so vor, als wäre die ganze Komplexität von Git mit den Verläufen in Form von gerichteten azyklischen Graphen und den langen, schwer zu merkenden Commit-IDs völlig übertrieben. Und das ist sie vielleicht auch – für einen so einfachen Fall. Schauen wir uns nun einmal etwas Komplizierteres an. Stellen Sie sich vor, dass nicht nur eine Person in Ihrem Repository arbeitet, sondern dass es drei sind. Der Einfachheit halber wollen wir annehmen, dass jeder Entwickler – Alice, Bob und Cal – in der Lage ist, Änderungen als Commits in drei getrennten, gleichnamigen Zweigen innerhalb eines gemeinsam genutzten Repository einzubringen. Da die Entwickler in getrennten Zweigen arbeiten, soll eine Person, Alice, dafür verantwortlich sein, die Integration der verschiedenen Beiträge zu verwalten. In der Zwischenzeit darf jeder der Entwickler die Entwicklung der anderen für seine Zwecke einsetzen, indem er sie direkt einbezieht oder bei Bedarf einen Merge mit dem Zweig eines der Kollegen durchführt. Die Programmierer entwickeln schließlich ein Repository mit dem in Abbildung 9-1 gezeigten Commit-Verlauf.
A
B
Alice
I
J P
Bob
Q
Cal
Abbildung 9-1: Eine Criss-cross-Merge-Situation
Stellen Sie sich vor, dass Cal das Projekt begonnen hat und Alice später dazugekommen ist. Nachdem Alice eine Weile daran gearbeitet hatte, kam Bob dazu. In der Zwischenzeit arbeitete Cal an seiner eigenen Version weiter. Schließlich hat Alice die Änderungen von Bob mit einem Merge bei sich aufgenommen, Bob wiederum arbeitete weiter, ohne die Änderungen von Alice wieder zurück in seinen Baum zu überführen. Es gibt also nun drei unterschiedliche Zweigverläufe, wie in Abbildung 9-2 zu sehen ist. Nehmen wir nun einmal an, Bob möchte die neuesten Änderungen von Cal bekommen. Das Diagramm sieht zwar recht kompliziert aus, dieser Teil ist aber noch relativ einfach. Durchlaufen Sie den Baum von Bob aus durch Alice hindurch, bis Sie den Punkt erreichen, an dem sie das erste Mal von Cal abgezweigt ist. Das ist A, die Merge-Basis zwischen Bob und Cal. Um seine Änderungen mit denen von Cal zusammenzuführen, muss
Merge-Strategien
| 149
A
B
C I
D
J P
Alice Bob
Q
Cal
Abbildung 9-2: Hier hat Alice die Änderungen von Bob in ihren Zweig überführt.
Bob alle Änderungen zwischen der Merge-Basis A und Cals neuestem Commit Q nehmen und in seinem eigenen Baum zusammenführen, wodurch sich K ergibt. Man erhält den Verlauf aus Abbildung 9-3.
A
B
C I
J P
D K
Q
Alice Bob Cal
Abbildung 9-3: Nach dem Zusammenführen der Änderungen von Cal in Bobs Zweig
Die Merge-Basis zwischen zwei oder mehr Zweigen können Sie immer mit dem Befehl git merge-base finden. Es ist möglich, dass es für eine Menge von Zweigen mehr als eine gleichermaßen gültige Merge-Basis gibt.
So weit, so gut. Alice beschließt nun, dass sie ebenfalls Cals neueste Änderungen haben möchte, merkt aber nicht, dass Bob Cals Baum bereits mit seinem zusammengeführt hat. So überführt sie also Cals Baum in ihren. Dabei handelt es sich um eine weitere einfache Operation, da es offensichtlich ist, wo sie von Cal abgezweigt ist. Der resultierende Verlauf wird in Abbildung 9-4 gezeigt. Nun merkt Alice, dass Bob ebenfalls nicht untätig war (L), und möchte mit ihm fusionieren. Wo liegt dieses Mal die Merge-Basis (zwischen L und E)? Leider ist die Antwort ungewiss. Wenn Sie den Baum nach oben hin durchlaufen, könnten Sie zu der Meinung gelangen, dass die ursprüngliche Revision von Cal eine gute Wahl ist. Das ist aber eigentlich nicht sinnvoll: Sowohl Alice als auch Bob haben inzwischen Cals neueste Revision. Wenn Sie nach den Unterschieden zwischen Cals Originalrevision und Bobs neuester Revision fragen, dann werden Sie merken, dass diese auch Cals neuere
150 | Kapitel 9: Merges
A
B
C I
J P
E
D K
L
Alice Bob
Q
Cal
Abbildung 9-4: Nachdem Alice Cals Änderungen in ihren Baum aufgenommen hat
Änderungen enthält, die Alice bereits hat, wodurch es also wahrscheinlich zu einem Merge-Konflikt kommen würde. Was wäre, wenn Sie Cals neueste Revision als Basis verwendeten? Das wäre besser, aber immer noch nicht optimal: Wenn Sie das Diff zwischen Cals neuester und Bobs neuester ermitteln, erhalten Sie alle Änderungen von Bob. Alice hat aber schon einige Änderungen von Bob, sodass es auch hier zu einem Merge-Konflikt kommen könnte. Und was ist, wenn Sie die Version benutzen, die Alice zuletzt aus der Version von Bob, J, zusammengestellt hat? Ein Diff zwischen dieser und Bobs neuester Version enthält nur die neuesten Änderungen von Bob, was Sie ja auch wollen. Allerdings sind auch die Änderungen von Cal enthalten, die Alice bereits besitzt! Was tun? Diese Art von Situation wird als Criss-cross-Merge (Kreuz-und-quer-Merge) bezeichnet, weil die Änderungen rückwärts und vorwärts zwischen den Zweigen zusammengeführt wurden. Würden die Änderungen nur in einer Richtung verlaufen (z.B. von Cal zu Alice zu Bob, aber niemals von Bob zu Alice oder von Alice zu Cal), dann wäre das Zusammenführen einfach. Dummerweise ist das Leben nicht immer so leicht. Die Git-Entwickler schrieben ursprünglich einen einfachen Mechanismus zum Vereinen von zwei Zweigen mit einem Merge-Commit, allerdings merkten sie aufgrund solcher Szenarien wie des gerade beschriebenen, dass ein schlauerer Ansatz erforderlich war. Daher verallgemeinerten und parametrisierten die Entwickler und führten alternative, konfigurierbare Merge-Strategien für den Umgang mit den verschiedenen Szenarien ein. Schauen wir uns die verschiedenen Strategien und ihre Anwendung an.
Degenerierte Merges Es gibt zwei gebräuchliche Degeneriert-Szenarien, die zu Merges führen. Diese werden als Already up-to-date und Fast-forward bezeichnet. Da keines dieser Szenarien tatsächlich ein neues Merge-Commit einführt, nachdem der Befehl git merge1 ausgeführt wurde, betrachten manche Leute sie nicht als echte Merge-Strategien:
Merge-Strategien
| 151
Already up-to-date Wenn in Ihrem Zielzweig bereits alle Commits aus dem anderen Zweig (seinem HEAD) vorhanden sind, auch wenn er von selbst dorthin gekommen ist, dann sagt man, dass der Zielzweig »already up-to-date« (bereits auf dem neuesten Stand) ist. Daraus folgt, dass keine neuen Commits zu Ihrem Zweig hinzugefügt werden. Falls Sie z.B. einen Merge ausführen und gleich im Anschluss exakt dieselbe MergeAnforderung folgen lassen, wird Ihnen mitgeteilt, dass Ihr Zweig schon auf dem neuesten Stand ist: # Zeigen, dass alternativ bereits mit master zusammengeführt wurde $ git show-branch ! [alternativ] alternative Zeilen 5 und 6 hinzufügen * [master] Merge von Zweig 'alternativ' -- [master] Merge von Zweig 'alternativ' +* [alternativ] alternative Zeilen 5 und 6 hinzufügen # Versuch, alternativ erneut mit master zusammenzuführen $ git merge alternativ Already up-to-date.
Fast-forward Ein Fast-forward-»Merge« tritt ein, wenn Ihr Zweig-HEAD bereits vollständig vorhanden und im anderen Zweig repräsentiert ist. Es ist die Umkehrung des »Already upto-date«-Falls. Da Ihr HEAD bereits in dem anderen Zweig vorhanden ist (wahrscheinlich aufgrund eines gemeinsamen Vorfahren), »klebt« Git an Ihren HEAD einfach die neuen Commits aus dem anderen Zweig an. Anschließend verschiebt Git Ihren Zweig-HEAD so, dass er auf den letzten, neuen Commit verweist. Natürlich werden auch der Index und Ihr Arbeitsverzeichnis entsprechend angepasst, um den neuen, endgültigen Commit-Zustand wiederzugeben. Der Fast-forward-Fall ist besonders bei Tracking-Zweigen sehr verbreitet, weil diese einfach die Remote-Commits von anderen Repositories sammeln und aufzeichnen. Die HEADs Ihrer lokalen Tracking-Zweige sind immer vollständig vorhanden, da dies die Stelle ist, wo sich der Zweig-HEAD nach der vorangegangenen Abrufoperation befand. Näheres erfahren Sie in Kapitel 11. Es ist wichtig, dass Git diese Fälle verarbeitet, aber keine tatsächlichen Commits einführt. Stellen Sie sich vor, was im Fast-forward-Fall passieren würde, wenn Git ein Commit erzeugen würde. Das Zusammenführen von A in B würde zuerst Abbildung 9-5
1
Ja, man kann Git zwingen, dennoch eines zu erzeugen, indem man im Fast-forward-Fall die Option --no-ff benutzt. Allerdings sollte man sich vollständig klar darüber sein, wieso man das will.
152 | Kapitel 9: Merges
erzeugen. Das Zusammenführen von B in A würde dann Abbildung 9-6 zur Folge haben, und ein weiteres Zusammenführen von A in B würde dann Abbildung 9-7 ergeben.
B
A Abbildung 9-5: Erster nicht konvergierender Merge
B
A Abbildung 9-6: Zweiter nicht konvergierender Merge
B
A Abbildung 9-7: Dritter nicht konvergierender Merge
Jeder neue Merge bedeutet ein neues Commit, sodass die Folge niemals zu einem stabilen Zustand zusammenfindet und zeigt, dass die beiden Zweige identisch sind.
Normale Merges Diese Merge-Strategien erzeugen alle am Ende ein Commit, das zu Ihrem aktuellen Zweig hinzugefügt wird und den kombinierten Merge-Zustand repräsentiert: Resolve Die Resolve-Strategie operiert nur auf zwei Zweigen, wobei der gemeinsame Vorfahr als Merge-Basis ermittelt und ein direkter Dreifach-Merge durchgeführt wird, indem die Änderungen von der Merge-Basis auf die Spitze des anderen Zweig-HEAD im aktuellen Zweig angewandt werden. Diese Methode ist intuitiv sinnvoll. Rekursiv Die rekursive Strategie ähnelt der Resolve-Strategie, da sie ebenfalls nur zwei Zweige auf einmal vereinen kann. Sie soll allerdings mit dem Szenario zurechtkommen, bei
Merge-Strategien
| 153
dem es mehr als eine Merge-Basis zwischen den beiden Zweigen gibt. In diesen Fällen führt Git einen temporären Merge aller gemeinsamen Merge-Basen aus und verwendet dann diesen als Basis, von der der resultierende Merge der beiden angegebenen Zweige abgeleitet wird, und zwar mithilfe eines normalen DreifachMerge-Algorithmus. Die temporäre Merge-Basis wird verworfen und der endgültige Merge-Status auf Ihrem Zielzweig bestätigt. Octopus Die Octopus-Strategie ist speziell dafür geschaffen worden, zwei oder mehr Zweige gleichzeitig zusammenzuführen. Konzeptuell ist sie ziemlich einfach: Intern wird mehrmals die rekursive Merge-Strategie aufgerufen, und zwar einmal für jeden Zweig, den Sie im Merge zusammenführen. Allerdings kommt diese Strategie mit einem Merge nicht zurecht, der irgendeine Form von Konfliktauflösung erfordert, die eine Benutzerinteraktion notwendig macht. In einem solchen Fall sind Sie gezwungen, eine Reihe von normalen Merges durchzuführen und die Konflikte nacheinander aufzulösen.
Rekursive Merges Ein einfaches Beispiel für einen Criss-cross-Merge wird in Abbildung 9-8 gezeigt.
b
B
a
A
Abbildung 9-8: Einfacher Criss-cross-Merge
Die Knoten a und b sind beide Merge-Basen für einen Merge zwischen A und B. Beide könnten jeweils als Merge-Basis verwendet werden und würden vernünftige Ergebnisse bringen. In diesem Fall würde die rekursive Strategie a und b zu einer temporären MergeBasis zusammenführen, die dann als Merge-Basis für A und B dient. Da a und b das gleiche Problem haben könnten, könnte ihr Zusammenführen einen weiteren Merge von noch älteren Commits erfordern. Deswegen wird dieser Algorithmus als »rekursiv« bezeichnet.
154 | Kapitel 9: Merges
Octopus-Merges Die Hauptgründe dafür, dass Git das gleichzeitige Zusammenführen mehrerer Zweige unterstützt, sind Allgemeingültigkeit und Eleganz des Designs. In Git kann ein Commit entweder keine Eltern (das erste Commit), ein Eltern-Commit (ein normales Commit) oder mehr als ein Eltern-Commit (ein Merge-Commit) haben. Sobald Sie »mehr als ein Eltern-Commit« haben, gibt es keinen speziellen Grund, dass man diese Zahl auf zwei beschränken sollte. Deshalb unterstützen Git-Datenstrukturen mehrere Eltern-Commits.2 Die Octopus-Merge-Strategie ist eine natürliche Folge dieser allgemeinen Designentscheidung, eine flexible Liste mit Commit-Eltern zuzulassen. Octopus-Merges sehen in Diagrammen ganz hübsch aus, weshalb Git-Benutzer dazu neigen, sie so oft wie möglich einzusetzen. Sie können sich den Endorphinrausch förmlich vorstellen, der einen Entwickler überkommt, wenn er sechs Zweige eines Programms zu einem zusammenführt. Abgesehen davon, dass sie nett aussehen, machen Octopus-Merges eigentlich nichts weiter. Sie könnten genausogut mehrere Merge-Commits vornehmen, also einen pro Zweig, und genau das gleiche Ziel erreichen.
Besondere Merges Es gibt zwei spezielle Merge-Strategien, die Sie kennen sollten, weil sie Ihnen manchmal dabei helfen können, seltsame Probleme zu lösen. Überspringen Sie diesen Abschnitt einfach, wenn Sie kein seltsames Problem haben. Die beiden besonderen Strategien sind Ours (unsere) und Subtree (Teilbaum). Diese Merge-Strategien erzeugen am Ende ein Commit, das zu Ihrem aktuellen Zweig hinzugefügt wird und den kombinierten Merge-Zustand repräsentiert: Ours Die Ours-Strategie führt eine beliebige Zahl anderer Zweige zusammen, verwirft aber eigentlich die Änderungen aus diesen Zweigen und verwendet nur die Dateien aus dem aktuellen Zweig. Das Ergebnis eines Ours-Merge ist identisch mit dem aktuellen HEAD, allerdings werden alle anderen genannten Zweige ebenfalls als Commit-Eltern aufgezeichnet. Das ist sinnvoll, wenn Sie wissen, dass Sie schon alle Änderungen aus den anderen Zweigen haben, aber trotzdem die beiden Verläufe kombinieren wollen. Das heißt: Die Strategie erlaubt Ihnen festzuhalten, dass Sie irgendwie den Merge ausgeführt haben, möglicherweise direkt von Hand, und dass künftige Git-Operationen nicht versuchen sollten, die Verläufe noch einmal zusammenzuführen. Git kann das als echten Merge behandeln, ganz unabhängig davon, wie es dazu gekommen ist.
2
Hier ist das Prinzip »Null, Eins oder Unendlich« am Werk.
Merge-Strategien
| 155
Subtree Die Subtree-Strategie integriert einen anderen Zweig, allerdings wird alles in diesem Zweig in einem speziellen Teilbaum des aktuellen Baums zusammengeführt. Sie geben den Teilbaum nicht vor, sondern Git ermittelt ihn automatisch.
Merge-Strategien anwenden Woher kennt Git nun also die passende Strategie, bzw. wie ermittelt es sie? Oder wie legen Sie eine andere Strategie fest, falls Ihnen die Wahl von Git nicht gefällt? Git versucht, die verwendeten Algorithmen so einfach und kostengünstig wie möglich zu halten. Deshalb probiert es zuerst already up-to-date und fast-forward aus, um die trivialen, einfachen Szenarien nach Möglichkeit zu eliminieren. Falls Sie mehr als einen anderen Zweig für das Zusammenführen in Ihrem aktuellen Zweig angeben, hat Git keine andere Wahl, als die octopus-Strategie auszuprobieren, da nur sie in der Lage ist, mehr als zwei Zweige in einem einzigen Merge zu vereinen. Wenn diese Spezialfälle nicht funktionieren, muss Git eine Standardstrategie einsetzen, die zuverlässig in allen anderen Szenarien funktioniert. Ursprünglich war resolve die Standard-Merge-Strategie von Git. In Criss-cross-Merge-Situationen, in denen es mehr als eine mögliche Merge-Basis gibt, funktioniert die Resolve-Strategie folgendermaßen: Wählen Sie eine der möglichen MergeBasen (entweder den letzten Merge aus Bobs Zweig oder den letzten aus Cals Zweig) und hoffen Sie das Beste. Das ist in Wirklichkeit nicht so schlimm, wie es klingt. Oft stellt sich heraus, dass Alice, Bob und Cal an unterschiedlichen Teilen des Codes gearbeitet haben. In diesem Fall entdeckt Git, dass es einige Änderungen erneut zusammenführt, die bereits vorhanden sind, und überspringt doppelt auftretende Änderungen, um Konflikte zu vermeiden. Und falls es leichte Änderungen gibt, die einen Konflikt verursachen, dann sollten diese Konflikte wenigstens leicht von einem Entwickler zu lösen sein. Da resolve nicht mehr das Git-Standardverfahren darstellt, müsste Alice es explizit anfordern, wenn sie es benutzen wollte: $ git merge -s resolve Bob
Im Jahre 2005 hat Fredrik Kuivinen die neue rekursive Merge-Strategie eingeführt, die seitdem den Standard bildet. Sie ist allgemeiner als resolve und erzeugt weniger Konflikte auf dem Linux-Kernel. Sie kommt auch ganz gut mit Merges mit Umbenennungen zurecht. Im gezeigten Beispiel, in dem Alice die gesamte Arbeit von Bob zusammenführen möchte, würde die rekursive Strategie so funktionieren: 1. Sie beginnt mit der neuesten Revision von Cal, die sowohl Alice als auch Bob haben. In diesem Fall ist das die neueste Revision von Cal, Q, die sowohl mit Bobs als auch mit Alices Zweig zusammengeführt wurde.
156 | Kapitel 9: Merges
2. Berechnen des Diff zwischen dieser Revision und der neuesten Revision, die Alice von Bob übernommen hat, und Einsetzen dieses Diff. 3. Berechnen des Diff zwischen dieser kombinierten Version und Bobs neuester Version und Einsetzen dieses Diff. Diese Methode wird als rekursiv bezeichnet, weil es je nach der Anzahl der Überkreuzungen und Merge-Basen, die Git entdeckt, viele zusätzliche Iterationen gibt. Und sie funktioniert. Die rekursive Methode ist nicht nur intuitiv sinnvoll, sie erbringt erwiesermaßen auch weniger Konflikte in echten Situationen als die einfachere Resolve-Strategie. Deshalb ist sie nun die Standardstrategie für git merge. Natürlich sieht der endgültige Verlauf immer gleich aus, unabhängig von der gewählten Strategie (siehe Abbildung 9-9).
A
B
C I
D
J
K P
Q
E
F L
Alice Bob Cal
Abbildung 9-9: Fertiger Criss-cross-Merge-Verlauf
Merge-Treiber Jede der in diesem Kapitel beschriebenen Merge-Strategien verwendet einen zugrunde liegenden Merge-Treiber, um die einzelnen Dateien aufzulösen und zusammenzuführen. Ein Merge-Treiber akzeptiert die Namen von drei temporären Dateien, die den gemeinsamen Vorfahren, die Zielzweigversion und die andere Zweigversion einer Datei repräsentieren. Der Treiber modifiziert die Zielzweigversion, um das »zusammengeführte« Ergebnis zu haben. Der Text-Merge-Treiber lässt die üblichen dreifachen Merge-Markierungen (<<<<<<<<, ======== und >>>>>>>) an Ort und Stelle. Der binäre Merge-Treiber bewahrt einfach die Zielzweigversion der Datei und lässt die Datei im Index, gekennzeichnet als Konflikt. Im Prinzip zwingt Sie das, Binärdateien von Hand zu verarbeiten. Der letzte eingebaute Merge-Treiber, union, lässt einfach alle Zeilen aus beiden Versionen in der zusammengeführten Datei. Durch Gits Attributmechanismus kann Git bestimmte Dateien oder Dateimuster an spezielle Merge-Treiber binden. Die meisten Textdateien werden vom text-Treiber verarbeitet, die meisten Binärdateien vom binary-Treiber. Für spezielle Anforderungen, die eine
Merge-Strategien
| 157
Ours und Subtree benutzen Man kann diese beiden Merge-Strategien zusammen benutzen. Z.B. wurde einst das Programm gitweb (das nun Teil von git ist) außerhalb des Haupt-git.git-Repository entwickelt. Bei Revision 0a8f4f wurde sein gesamter Verlauf unter dem gitweb-Teilbaum nach git.git überführt. Falls Sie etwas Ähnliches vorhaben, können Sie folgendermaßen vorgehen: 1. Kopieren Sie die aktuellen Dateien aus dem gitweb.git-Projekt in das Unterverzeichnis gitweb Ihres Projekts. 2. Bestätigen Sie sie wie üblich mit einem Commit. 3. Holen Sie mit der ours-Strategie aus dem gitweb.git-Projekt: $ git pull -s ours gitweb.git master
Sie benutzen hier ours, weil Sie wissen, dass Sie bereits die neueste Version der Dateien haben und diese sich schon exakt dort befinden, wo Sie sie haben wollen (das ist nicht die Stelle, an der die normale rekursive Strategie sie abgelegt haben würde). 4. Künftig können Sie die neuesten Änderungen mit der subtree-Strategie vom gitweb. git-Projekt holen: $ git pull -s subtree gitweb.git master
Da die Dateien bereits in Ihrem Repository existieren, weiß Git automatisch, in welchen Teilbaum es sie legen soll, und führt die Aktualisierungen ohne Konflikte durch.
anwendungsspezifische Merge-Operation verlangen, können Sie Ihre eigenen MergeTreiber herstellen und an diese speziellen Dateien binden. Wenn Sie der Meinung sind, dass Sie eigene Merge-Treiber benötigen, sollten Sie auch einmal über eigene Diff-Treiber nachdenken!
Wie Git über Merges denkt Zunächst einmal scheint Gits automatische Unterstützung für Merges wirklich Zauberei zu sein, vor allem im Vergleich zu den komplizierteren und fehleranfälligen Schritten, die in anderen Versionskontrollsystemen für Merges erforderlich sind. Wir wollen uns anschauen, was hinter den Kulissen vor sich geht, damit das alles möglich ist.
158 | Kapitel 9: Merges
Merges und Gits Objektmodell In den meisten Versionskontrollsystemen besitzt jedes Commit nur ein Eltern-Commit. Wenn Sie auf einem solchen System irgendein_Zweig in mein_Zweig überführen, erzeugen Sie ein neues Commit in mein_Zweig mit den Änderungen aus irgendein_Zweig. Falls Sie umgekehrt mein_Zweig in irgendein_Zweig überführen, erzeugt das ein neues Commit auf irgendein_Zweig, das die Änderungen aus mein_Zweig enthält. Das Überführen von Zweig A in Zweig B und das Überführen von Zweig B in Zweig A sind zwei unterschiedliche Operationen. Allerdings bemerkten die Git-Designer, dass jede dieser beiden Operationen am Ende die gleiche Menge an Dateien ergibt. Das bedeutet, dass man beide Operationen einfach so ausdrücken konnte: »Führe alle Änderungen aus irgendein_Zweig und ein_anderer_Zweig zu einem einzigen Zweig zusammen.« In Git ergibt ein Merge ein neues Baumobjekt mit den zusammengeführten Dateien, führt aber gleichzeitig ein neues Commit-Objekt ein, und zwar nur auf dem Zielzweig. Nach den Befehlen $ git checkout mein_Zweig $ git merge irgendein_Zweig
sieht das Objektmodell so aus wie in Abbildung 9-10.
CA
CB
CC
TA
TB
TC
CX
CY
CZ
TX
TY
TZ
some_branch
CZC
my_branch
TZC
Abbildung 9-10: Objektmodell nach einem Merge
In Abbildung 9-10 ist jedes Cx ein Commit-Objekt, und jedes Tx repräsentiert das dazugehörige Baumobjekt. Sie sehen, dass es ein gemeinsames Merge-Commit (CZC) gibt, das sowohl CC als auch CZ als Commit-Eltern, aber nur eine resultierende Menge von Dateien besitzt, die im TZC-Baum repräsentiert werden. Das zusammengeführte Baumobjekt repräsentiert symmetrisch beide Quellzweige gleichermaßen. Da aber mein_Zweig der
Wie Git über Merges denkt | 159
ausgecheckte Zweig war, in dem der Merge durchgeführt wurde, wurde nur mein_Zweig aktualisiert und zeigt nun das neue Commit; irgendein_Zweig bleibt, wo er war. Das ist nicht nur eine Frage der Semantik. Es spiegelt die Git zugrunde liegende Philosophie wider, dass alle Zweige3 gleich erzeugt sind.
Squash-Merges Stellen Sie sich vor, dass irgendein_Zweig nicht nur ein Commit, sondern stattdessen fünf oder zehn oder sogar Hunderte von Commits enthält. In den meisten Systemen würde das Zusammenführen von irgendein_Zweig in mein_Zweig das Erzeugen eines einzelnen Diff, das Anwenden dieses Diff als Patch auf mein_Zweig und das Erzeugen eines neuen Elements im Verlauf umfassen. Man bezeichnet das als Squash-Commit, weil alle einzelnen Commits in einer großen Änderung »zusammengequetscht« werden. Soweit es den Verlauf von mein_Zweig betrifft, wäre der Verlauf von irgendein_Zweig verloren. In Git werden die beiden Zweige gleich behandelt, sodass es unangebracht wäre, die eine oder die andere Seite zusammenzuquetschen. Stattdessen wird auf beiden Seiten der gesamte Verlauf der Commits bewahrt. Anhand von Abbildung 9-10 können Sie erkennen, dass Sie als Benutzer für diese Komplexität bezahlen. Hätte Git ein Squash-Commit durchgeführt, müssten Sie kein Diagramm sehen (oder darüber nachdenken), das sich teilt und dann wieder vereint. Der Verlauf von mein_Zweig wäre einfach eine gerade Linie. Wenn es gewünscht wird, kann Git ebenfalls Squash-Commits durchführen. Geben Sie einfach die Option --squash bei git merge oder git pull an. Seien Sie jedoch vorsichtig! Das Zusammenquetschen von Commits bringt den Verlauf von Git durcheinander und verkompliziert künftige Merges, weil die zusammengequetschten Commits den ursprünglichen CommitVerlauf verändern (siehe Kapitel 10).
Die zusätzliche Komplexität mag ungünstig sein, lohnt sich jedoch. Diese Eigenschaft bedeutet z.B., dass die Befehle git blame und git bisect, die in Kapitel 6 besprochen wurden, viel leistungsstärker sind als ihre Äquivalente in anderen Systemen. Und wie Sie an der rekursiven Merge-Strategie gesehen haben, ist Git in der Lage, aufgrund dieser zusätzlichen Komplexität und des daraus resultierenden detaillierten Verlaufs sehr komplizierte Merges zu automatisieren. Auch wenn die Merge-Operation selbst beide Eltern-Commits als gleich behandelt, könnten Sie das erste Eltern-Commit besonders behandeln lassen, wenn Sie später im Verlauf zurückgehen. Manche Befehle, wie git log und gitk, unterstützen die Option --first-parent, die nur dem ersten ElternCommit eines jeden Merge folgt. Der daraus resultierende Verlauf sieht fast so aus, als hätten Sie bei all Ihren Merges --squash benutzt. 3
Und entsprechend alle vollständigen Repository-Klone.
160 | Kapitel 9: Merges
Wieso nicht einfach alle Änderungen nacheinander zusammenführen? Sie werden sich fragen, ob es nicht möglich wäre, beides zu haben: einen einfachen, linearen Verlauf, in dem jedes einzelne Commit repräsentiert wird? Git könnte einfach alle Commits aus irgendein_Zweig nehmen und sie nacheinander auf mein_Zweig anwenden. Das wäre aber überhaupt nicht das Gleiche. Eine wichtige Beobachtung in Bezug auf Gits Commit-Verläufe besagt, dass jede Revision im Verlauf real ist. (Sie erfahren in Kapitel 12 mehr über das Behandeln von alternativen Verläufen als gleiche Realitäten.) Wenn Sie eine Abfolge der Patches von jemandem auf Ihre Version anwenden, erzeugen Sie mit der Vereinigung Ihrer und seiner Änderungen eine Reihe völlig neuer Versionen. Vermutlich werden Sie wie üblich die fertige Version testen. Aber was ist mit all diesen neuen Zwischenversionen? In der Realität haben diese Versionen niemals existiert: Niemand hat diese Commits wirklich erzeugt, sodass niemand sicher sagen kann, ob sie jemals funktioniert haben. Git bewahrt einen detaillierten Verlauf auf, sodass Sie später noch einmal anschauen können, wie Ihre Dateien in einem bestimmten Augenblick in der Vergangenheit ausgesehen haben. Wenn einige Ihrer zusammengeführten Commits Dateiversionen widerspiegeln, die niemals wirklich existiert haben, dann haben Sie eigentlich den Grund für das Nachhalten einer ausführlichen History verloren! Deshalb funktionieren Git-Merges auch nicht so. Würden Sie fragen, »Wie war es fünf Minuten, bevor ich den Merge ausgeführt habe?«, dann wäre die Antwort ungewiss. Sie müssen speziell nach mein_Zweig oder nach irgendein_Zweig fragen, da beide vor fünf Minuten unterschiedlich waren und Git für jeden Zweig einzeln Auskunft geben kann. Obwohl Sie vermutlich fast immer das normale Verhalten für Verläufe haben wollen, kann Git auch eine Folge von Patches anwenden (siehe Kapitel 13). Dieser Prozess wird als Rebasing bezeichnet und ist eines der Themen von Kapitel 10. Die Folgen der Änderung von Commit-Verläufen werden in »Den öffentlichen Verlauf ändern« auf Seite 237 besprochen.
Wie Git über Merges denkt | 161
Kapitel 10
KAPITEL 10
Commits verändern
Ein Commit zeichnet den Verlauf Ihrer Arbeit auf und erhält Ihre Änderungen wie etwas Unantastbares. Das Commit selbst ist jedoch nicht in Stein gemeißelt. Git bietet verschiedene Werkzeuge und Befehle, die speziell dafür gedacht sind, den Commit-Verlauf, der in Ihrem Repository katalogisiert ist, zu modifizieren und zu verbessern. Es gibt viele gute Gründe dafür, dass man ein Commit oder die gesamte CommitSequenz verändern oder überarbeiten könnte: • Man kann ein Problem beheben, bevor es sich weitervererbt. • Man kann eine große, umfangreiche Änderung in eine Reihe kleiner thematischer Commits zerlegen. Umgekehrt kann man einzelne Commits zu einem größeren kombinieren. • Man kann Bewertungen und Vorschläge einbauen. • Man kann die Reihenfolge der Commits ändern, um sie an die Anforderungen bei der Kompilierung anzupassen. • Man kann Commits logischer anordnen. • Man kann Debug-Code entfernen, der versehentlich mit einem Commit bestätigt wurde. Wie Sie in Kapitel 11 sehen werden, wo erläutert wird, wie man ein Repository für die gemeinsame Nutzung bereitstellt, gibt es noch viel mehr Gründe, wieso Sie die Commits vor dem Veröffentlichen Ihres Repository ändern könnten. Im Allgemeinen sollten Sie nicht zögern, ein Commit oder eine Folge von Commits zu verändern, wenn diese damit sauberer und verständlicher werden. Natürlich muss man wie in der gesamten Softwareentwicklung abwägen zwischen wiederholter (bis hin zu übertriebener) Verfeinerung und der Akzeptanz von etwas, das bereits zufriedenstellend ist. Streben Sie nach sauberen, wohlstrukturierten Patches, die sowohl für Sie als auch für Ihre Mitarbeiter eine exakte Bedeutung haben. Irgendwann jedoch ist gut genug auch wirklich gut genug.
| 163
Das Ändern der Geschichte – die zugrunde liegende Philosophie Für die Manipulation des Verlaufs bzw. der History einer Entwicklung gibt es verschiedene Denkansätze. Eine Philosophie könnte man als »realistischen Verlauf« bezeichnen: Jedes Commit wird beibehalten und nichts wird verändert. Eine Variante ist ein »feinkörniger realistischer Verlauf«, bei dem Sie jede Änderung so bald wie möglich mit einem Commit bestätigen und auf diese Weise sicherstellen, dass jeder kleine Schritt für die Nachwelt erhalten bleibt. Eine andere Möglichkeit ist der »didaktische realistische Verlauf«, bei dem Sie sich Zeit lassen und Ihre beste Arbeit nur in bequemen und passenden Momenten bestätigen. Durch die Möglichkeit, die Geschichte anzupassen – um möglicherweise eine schlechte Designentscheidung auszumerzen oder Commits logischer anzuordnen –, können Sie einen »idealistischeren« Verlauf schaffen. Als Entwickler werden Sie einen gewissen Sinn in einem vollständigen, feinkörnigen, realistischen Verlauf erkennen, da dieser archäologische Details darüber bewahrt, wie sich eine gute oder schlechte Idee entwickelt hat. Eine komplette History könnte Informationen darüber liefern, wann sich ein Bug eingeschlichen hat, oder einen genauen Bug-Fix erklären. Eine Analyse des Verlaufs könnte sogar Einblicke in die Arbeitsweise eines Entwicklers oder eines Entwicklerteams erbringen und Verbesserungen ermöglichen. Viele dieser Details gehen vermutlich verloren, wenn in einem überarbeiteten Verlauf Zwischenschritte fehlen. Hat der Entwickler so eine gute Lösung einfach intuitiv gefunden? Oder waren etwas mehr Verfeinerungsschritte erforderlich? Was ist die eigentliche Ursache für einen Bug? Wenn die Zwischenschritte nicht im Commit-Verlauf festgehalten werden, sind Antworten auf diese Fragen kaum zu liefern. Andererseits ist ein sauberer Verlauf, der wohldefinierte Schritte zeigt, die jeweils einen logischen Fortschritt repräsentieren, oft eine Freude – beim Lesen und beim Arbeiten. Man muss sich darüber hinaus keine Sorgen über die Launen eines möglicherweise fehlerhaften oder suboptimalen Schrittes in der Geschichte des Repository machen. Andere Entwickler, die sich den Verlauf anschauen, können dadurch eine bessere Entwicklungstechnik erwerben. Ist daher ein detaillierter realistischer Verlauf ohne Informationsverlust der beste Ansatz? Oder ist ein sauberer Verlauf besser? Wahrscheinlich liegt die Wahrheit in der Mitte. Wenn Sie sich die Vorteile der Git-Zweige zunutze machen, können Sie vermutlich sogar sowohl einen feinkörnigen realistischen Verlauf als auch einen idealisierten Verlauf im selben Repository darstellen. Git bietet Ihnen die Möglichkeit, den tatsächlichen Verlauf aufzuräumen und ihn in einen idealisierteren oder saubereren Verlauf umzuwandeln, bevor Sie ihn veröffentlichen. Wie Sie sich letztendlich entscheiden, hängt ganz und gar von Ihnen und Ihren Projektregeln ab.
164 | Kapitel 10: Commits verändern
Vorsicht beim Verändern des Verlaufs Im Prinzip müssen Sie sich keinen Zwang antun, den Verlauf der Commits in Ihrem Repository zu verändern und zu verbessern, solange noch kein anderer Entwickler1 eine Kopie Ihres Repository erhalten hat. Oder um es pedantischer auszudrücken: Sie können einen bestimmten Zweig Ihres Repository verändern, solange noch niemand anders eine Kopie dieses Zweigs besitzt. Merken Sie sich einfach, dass Sie einen Teil eines Zweigs nicht mehr umschreiben, überarbeiten oder verändern dürfen, wenn Sie diesen veröffentlicht haben und er auch in einem anderen Repository vorliegen könnte. Nehmen Sie z. B. an, dass Sie an Ihrem master-Zweig gearbeitet und die Commits A bis D einem anderen Entwickler zur Verfügung gestellt haben (siehe Abbildung 10-1). Nachdem das geschehen ist, wird diese Aufzeichnung als »veröffentlichter Verlauf« bezeichnet. A
B
C
D
master
Abbildung 10-1: Ihr veröffentlichter Verlauf
Nehmen Sie weiterhin an, dass Sie die Entwicklung fortsetzen und die neuen Commits W bis Z als unveröffentlichten Verlauf im selben Zweig erzeugen. Das wird in Abbildung 10-2 dargestellt. A
B
C
D
W
X
Y
Z
master
Abbildung 10-2: Ihr unveröffentlichter Verlauf
In dieser Lage müssen Sie sorgfältig darauf achten, alle Commits vor W unangetastet zu lassen. Solange Sie jedoch Ihren master-Zweig nicht wieder veröffentlichen, gibt es keinen Grund, die Commits W bis Z nicht zu verändern, und das schließt das Ändern der Reihenfolge, das Kombinieren sowie das Entfernen eines oder mehrerer Commits und – näherliegend – das Hinzufügen weiterer Commits in Folge der Entwicklung ein. Sie erhalten am Ende einen neuen und verbesserten Commit-Verlauf, wie Abbildung 10-3 zeigt. In diesem Beispiel wurden die Commits X und Y zu einem neuen Commit kombiniert, Commit W wurde leicht verändert und ergab das neue, ähnliche Commit W', Commit Z wurde im Verlauf weiter nach vorn verschoben und das neue Commit P wurde eingeführt.
1
Das schließt auch Sie ein!
Vorsicht beim Verändern des Verlaufs
| 165
A
B
C
D
W'
Z
P
XY
master
Abbildung 10-3: Ihr neuer Verlauf
In diesem Kapitel erkunden wir Techniken, mit deren Hilfe Sie Ihren Commit-Verlauf verändern und verbessern können. Sie müssen selbst einschätzen, ob der neue Verlauf besser ist, wann der Verlauf gut genug ist und wann er für eine Veröffentlichung bereit ist.
git reset benutzen Der Befehl git reset bringt Ihr Repository und Ihr Arbeitsverzeichnis in einen bekannten Zustand. Insbesondere stellt git reset das HEAD-Ref auf ein angegebenes Commit und aktualisiert standardmäßig den Index, sodass er diesem Commit entspricht. Falls gewünscht, kann git reset auch das Arbeitsverzeichnis so modifizieren, dass es die Revision Ihres Projekts widerspiegelt, die durch das angegebene Commit repräsentiert wird. Man könnte git reset als »destruktiv« interpretieren, weil man damit Änderungen im Arbeitsverzeichnis überschreiben und zerstören kann. In der Tat können Daten verloren gehen. Selbst wenn Sie ein Backup Ihrer Dateien haben, ist es unter Umständen nicht möglich, die Arbeit wiederherzustellen. Allerdings besteht der ganze Sinn dieses Befehls darin, bekannte Zustände für den HEAD, den Index und das Arbeitsverzeichnis zu etablieren und wiederherzustellen. Der git reset-Befehl kennt drei Optionen: git reset --soft Commit
Die Option --soft ändert das HEAD-Ref so, dass es auf ein bestimmtes Commit zeigt. Der Inhalt des Index und des Arbeitsverzeichnisses bleibt unverändert. Diese Version des Befehls hat den geringsten Effekt, da nur der Zustand einer symbolischen Referenz geändert wird, sodass diese auf ein neues Commit zeigt. git reset --mixed Commit --mixed ändert HEAD, sodass er auf das angegebene Commit zeigt. Der Inhalt des
Index wird ebenfalls modifiziert, sodass er sich an die Baumstruktur anpasst, die von dem Commit benannt wird; der Inhalt des Arbeitsverzeichnisses dagegen bleibt unverändert. Diese Version des Befehls hinterlässt den Index so, als hätten Sie alle Änderungen, die von dem Commit repräsentiert werden, gerade erst bereitgestellt, und teilt Ihnen mit, was in Ihrem Arbeitsverzeichnis modifiziert verbleibt. --mixed ist der Standardmodus für git reset. git reset --hard Commit
Diese Variante ändert das HEAD-Ref, sodass es auf das angegebene Commit verweist. Der Inhalt Ihres Index wird ebenfalls modifiziert, damit er mit der Baumstruktur
166 | Kapitel 10: Commits verändern
übereinstimmt, die von dem angegebenen Commit repräsentiert wird. Außerdem ändert sich der Inhalt Ihres Arbeitsverzeichnisses und gibt nun den Zustand des Baums wieder, der von dem genannten Commit repräsentiert wird. Wenn Ihr Arbeitsverzeichnis sich ändert, wird auch die komplette Verzeichnisstruktur geändert, damit sie wieder dem angegebenen Commit entspricht. Modifikationen gehen verloren, und neue Dateien werden entfernt. Dateien, die in dem angegebenen Commit vorhanden sind, im Arbeitsverzeichnis dagegen nicht mehr existieren, werden wiedereingesetzt. Diese Effekte werden in Tabelle 10-1 zusammengefasst. Tabelle 10-1: Wirkungen der git reset-Optionen Option
HEAD
--soft
Ja
Index
--mixed
Ja
Ja
--hard
Ja
Ja
Arbeitsverzeichnis
Ja
Der Befehl git reset speichert außerdem den ursprünglichen HEAD-Wert im Ref ORIG_ HEAD. Das ist z.B. dann sinnvoll, wenn Sie die Commit-Lognachricht des ursprünglichen HEAD als Basis für irgendein folgendes Commit verwenden wollen. Im Hinblick auf das Objektmodell gilt, dass git reset den aktuellen Zweig-HEAD innerhalb des Commit-Graphen auf ein bestimmtes Commit verschiebt. Wenn Sie --hard angeben, wird auch das Arbeitsverzeichnis umgewandelt. Schauen wir uns anhand einiger Beispiele an, wie git reset arbeitet. Im folgenden Beispiel zeigt git status, dass die Datei foo.c versehentlich im Index bereitgestellt wurde. Um das Bestätigen der Datei zu vermeiden, ziehen Sie die Bereitstellung mit git reset HEAD wieder zurück: $ git add foo.c # Ups! Wollte foo.c gar nicht hinzufügen! $ git ls-files foo.c main.c $ git reset HEAD foo.c $ git ls-files main.c
In dem Commit, das durch HEAD repräsentiert wird, gibt es keinen Pfadnamen foo.c (sonst wäre git add foo.c überflüssig). Hier könnte git reset auf HEAD für foo.c umschrieben werden mit »Sorge im Hinblick auf die Datei foo.c dafür, dass mein Index so aussieht wie
git reset benutzen
| 167
in HEAD, wo die Datei nicht vorhanden war«. Oder mit anderen Worten: »Entferne foo.c aus dem Index.« Eine andere gebräuchliche Anwendung für git reset besteht darin, das oberste Commit in einem Zweig einfach zu wiederholen oder zu eliminieren. Als Beispiel wollen wir einen Zweig mit zwei Commits darin anlegen: $ git init Initialized empty Git repository in /tmp/reset/.git/ $ echo foo >> master_datei $ git add master_datei $ git commit Created initial commit e719b1f: Hinzufügen von master_datei zu master-Zweig 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 master_datei $ echo "mehr foo" >> master_datei $ git commit master_datei Created commit 0f61a54: Mehr foo hinzufügen 1 files changed, 1 insertions(+), 0 deletions(-) $ git show-branch --more=5 [master] Mehr foo hinzufügen [master^] Hinzufügen von master_datei zu master-Zweig
Nehmen Sie nun an, dass das zweite Commit falsch ist und Sie zurückgehen und es anders machen wollen. Das ist eine klassische Anwendung für git reset --mixed HEAD^. Rufen Sie sich aus »Commits festlegen« auf Seite 73 ins Gedächtnis zurück, dass HEAD^ auf das Eltern-Commit des aktuellen master-HEAD verweist und den Zustand unmittelbar vor Abschluss des zweiten, fehlerhaften Commit repräsentiert. # --mixed ist die Vorgabe $ git reset HEAD^ master_datei: locally modified $ git show-branch --more=5 [master] Hinzufügen von master_datei zu master-Zweig $ cat master_datei foo mehr foo
Nach git reset HEAD^ hat Git den neuen Zustand von master_datei sowie das gesamte Arbeitsverzeichnis genau so gelassen, wie es unmittelbar vor dem »Mehr foo hinzufügen«-Commit war. Da die Option --mixed den Index zurücksetzt, müssen Sie alle Änderungen erneut im Index bereitstellen, die Teil des neuen Commit werden sollen. Das gibt Ihnen die Möglichkeit, master_datei noch einmal zu bearbeiten, weitere Dateien hinzuzufügen oder andere Änderungen auszuführen, bevor Sie ein neues Commit erzeugen.
168 | Kapitel 10: Commits verändern
$ echo "noch mehr foo" >> master_datei $ git commit master_datei Created commit 04289da: foo aktualisiert 1 files changed, 2 insertions(+), 0 deletions(-) $ git show-branch --more=5 [master] foo aktualisiert [master^] Hinzufügen von master_datei zu master-Zweig
Nun wurden auf dem master-Zweig nur zwei Commits durchgeführt, nicht drei. Falls Sie wiederum den Index gar nicht ändern müssen (weil alles korrekt bereitgestellt wurde), aber noch einmal an der Commit-Meldung arbeiten wollen, dann verwenden Sie stattdessen --soft: $ git reset --soft HEAD^ $ git commit
Der Befehl git reset --soft HEAD^ bringt Sie zurück zum vorigen Platz im Commit-Graphen, lässt den Index aber unverändert. Alles wird genauso bereitgestellt wie vor dem git reset-Befehl. Sie bekommen einfach noch einen Versuch bei der Commit-Meldung. Auch wenn Sie den Befehl nun verstanden haben, setzen Sie ihn nicht ein. Lesen Sie stattdessen weiter unten alles über git commit --amend!
Nehmen Sie jedoch an, dass Sie das zweite Commit vollständig eliminieren wollen und Ihnen dessen Inhalt egal ist. Verwenden Sie in diesem Fall die Option --hard: $ git reset --hard HEAD^ HEAD is now at e719b1f Hinzufügen von master_datei zu master-Zweig $ git show-branch --more=5 [master] Hinzufügen von master_datei zu master-Zweig
Genau wie git reset --mixed HEAD^ sorgt die Option --hard dafür, dass der master-Zweig zurück in seinen unmittelbar vorangegangenen Zustand gezogen wird. Sie modifiziert außerdem das Arbeitsverzeichnis, sodass es ebenfalls den vorherigen Zustand widerspiegelt, nämlich HEAD^. Speziell der Zustand von master_datei in Ihrem Arbeitsverzeichnis wird verändert – die Datei enthält wieder nur noch eine, die Originalzeile: $ cat master_datei foo
Obwohl diese Beispiele alle in irgendeiner Form HEAD benutzen, können Sie git reset auf ein beliebiges Commit im Repository anwenden. Um z.B. mehrere Commits in Ihrem aktuellen Zweig zu eliminieren, könnten Sie git reset --hard HEAD~3 oder sogar git reset --hard master~3 benutzen. Seien Sie aber vorsichtig: Sie können zwar andere Commits mithilfe eines Zweignamens benennen, das ist aber nicht identisch mit dem Auschecken des Zweigs. Während der git
git reset benutzen
| 169
reset-Operation bleiben Sie im selben Zweig. Sie können Ihr Arbeitsverzeichnis so verändern, dass es wie der Kopf eines anderen Zweigs aussieht, Sie sind aber immer noch in Ihrem ursprünglichen Zweig.
Um die Benutzung von git reset mit anderen Zweigen zu verdeutlichen, wollen wir einen zweiten Zweig namens dev anlegen und eine neue Datei in ihn einfügen: # Sollte bereits auf master sein, gehen Sie aber sicher $ git checkout master Already on "master" $ git checkout -b dev $ echo bar >> dev_datei $ git add dev_datei $ git commit Created commit 7ecdc78: Hinzufügen von dev_datei zu dev-Zweig 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 dev_datei
Zurück auf dem master-Zweig gibt es nur eine Datei: $ git checkout master Switched to branch "master" $ git rev-parse HEAD e719b1fe81035c0bb5e1daaa6cd81c7350b73976 $ git rev-parse master e719b1fe81035c0bb5e1daaa6cd81c7350b73976 $ ls master_datei
Mittels --soft wird nur die HEAD-Referenz geändert: # Ändern Sie HEAD, damit es auf das dev-Commit zeigt $ git reset --soft dev $ git rev-parse HEAD 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f $ ls master_datei $ git show-branch ! [dev] Hinzufügen von dev_datei zu dev-Zweig * [master] Hinzufügen von dev_datei zu dev-Zweig -+* [dev] Hinzufügen von dev_datei zu dev-Zweig
Es sieht sicherlich so aus, als wären der master-Zweig und der dev-Zweig beim selben Commit. Und zu einem gewissen Grad sind sie es auch (Sie befinden sich weiterhin im master-Zweig, und das ist auch gut so), allerdings hinterlässt diese Operation das Ganze in einem seltsamen Zustand. Was würde nämlich passieren, wenn Sie jetzt ein Commit
170 | Kapitel 10: Commits verändern
durchführten? Der HEAD verweist auf ein Commit, das die Datei dev_datei enthält, allerdings befindet sich diese Datei nicht im master-Zweig: $ echo "Funny" >> neu $ git add neu $ git commit -m "Welches Eltern-Commit?" Created commit f48bb36: Welches Eltern-Commit? 2 files changed, 1 insertions(+), 1 deletions(-) delete mode 100644 dev_datei create mode 100644 neu $ git show-branch ! [dev] Hinzufügen von dev_datei zu dev-Zweig * [master] Welches Eltern-Commit? -* [master] Welches Eltern-Commit? +* [dev] Hinzufügen von dev_datei zu dev-Zweig
Git hat korrekterweise neu hinzugefügt und offensichtlich festgestellt, dass dev_datei in diesem Commit nicht vorhanden ist. Aber wieso hat Git dieses dev_datei denn entfernt? Git hat recht damit, dass dev_datei nicht Bestandteil dieses Commit ist, es ist aber irreführend zu sagen, dass es entfernt wurde, weil es vorher auch nicht da war! Weshalb hat Git also entschieden, die Datei zu entfernen? Die Antwort lautet, dass Git das Commit benutzt, auf das HEAD zu dem Zeitpunkt verwies, zu dem ein neues Commit gemacht wurde. Schauen wir uns an, welches das war: $ git cat-file -p HEAD tree 948ed823483a0504756c2da81d2e6d8d3cd95059 parent 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f author Jon Loeliger <[email protected]> 1229631494 -0600 committer Jon Loeliger <[email protected]> 1229631494 -0600 Welches Eltern-Commit?
Das Eltern-Commit dieses Commit ist 7ecdc7, die Spitze des dev-Zweigs und nicht des master-Zweigs. Dieses Commit wurde allerdings durchgeführt, als wir im master-Zweig waren. Diese Vermischung dürfte uns nicht überraschen, da master-HEAD so geändert wurde, dass es auf dev-HEAD verweist! Sie könnten an dieser Stelle zu dem Schluss kommen, dass das letzte Commit völliger Humbug ist und ganz entfernt werden sollte. Und das ist richtig so. Dieser verwirrende Zustand darf nicht im Repository verbleiben. Wie beim eben gezeigten Beispiel scheint dies eine ausgezeichnete Gelegenheit für den Befehl git reset --hard HEAD^ zu sein. Allerdings ist die Lage jetzt ziemlich verzwickt. Der offensichtliche Ansatz, zur »vorherigen Version« des master-HEAD zurückzugelangen, ist einfach die Benutzung von HEAD^: # Achten Sie zuerst darauf, dass wir im master-Zweig sind $ git checkout master
git reset benutzen
| 171
# SCHLECHTES BEISPIEL! # Zum vorhergehenden Zustand von master zurücksetzen $ git reset --hard HEAD^
Wo liegt das Problem? Sie haben gerade gesehen, dass das Eltern-Commit von HEAD auf dev verweist und nicht auf das vorhergehende Commit im Original-master-Zweig: # Jupp, HEAD^ zeigt auf den dev-HEAD. Mist. $ git rev-parse HEAD^ 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f
Es gibt verschiedene Möglichkeiten, um das Commit zu ermitteln, auf das der masterZweig wirklich zurückgesetzt werden soll: $ git log commit f48bb36016e9709ccdd54488a0aae1487863b937 Author: Jon Loeliger <[email protected]> Date: Thu Dec 18 14:18:14 2008 -0600 Welches Eltern-Commit? commit 7ecdc781c3eb9fbb9969b2fd18a7bd2324d08c2f Author: Jon Loeliger <[email protected]> Date: Thu Dec 18 13:05:08 2008 -0600 Hinzufügen von dev_datei zu dev-Zweig commit e719b1fe81035c0bb5e1daaa6cd81c7350b73976 Author: Jon Loeliger <[email protected]> Date: Thu Dec 18 11:44:45 2008 -0600 Hinzufügen von master_datei zu master-Zweig
Das letzte Commit (e719b1f) ist richtig. Eine andere Methode nutzt das Reflog, das einen Verlauf der Änderungen an den Refs in Ihrem Repository zeigt: $ git reflog f48bb36... HEAD@{0}: commit: Welches Eltern-Commit? 7ecdc78... HEAD@{1}: dev: updating HEAD e719b1f... HEAD@{2}: checkout: moving from dev to master 7ecdc78... HEAD@{3}: commit: Hinzufügen von dev_datei zu dev-Zweig e719b1f... HEAD@{4}: checkout: moving from master to dev e719b1f... HEAD@{5}: checkout: moving from master to master e719b1f... HEAD@{6}: HEAD^: updating HEAD 04289da... HEAD@{7}: commit: foo aktualisiert e719b1f... HEAD@{8}: HEAD^: updating HEAD 72c001c... HEAD@{9}: commit: Mehr foo hinzufügen e719b1f... HEAD@{10}: HEAD^: updating HEAD 0f61a54... HEAD@{11}: commit: Mehr foo hinzufügen
Wenn man sich diese Liste anschaut, merkt man, dass die dritte Zeile den Wechsel vom dev-Zweig zum master-Zweig verzeichnet. Zu diesem Zeitpunkt war e719b1f der master-
172 | Kapitel 10: Commits verändern
HEAD. Sie könnten also wieder direkt e719b1f oder den symbolischen Namen HEAD@{2}
benutzen: $ git rev-parse HEAD@{2} e719b1fe81035c0bb5e1daaa6cd81c7350b73976 $ git reset --hard HEAD@{2} HEAD is now at e719b1f Hinzufügen von master_datei zu master-Zweig $ git show-branch ! [dev] Hinzufügen von dev_datei zu dev-Zweig * [master] Hinzufügen von master_datei zu master-Zweig -+ [dev] Hinzufügen von dev_datei zu dev-Zweig +* [master] Hinzufügen von master_datei zu master-Zweig
Wie gerade demonstriert, kann das Reflog oft dabei helfen, frühere Zustandsinformationen für Refs, etwa Zweignamen, zu finden. Ebenso ist es verkehrt, wenn man versucht, die Zweige mithilfe von git reset --hard zu wechseln: $ git reset --hard dev HEAD is now at 7ecdc78 Hinzufügen von dev_datei zu dev-Zweig $ ls dev_datei
master_datei
Auch das scheint korrekt zu sein. In diesem Fall wurde das Arbeitsverzeichnis sogar mit den richtigen Dateien aus dem dev-Zweig gefüllt. Das hat aber nicht funktioniert. Der master-Zweig bleibt aktuell: $ $ git branch dev * master
Genau wie im vorigen Beispiel würde ein Commit an dieser Stelle den Graphen völlig durcheinanderbringen. Und wie zuvor besteht die richtige Aktion darin, den korrekten Zustand zu ermitteln und in diesen zurückzusetzen: $ git reset --hard e719b1f
Oder wahrscheinlich sogar zu $ git reset --soft e719b1f
Mit --soft wird das Arbeitsverzeichnis nicht verändert, was bedeutet, dass Ihr Arbeitsverzeichnis nun den Gesamtinhalt (Dateien und Verzeichnisse) repräsentiert, der an der Spitze des dev-Zweigs vorhanden ist. Da außerdem HEAD jetzt korrekterweise wieder auf die ursprüngliche Spitze des master-Zweigs verweist, würde ein Commit an dieser Stelle einen gültigen Graphen mit dem neuen master-Zustand ergeben, der exakt gleich der Spitze des dev-Zweiges wäre. Natürlich muss das nicht unbedingt das sein, was Sie wollen. Aber Sie können es tun.
git reset benutzen
| 173
git cherry-pick benutzen Der Befehl git cherry-pick Commit wendet die Änderungen, die von dem genannten Commit eingeführt wurden, auf den aktuellen Zweig an. Er führt ein neues, eigenes Commit ein. Strenggenommen wird bei Benutzung von git cherry-pick der vorhandene Verlauf innerhalb eines Repository nicht verändert, sondern erweitert. Wie bei anderen Git-Operationen, die über das Anwenden eines Diff Änderungen einführen, müssen Sie möglicherweise Konflikte auflösen, damit die Änderungen aus dem angegebenen Commit voll wirksam werden. Der Befehl git cherry-pick wird typischerweise verwendet, um bestimmte Commits von einem Zweig in einem Repository auf einen anderen Zweig zu überführen. Eine gebräuchliche Anwendung ist es, Commits aus einem Wartungszweig in einen Entwicklungszweig vor- oder zurückzuportieren. In Abbildung 10-4 enthält der dev-Zweig die normale Entwicklung, während rel_2.3 die Commits für die Wartung von Release 2.3 enthält. A
B
C
D
V
E
W
F
X
G
Y
H
Z
dev
rel_2.3
Abbildung 10-4: Vor der Ausführung von git cherry-pick auf einem Commit
Während der normalen Entwicklungsarbeit wird in der Entwicklungslinie mit Commit F ein Bug behoben. Stellt sich heraus, dass dieser Bug auch im Release 2.3 enthalten ist, dann kann der Bug-Fix F mithilfe des Befehls git cherry-pick auf den Zweig rel_2.3 angewandt werden: $ git checkout rel_2.3 $ git cherry-pick dev~2
# Commit F, oben
Nach cherry-pick ähnelt der Graph Abbildung 10-5. A
B
C
V
D
W
E
X
F
Y
G
Z
H
F'
Abbildung 10-5: Nach der Ausführung von git cherry-pick auf einem Commit
174 | Kapitel 10: Commits verändern
dev
rel_2.3
In Abbildung 10-5 ähnelt Commit F' im Wesentlichen Commit F, es ist aber ein neues Commit und muss angepasst werden – wahrscheinlich mit einer Konfliktauflösung –, damit es sich in Commit Z einbringt statt in Commit E. Keines der Commits, die auf F folgen, wird nach F' angewandt; es wird nur das genannte Commit ausgewählt und eingesetzt. Eine andere gebräuchliche Anwendung für cherry-pick ist das Umbauen einer Reihe von Commits, indem selektiv ein Stapel aus einem Zweig ausgewählt und in einen neuen Zweig eingeführt wird. Nehmen Sie an, Sie haben eine Reihe von Commits in Ihrem Entwicklungszweig mein_ dev (siehe Abbildung 10-6) und wollen diese in Ihren master-Zweig einführen, allerdings in einer völlig anderen Reihenfolge. A
B
C
D
V
master
W
X
Y
Z
my_dev
Abbildung 10-6: Vor dem Mischen mit git cherry-pick
Um sie in der Reihenfolge Y, W, X, Z auf den master-Zweig anzuwenden, könnten Sie folgende Befehle benutzen: $ $ $ $ $
git git git git git
checkout master cherry-pick mein_dev^ cherry-pick mein_dev~3 cherry-pick mein_dev~2 cherry-pick mein_dev
# # # #
Y W X Z
Anschließend würde Ihr Commit-Verlauf in etwa so aussehen wie in Abbildung 10-7. A
B
C
V
D
W
Y'
X
W'
Y
X'
Z
Z'
dev
my_dev
Abbildung 10-7: Nach dem Mischen mit git cherry-pick
In Situationen, in denen die Reihenfolge der Commits sprunghafte Änderungen erfährt, müssen Sie mit hoher Wahrscheinlichkeit Konflikte auflösen. Das hängt völlig von der Beziehung zwischen den Commits ab. Wenn sie stark verkoppelt sind und Überschneidungen enthalten, werden Sie Konflikte erhalten, die gelöst werden müssen. Sind sie dagegen sehr unabhängig voneinander, können Sie sie leicht verschieben.
git cherry-pick benutzen
| 175
git revert benutzen Der Befehl git revert Commit ähnelt im Wesentlichen dem Befehl git cherry-pick Commit, aber mit einem wichtigen Unterschied: Er wendet die Umkehrung des angegebenen Commit an. Das heißt, dass dieser Befehl benutzt wird, um ein neues Commit einzuführen, das die Wirkung eines angegebenen Commit umkehrt. Wie git cherry-pick verändert auch der revert-Befehl den vorhandenen Verlauf in einem Repository nicht. Stattdessen fügt er dem Verlauf ein neues Commit hinzu. Eine gebräuchliche Anwendung für git revert besteht darin, die Auswirkungen eines Commit »rückgängig zu machen«, der – möglicherweise tief – im Verlauf eines Zweigs vergraben ist. In Abbildung 10-8 hat sich im master-Zweig eine ganze Serie von Änderungen aufgetürmt. Aus irgendeinem Grund – vermutlich durch Tests – hat sich Commit D als fehlerhaft erwiesen. A
B
C
D
E
F
G
master
Abbildung 10-8: Vor einem einfachen git revert
Um die Lage zu bereinigen, könnte man einfach Änderungen vornehmen, die die Wirkungen von D aufheben, und dann dieses Ergebnis direkt mit einem Commit bestätigen. In Ihrer Commit-Meldung merken Sie dann an, dass der Zweck dieses Commit darin bestand, die Änderungen umzukehren, die durch das frühere Commit verursacht wurden. Einfacher geht es, wenn man git revert benutzt: $ git revert master~3
# Commit D
Das Ergebnis sieht so aus wie in Abbildung 10-9, wo Commit D' die Umkehrung von Commit D bildet. A
B
C
D
E
F
G
D'
master
Abbildung 10-9: Nach einem einfachen git revert
reset, revert und checkout Die drei Git-Befehle reset, revert und checkout können ein wenig verwirrend sein, weil sie ähnliche Operationen durchzuführen scheinen. Darüber hinaus haben die Wörter Reset, Revert und Checkout in anderen Versionskontrollsystemen eine andere Bedeutung. Es gibt jedoch Richtlinien dafür, wann man die jeweiligen Befehle benutzen sollte und wann nicht.
176 | Kapitel 10: Commits verändern
Wenn Sie in einen anderen Zweig wechseln wollen, dann benutzen Sie git checkout. Ihr aktueller Zweig und das HEAD-Ref wechseln auf die Spitze des angegebenen Zweigs. Der Befehl git reset nimmt keine Veränderung an Ihrem Zweig vor. Wenn Sie jedoch den Namen eines Zweigs vorgeben, ändert er den Zustand Ihres aktuellen Arbeitsverzeichnisses derart, dass es aussieht wie die Spitze des genannten Zweigs. Mit anderen Worten: git reset soll die HEAD-Referenz des aktuellen Zweigs zurücksetzen. Da git reset --hard einen bekannten Zustand wiederherstellen soll, ist es auch in der Lage, fehlgeschlagene oder überfällige Merge-Versuche zu entsorgen, was git checkout nicht tut. Wenn Sie also ein ausstehendes Merge-Commit haben und versuchen, die Angelegenheit mit git checkout statt mit git reset --hard zu retten, wird Ihr nächstes Commit fälschlicherweise ein Merge-Commit sein! Die Verwirrung in Bezug auf git checkout ergibt sich aus seiner zusätzlichen Fähigkeit, eine Datei aus dem Objektspeicher zu extrahieren und in Ihr Arbeitsverzeichnis zu packen, wobei möglicherweise eine Version in Ihrem Arbeitsverzeichnis ersetzt wird. Manchmal gehört die Version dieser Datei zur aktuellen HEAD-Version, manchmal dagegen handelt es sich um eine frühere Version. # datei.c aus Index auschecken $ git checkout -- pfad/auf/datei.c # datei.c aus rev v2.3 auschecken $ git checkout v2.3 -- eine/datei.c
Git nennt das »einen Pfad auschecken«. Im ersten Fall scheint das Abrufen der aktuellen Version aus dem Objektspeicher eine Form von »Reset«-Operation zu sein – das heißt, die lokalen Änderungen an der Datei im Arbeitsverzeichnis werden verworfen, weil die Datei in ihre aktuelle HEAD-Version »zurückgesetzt« wird. Das ist doppelplus-ungutes Git-Denken. Im zweiten Fall wird eine frühere Version der Datei aus dem Objektspeicher gezogen und in Ihr Arbeitsverzeichnis gelegt. Das hat den Anschein einer »Umkehr«-Operation auf der Datei. Auch das ist doppelplus-ungutes Git-Denken. In beiden Fällen sollte man sich die Operation nicht als Git-Reset oder eine Umkehrung vorstellen, sondern die Datei wird von einem bestimmten Commit »ausgecheckt«: HEAD bzw. v2.3. Der Befehl git revert agiert auf vollständigen Commits, nicht auf Dateien. Wenn ein anderer Entwickler Ihr Repository geklont oder einige Ihrer Commits geholt hat, hat das Auswirkungen auf das Ändern des Commit-Verlaufs. In diesem Fall sollten Sie besser keine Befehle verwenden, die den Verlauf in Ihrem Repository verändern. Nehmen Sie stattdessen git revert; benutzen Sie weder git reset noch den Befehl git commit --amend, der im nächsten Abschnitt beschrieben wird.
reset, revert und checkout
| 177
Das oberste Commit ändern Eine der einfachsten Methoden zum Verändern des neuesten Commit auf Ihrem aktuellen Zweig bietet der Befehl git commit --amend. Typischerweise impliziert »amend«, also »berichtigen«, dass das Commit grundsätzlich denselben Inhalt hat, irgendein Aspekt aber der Korrektur oder der Bereinigung bedarf. Das eigentliche Commit-Objekt, das in den Objektspeicher eingeführt wird, ist dann natürlich anders. Oft wird git commit --amend benutzt, um Tippfehler nach einem Commit zu korrigieren. Das ist aber nicht die einzige Anwendung. Wie bei jedem Commit kann dieser Befehl eine oder mehrere Dateien im Repository verbessern und tatsächlich eine Datei als Teil des neuen Commit hinzufügen oder löschen. Wie beim normalen git commit-Befehl bietet Ihnen git commit --amend einen Editor an, in dem Sie die Commit-Meldung ändern können. Nehmen Sie z.B. an, dass Sie an einer Rede arbeiten und gerade folgendes Commit2 ausgeführt haben: $ git show commit 0ba161a94e03ab1e2b27c2e65e4cbef476d04f5d Author: Jon Loeliger <[email protected]> Date: Thu Jun 26 15:14:03 2008 -0500 Redeentwurf diff --git a/speech.txt b/speech.txt new file mode 100644 index 0000000..310bcf9 --- /dev/null +++ b/speech.txt @@ -0,0 +1,5 @@ +Three score and seven years ago +our fathers brought forth on this continent, +a new nation, conceived in Liberty, +and dedicated to the proposition +that all men are created equal.
An dieser Stelle wird das Commit in Gits Objekt-Repository gespeichert, allerdings enthält es kleinere Fehler im Text. Um diese zu korrigieren, könnten Sie die Datei einfach bearbeiten und ein zweites Commit durchführen. Der Verlauf würde sich dann folgendermaßen darstellen: $ git show-branch --more=5 [master] Fehler im zeitlichen Ablauf beheben [master^] Redeentwurf
2
Es handelt sich hier um die Gettysburg Address von Abraham Lincoln, eine seiner berühmtesten Reden. Siehe http://de.wikipedia.org/wiki/Gettysburg_Address.
178 | Kapitel 10: Commits verändern
Falls Sie allerdings in Ihrem Repository einen etwas saubereren Commit-Verlauf hinterlassen wollen, können Sie dieses Commit direkt verändern und ersetzen. Bearbeiten Sie dazu die Datei in Ihrem Arbeitsverzeichnis. Korrigieren Sie die Tippfehler und fügen Sie bei Bedarf Dateien hinzu oder löschen Sie welche. Aktualisieren Sie wie üblich den Index mit git add oder git rm. Führen Sie dann den Befehl git commit --amend aus: # speech.txt nachbearbeitet $ git diff diff --git a/speech.txt b/speech.txt index 310bcf9..7328a76 100644 --- a/speech.txt +++ b/speech.txt @@ -1,5 +1,5 @@ -Three score and seven years ago +Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition -that all men are created equal. +that all men and women are created equal. $ git add speech.txt $ git commit --amend # Auch die Commit-Meldung kann nötigenfalls bearbeitet werden # In diesem Beispiel wurde etwas geändert ...
Bei einer Berichtigung kann jeder sehen, dass das ursprüngliche Commit modifiziert wurde und das vorhandene Commit ersetzt: $ git show-branch --more=5 [master] Redeentwurf, der vertraut klingt $ git show commit 47d849c61919f05da1acf983746f205d2cdb0055 Author: Jon Loeliger <[email protected]> Date: Thu Jun 26 15:14:03 2008 -0500 Redeentwurf, der vertraut klingt diff --git a/speech.txt b/speech.txt new file mode 100644 index 0000000..7328a76 --- /dev/null +++ b/speech.txt @@ -0,0 +1,5 @@ +Four score and seven years ago +our fathers brought forth on this continent, +a new nation, conceived in Liberty, +and dedicated to the proposition +that all men and women are created equal.
Das oberste Commit ändern
| 179
Dieser Befehl kann die Metainformationen in einem Commit bearbeiten. Z.B. ändern Sie mit --author den Autor des Commit: $ git commit --amend --author "Bob Miller " # ... schließen Sie einfach den Editor ... $ git log commit 0e2a14f933a3aaff9edd848a862e783d986f149f Author: Bob Miller Date: Thu Jun 26 15:14:03 2008 -0500 Redeentwurf, der vertraut klingt
Wenn man es bildlich ausdrückt, ändert sich der Commit-Graph beim Einsatz von git commit --amend von der Darstellung in Abbildung 10-10 in die Darstellung in Abbildung 10-11. A
B
C
HEAD
Abbildung 10-10: Commit-Graph vor git commit --amend
A
B
C'
HEAD
Abbildung 10-11: Commit-Graph nach git commit --amend
Das Wesen des C-Commits ist im Prinzip gleich geblieben, allerdings wurde es verändert, sodass es jetzt C' ist. Das HEAD-Ref zeigt nicht mehr auf das alte Commit C, sondern auf den Ersatz, nämlich C'.
Rebasing von Commits Der Befehl git rebase wird verwendet, um die Basis einer Folge von Commits zu ändern. Er erfordert wenigstens den Namen des anderen Zweigs, auf den Ihre Commits verlagert werden. Standardmäßig werden die Commits vom aktuellen Zweig umgesiedelt, die sich noch nicht auf dem anderen Zweig befinden. Eine gebräuchliche Anwendung für git rebase besteht darin, einige der von Ihnen entwickelten Commits in Bezug auf einen anderen Zweig auf dem aktuellen Stand zu halten; meist handelt es sich um einen master-Zweig oder einen Tracking-Zweig aus einem anderen Repository. In Abbildung 10-12 wurden zwei Zweige entwickelt. Ursprünglich startete der topicZweig auf dem master-Zweig, und zwar bei Commit B. In der Zwischenzeit hat er sich bis Commit E weiterentwickelt.
180 | Kapitel 10: Commits verändern
A
B
C
D
W
X
E
master
Y
Z
topic
Abbildung 10-12: Vor git rebase
Sie können Ihre Commit-Reihen hinsichtlich des master-Zweigs auf dem aktuellen Stand halten, indem Sie die Commits so schreiben, dass sie auf Commit E und nicht auf Commit B basieren. Da der topic-Zweig der aktuelle Zweig sein muss, können Sie entweder $ git checkout topic $ git rebase master
oder $ git rebase master topic
benutzen. Wenn die Rebasing-Operation abgeschlossen ist, sieht der neue CommitGraph ungefähr so aus wie in Abbildung 10-13. A
B
C
D
E
master
W'
X'
Y'
Z'
topic
Abbildung 10-13: Nach git rebase
Die Verwendung des git rebase-Befehls in solchen Situationen wie in Abbildung 10-12 wird oft als Vorwärtsportierung bezeichnet. In diesem Beispiel wurde der topic-Zweig auf den master-Zweig vorwärtsportiert. Es hat nichts mit Zauberei zu tun, ob ein Rebase eine Vorwärts- oder eine Rückwärtsportierung ist; beide benutzen wahrscheinlich git rebase. Die Interpretation hängt oft damit zusammen, welche Funktionalitäten vor oder hinter anderen Funktionalitäten angenommen werden. Im Zusammenhang mit einem Repository, das Sie von irgendwoher geklont haben, ist es üblich, Ihren Entwicklungszweig mithilfe der git rebase-Operation auf den origin/master-Tracking-Zweig vorwärtszuportieren. In Kapitel 11 zeigen wir Ihnen, dass diese Operation oft von demjenigen angefordert wird, der ein Repository pflegt, etwa mit dieser Aufforderung: »Bitte verlagert Euren Patch an die Spitze des Master-Zweigs.« Mit dem git rebase-Befehl kann man sogar eine komplette Entwicklungslinie von einem Zweig auf einen völlig anderen Zweig verlagern. Dabei hilft die Option --onto.
Rebasing von Commits | 181
Nehmen Sie z.B. an, dass Sie auf dem feature-Zweig eine neue Funktion entwickelt haben. Die entsprechenden Commits P und Q basierten auf dem maint-Zweig, wie Abbildung 10-14 zeigt. Um die Commits P und Q auf dem feature-Zweig vom maint- auf den master-Zweig umzusetzen, führen Sie diesen Befehl aus: $ git rebase --onto master maint^ feature
A
B
C
W
D
X
E
master
Y
Z
P
maint
Q
feature
Abbildung 10-14: Vor der git rebase-Umsetzung
Den resultierenden Commit-Graphen sehen Sie in Abbildung 10-15. P A
B
C
W
D
X
E
Y
Q
feature master
Z
maint
Abbildung 10-15: Nach der git rebase-Umsetzung
Die Rebasing-Operation verlagert die Commits nacheinander von der entsprechenden Originalstelle an eine neue Commit-Basis. In der Folge könnten alle Commits, die verschoben wurden, mit Konflikten behaftet sein, die aufgelöst werden müssen. Wenn ein Konflikt gefunden wird, setzt die Rebasing-Operation ihre Verarbeitung zeitweise aus, damit Sie den Konflikt lösen können. Alle Konflikte, die in diesem Zusammenhang auftreten, sollten so behandelt werden, wie es im Abschnitt »Ein Merge mit einem Konflikt« auf Seite 134 beschrieben ist. Nachdem alle Konflikte aufgelöst und der Index entsprechend aktualisiert wurden, kann die Rebasing-Operation mit dem Befehl git rebase --continue fortgesetzt werden. Der Befehl bestätigt dann den aufgelösten Konflikt mit einem Commit und macht mit dem nächsten Commit in der Serie weiter. Falls Sie während der Untersuchung eines Rebasing-Konflikts entscheiden, dass dieses spezielle Commit eigentlich nicht notwendig ist, können Sie den Befehl git rebase anwei-
182 | Kapitel 10: Commits verändern
sen, dieses Commit einfach mit git rebase --skip zu überspringen und mit dem nächsten weiterzumachen. Das ist allerdings vielleicht nicht ganz korrekt, vor allem, wenn nachfolgende Commits stark von den Änderungen abhängen, die durch dieses Commit eingeführt wurden. Die Probleme könnten sich in diesem Fall lawinenartig verschärfen, sodass es wohl besser ist, den Konflikt tatsächlich aufzulösen. Sollte sich die rebase-Operation letztendlich als Fehler herausstellen, bricht der Befehl git rebase --abort sie ab und bringt das Repository wieder in den Zustand zurück, der vor dem Starten des ursprünglichen git rebase vorlag.
git rebase -i benutzen Nehmen Sie einmal an, Sie fangen an, ein Haiku zu schreiben, und schaffen zwei komplette Zeilen, bevor Sie es einchecken: $ git init Initialized empty Git repository in .git/ $ git config user.email "[email protected]" $ cat haiku Talk about colour No jealous behaviour here $ git add haiku $ git commit -m"Starte mein Haiku" Created initial commit a75f74e: Starte mein Haiku 1 files changed, 2 insertions(+), 0 deletions(-) create mode 100644 haiku
Sie schreiben weiter, beschließen aber, dass Sie die amerikanische Schreibweise von »color« anstelle der britischen verwenden wollen. Deshalb führen Sie ein Commit aus, um das zu ändern: $ git diff diff --git a/haiku b/haiku index 088bea0..958aff0 100644 --- a/haiku +++ b/haiku @@ -1,2 +1,2 @@ -Talk about colour +Talk about color No jealous behaviour here $ git commit -a -m"color anstelle von colour benutzen" Created commit 3d0f83b: color anstelle von colour benutzen 1 files changed, 1 insertions(+), 1 deletions(-)
Schließlich entwickeln Sie die letzte Zeile und bestätigen das Haiku mit einem Commit: $ git diff diff --git a/haiku b/haiku index 958aff0..cdeddf9 100644 --- a/haiku
Rebasing von Commits | 183
+++ b/haiku @@ -1,2 +1,3 @@ Talk about color No jealous behaviour here +I favour red wine $ git commit -a -m"Mein Farb-Haiku ist fertig" Created commit 799dba3: Mein Farb-Haiku ist fertig 1 files changed, 1 insertions(+), 0 deletions(-)
Jetzt haben Sie allerdings gemerkt, dass Sie bei der Schreibweise nicht konsequent waren und beschließen daher, alle britischen »ou«-Schreibweisen in amerikanische »o«-Schreibweisen zu ändern: $ git diff diff --git a/haiku b/haiku index cdeddf9..064c1b5 100644 --- a/haiku +++ b/haiku @@ -1,3 +1,3 @@ Talk about color -No jealous behaviour here -I favour red wine +No jealous behavior here +I favor red wine $ git commit -a -m"Amerikanische Schreibweise benutzen" Created commit b61b041: Amerikanische Schreibweise benutzen 1 files changed, 2 insertions(+), 2 deletions(-)
Sie haben inzwischen einen Commit-Verlauf, der so aussieht: $ git show-branch --more=4 [master] Amerikanische Schreibweise benutzen [master^] Mein Farb-Haiku ist fertig [master~2] color anstelle von colour benutzen [master~3] Starte mein Haiku
Nachdem Sie sich die Commit-Folge angeschaut oder einige Rückmeldungen erhalten haben, wollen Sie das Haiku lieber erst fertigstellen, bevor Sie es korrigieren. Deshalb wünschen Sie nun folgenden Commit-Verlauf: [master] Amerikanische Schreibweise benutzen [master^] color anstelle von colour benutzen [master~2] Mein Farb-Haiku ist fertig [master~3] Starte mein Haiku
Allerdings bemerken Sie jetzt, dass es keinen Grund dafür gibt, zwei ähnliche Commits zu haben, die die Schreibweise unterschiedlicher Wörter korrigieren. Deshalb wollen Sie master und master^ zu einem Commit zusammenlegen. [master] Amerikanische Schreibweise benutzen [master^] Mein Farb-Haiku ist fertig [master~2] Starte mein Haiku
184 | Kapitel 10: Commits verändern
Das Umsortieren, Bearbeiten, Entfernen und Zusammensetzen mehrerer Commits zu einem sowie das Aufteilen eines Commit auf mehrere Teile lässt sich mit dem Befehl git rebase und der Option -i oder --interactive leicht durchführen. Dieser Befehl erlaubt Ihnen, die Commits zu modifizieren, aus denen ein Zweig besteht, und sie wieder in denselben oder einen anderen Zweig zu legen. Bei der typischen Anwendung, die auch im Beispiel demonstriert wird, wird derselbe Zweig »an Ort und Stelle« modifiziert. In diesem Fall gibt es drei Änderungssätze zwischen vier Commits. git rebase -i muss den Namen des Commit erfahren, hinter dem Sie etwas ändern wollen: $ git rebase -i master~3
Sie gelangen in einen Editor mit einer Datei, die so aussieht: pick 3d0f83b color anstelle von colour benutzen pick 799dba3 Mein Farb-Haiku ist fertig pick b61b041 Amerikanische Schreibweise benutzen # # # # # # # # # #
Rebase a75f74e..b61b041 onto a75f74e Commands: pick = use commit edit = use commit, but stop for amending squash = use commit, but meld into previous commit If you remove a line here THAT COMMIT WILL BE LOST. However, if you remove everything, the rebase will be aborted.
Die ersten drei Zeilen listen die Commits auf, die sich innerhalb des änderbaren CommitBereichs befinden, den Sie auf der Kommandozeile angegeben haben. Die Commits werden zunächst in der Reihenfolge vom ältesten zum jüngsten aufgelistet; vor ihnen steht jeweils das Wort pick. Würden Sie den Editor jetzt verlassen, würden die einzelnen Commits ausgewählt (in der angegebenen Reihenfolge), auf den Zielzweig angewandt und mit einem Commit bestätigt. Die Zeilen, denen ein # voransteht, enthalten Hinweise und Kommentare, die vom Programm ignoriert werden. An dieser Stelle haben Sie jedoch die Möglichkeit, die Commits neu anzuordnen, zusammenzufassen, zu ändern oder zu löschen. Um die genannten Schritte auszuführen, bringen Sie die Commits in Ihrem Editor in eine andere Reihenfolge und verlassen ihn dann: pick 799dba3 Mein Farb-Haiku ist fertig pick 3d0f83b color anstelle von colour benutzen pick b61b041 Amerikanische Schreibweise benutzen
Erinnern Sie sich daran, dass das allererste Commit für das Rebasing das »Starte mein Haiku«-Commit war. Das nächste Commit wird »Mein Farb-Haiku ist fertig«, gefolgt von den »color ...«- und »Amerikanische ...«-Commits: $ git rebase -i master~3 # sortieren Sie die ersten beiden Commits neu und verlassen Sie dann den Editor
Rebasing von Commits | 185
Successfully rebased and updated refs/heads/master $ git show-branch --more=4 [master] Amerikanische Schreibweise benutzen [master^] color anstelle von colour benutzen [master~2] Mein Farb-Haiku ist fertig [master~3] Starte mein Haiku
Hier wurde der Verlauf der Commits umgeschrieben; die beiden Schreibweisen-Commits stehen zusammen, und die beiden Schreib-Commits ebenfalls. Ihr nächster Schritt besteht nun darin, die beiden Schreibweisen-Commits zu einem Commit zusammenzufassen. Starten Sie dazu erneut den Befehl git rebase -i master~3. Dieses Mal ändern Sie die Commit-Liste von pick d83f7ed Mein Farb-Haiku ist fertig pick 1f7342b color anstelle von colour benutzen pick 1915dae Amerikanische Schreibweise benutzen
in pick d83f7ed Mein Farb-Haiku ist fertig pick 1f7342b color anstelle von colour benutzen squash 1915dae Amerikanische Schreibweise benutzen
Das dritte Commit wird in das unmittelbar davor stehende Commit gequetscht und das neue Commit-Log-Template wird aus der Kombination der beiden zusammengefassten Commits gebildet. In diesem Beispiel werden die beiden Commit-Log-Meldungen zusammengefasst und in einem Editor angeboten: # Dies ist eine Kombination aus zwei Commits. # Die erste Commit-Meldung lautet so: color anstelle von colour benutzen # Dies ist die zweite Commit-Meldung: Amerikanische Schreibweise benutzen
Die Meldung kann folgendermaßen gekürzt werden: Amerikanische Schreibweise benutzen
Auch hier werden alle #-Zeilen ignoriert. Schließlich kann man sich die Ergebnisse der Rebasing-Sequenz anschauen: $ git rebase -i master~3 # zusammenfassen und die Commit-Log-Meldung umschreiben Created commit cf27784: Amerikanische Schreibweise benutzen 1 files changed, 3 insertions(+), 3 deletions(-) Successfully rebased and updated refs/heads/master
186 | Kapitel 10: Commits verändern
$ git show-branch --more=4 [master] Amerikanische Schreibweise benutzen [master^] Mein Farb-Haiku ist fertig [master~2] Starte mein Haiku
Für die hier gezeigten Schritte des Umsortierens und Zusammenfassens wurde git rebase -i master~3 zweimal aufgerufen; man hätte die beiden Phasen allerdings auch in einem Schritt ausführen können. Es ist außerdem absolut zulässig, mehrere aufeinanderfolgende Commits in einem einzigen Schritt zu einem zusammenzufassen.
rebase und merge im Vergleich Neben dem Problem der einfachen Veränderung eines Verlaufs hat die Rebasing-Operation weitere Auswirkungen, derer Sie sich bewusst sein sollten. Das Rebasing einer Folge von Commits auf der Spitze eines Zweigs, also das Ändern der Commit-Basis, ähnelt dem Zusammenführen der beiden Zweige: In beiden Fällen ist der neue Kopf dieses Zweigs eine Kombination aus beiden repräsentierten Zweigen. Sie könnten sich fragen, »Sollte ich auf meinen Commits einen Merge ausführen oder ihre Basis ändern?«. In Kapitel 11 ist das eine wichtige Fragestellung – vor allem wenn mehrere Entwickler, Repositories und Zweige ins Spiel kommen. Der Vorgang des Rebasing einer Folge von Commits veranlasst Git, eine völlig neue Folge von Commits zu generieren. Diese tragen neue SHA1-Commit-IDs, basieren auf einem neuen Anfangszustand und repräsentieren unterschiedliche Diffs, auch wenn sie Änderungen umfassen, mit denen derselbe Endzustand erreicht werden kann. Wenn man sich einer Situation wie in Abbildung 10-12 gegenübersieht, stellt das Rebasing zu Abbildung 10-13 kein Problem dar, weil niemand oder kein anderes Commit von dem Zweig abhängig ist, dessen Basis geändert wird. Allerdings kann es Ihnen selbst in Ihrem eigenen Repository passieren, dass Sie weitere Zweige haben, die auf demjenigen beruhen, den Sie verlagern wollen. Betrachten Sie dazu den Graphen in Abbildung 10-16. A
B
C
X
D
master
Y
Z
P
dev
Q
dev2
Abbildung 10-16: Vor git rebase multibranch
Möglicherweise glauben Sie, dass der Befehl # dev-Zweig an die Spitze von master verschieben $ git rebase master dev
Rebasing von Commits | 187
den Graphen in Abbildung 10-17 ergibt. Tut er aber nicht. Der erste Hinweis, dass das nicht der Fall war, lässt sich aus der Ausgabe des Befehls ableiten. $ git rebase master dev First, rewinding head to replay your work on top of it... Applying: X Applying: Y Applying: Z
A
B
C
D
master
X'
Y'
Z'
P'
dev
Q'
dev2
Abbildung 10-17: Gewünschtes git rebase multibranch
Diese besagt, dass Git die Commits nur für X, Y und Z angewandt hat. Von P oder Q war nicht die Rede, und Sie erhalten stattdessen den Graphen in Abbildung 10-18. X'
A
B
C
X
Y'
Z'
D
Y
dev
master
P
Q
dev2
Abbildung 10-18: Tatsächliches git rebase multibranch
Die Commits X', Y' und Z' sind die neuen Versionen der alten Commits, die von B abstammen. Die alten Commits X und Y existieren in dem Graphen weiterhin, da sie immer noch vom Zweig dev2 aus erreichbar sind. Das Original-Commit Z dagegen wurde entfernt, da es nicht mehr erreichbar ist. Der Zweigname, der auf das Commit gezeigt hat, wurde auf die neue Version dieses Commit verschoben. Der Verlauf des Zweigs sieht jetzt so aus, als würde er doppelte Commit-Meldungen enthalten: $ git show-branch * [dev] Z ! [dev2] Q ! [master] D
188 | Kapitel 10: Commits verändern
--* * * * + * + + + + + *++
[dev] Z [dev^] Y [dev~2] X [master] D [master^] C [dev2] Q [dev2^] P [dev2~2] Y [dev2~3] X [master~2] B
Aber erinnern Sie sich: Dabei handelt es sich um unterschiedliche Commits, die im Prinzip die gleiche Änderung durchführen. Wenn Sie einen Zweig mit einem der neuen Commits in einen anderen Zweig überführen, der eines der alten Commits enthält, kann Git nicht wissen, dass Sie dieselbe Änderung zweimal anwenden. Das Ergebnis sind daher doppelte Einträge in git log, höchstwahrscheinlich ein Merge-Konflikt und allgemeine Verwirrung. Sie müssen eine Möglichkeit finden, aufzuräumen. Sollte dieser resultierende Graph tatsächlich dem entsprechen, was Sie haben wollen, sind Sie fertig. Wahrscheinlich ist es aber so, dass das Verschieben des gesamten Zweigs (einschließlich der Teilzweige) das ist, was Sie wirklich wünschen. Um diesen Graphen zu erreichen, müssen Sie wiederum die Basis des Zweigs dev2 auf das neue Commit Y' im dev-Zweig legen: $ git rebase dev^ dev2 First, rewinding head to replay your work on top of it... Applying: P Applying: Q $ git show-branch ! [dev] Z * [dev2] Q ! [master] D --* [dev2] Q * [dev2^] P + [dev] Z +* [dev2~2] Y +* [dev2~3] X +*+ [master] D
Das ist nun der Graph, der zuvor in Abbildung 10-17 gezeigt wurde. Eine andere Situation, die sehr verwirrend sein kann, ist das Rebasing eines Zweigs, der einen Merge enthält. Nehmen Sie z.B. an, dass Ihre Zweigstruktur so aussieht wie in Abbildung 10-19. Falls Sie die gesamte dev-Zweigstruktur von Commit N nach unten durch Commit X weg von B und nach D verschieben wollen, wie in Abbildung 10-20 gezeigt, dann glauben Sie vermutlich, dass Sie mit etwas Einfachem wie git rebase master dev davonkommen.
Rebasing von Commits | 189
P
X
A
B
M
Y
C
N
dev
Z
D
master
Abbildung 10-19: Vor git rebase merge
P
X
A
B
C
Y
M
N
dev
Z
D
master
Abbildung 10-20: Gewünschtes git rebase merge
Und wieder liefert dieser Befehl überraschende Ergebnisse: $ git rebase master dev First, rewinding head to replay your work on top of it... Applying: X Applying: Y Applying: Z Applying: P Applying: N
Es sieht so aus, als hätte es das Richtige getan. Schließlich behauptet Git, dass es alle (Nicht-Merge) Commit-Änderungen angewandt hätte. Ist aber wirklich alles richtig? $ git show-branch * [dev] N ! [master] D -* [dev] N * [dev^] P * [dev~2] Z * [dev~3] Y * [dev~4] X *+ [master] D
All diese Commits sitzen jetzt in einem langen String! Was ist hier geschehen?
190 | Kapitel 10: Commits verändern
Git muss den Teil des Graphen, der von dev aus erreichbar ist, zurück an die Merge-Basis bei B verschieben, damit es die Commits im Bereich master..dev findet. Um all diese Commits aufzulisten, führt Git eine topologische Suche auf diesem Teil des Graphen durch und stellt eine linearisierte Folge aller Commits in diesem Bereich her. Sobald diese Folge ermittelt ist, wendet Git die Commits nacheinander an, wobei es beim Ziel-Commit D beginnt. Deshalb sagen wir: »Das Rebasing hat den ursprünglichen Verlauf des Zweiges (mit Merges) auf dem Master-Zweig linearisiert.« Falls es das ist, was Sie wollen, oder falls es Ihnen egal ist, dass die Form des Graphen sich geändert hat, dann sind Sie fertig. Sollten Sie jedoch in solchen Fällen explizit die Verzweigungs- und Merging-Struktur des gesamten Zweigs, dessen Basis Sie ändern, erhalten wollen, dann verwenden Sie die Option --preserve-merges: # Diese Option ist eine Eigenschaft von Version 1.6.1 $ git rebase --preserve-merges master dev Successfully rebased and updated refs/heads/dev
Mithilfe des Git-Alias aus »Einen Alias konfigurieren« auf Seite 30 können wir sehen, dass die resultierende Graphenstruktur die ursprüngliche Merge-Struktur bewahrt: $ git show-graph * 061f9fd... N * f669404... Merge von Zweig 'dev2' in dev |\ | * c386cfc... Z * | 38ab25e... P |/ * b93ad42... Y * 65be7f1... X * e3b9e22... D * f2b96c4... C * 8619681... B * d6fba18... A
Und das sieht so aus wie der Graph in Abbildung 10-21. A
B
C
D
master
X
Y
Z
P
M
N
dev
Abbildung 10-21: git rebase merge nach der Linearisierung
Einige der Prinzipien für das Beantworten der Frage nach rebase im Vergleich mit merge gelten gleichermaßen für Ihr eigenes Repository wie für ein verteiltes oder Mehr-Repository-Szenario. In Kapitel 12 erfahren Sie mehr über die zusätzlichen Implikationen für Entwickler, die andere Repositories benutzen.
Rebasing von Commits | 191
Je nach Entwicklungsstil und eigentlichem Ziel mag es akzeptabel sein oder nicht, wenn der Entwicklungsverlauf Ihres Originalzweigs beim Rebasing linearisiert wird. Wenn Sie bereits Commits auf demjenigen Zweig veröffentlicht oder weitergegeben haben, dessen Basis Sie ändern wollen, dann bedenken Sie die negativen Auswirkungen auf andere. Sollte die Rebase-Operation nicht die richtige Wahl sein und Sie die Änderungen auf dem Zweig weiterhin brauchen, dann ist ein Merge möglicherweise die bessere Möglichkeit. Merken Sie sich folgende wichtige Konzepte: • Beim Rebasing werden Commits als neue Commits umgeschrieben. • Alte Commits, die nicht mehr erreichbar sind, verschwinden. • Alle Benutzer von alten Commits aus der Zeit vor dem Rebasing stranden irgendwo. • Wenn Sie einen Zweig haben, der ein Commit aus der Zeit vor dem Rebasing verwendet, müssen Sie wahrscheinlich auch dessen Basis ändern, also ein Rebasing durchführen. • Wenn es in einem anderen Repository Benutzer eines Commit aus der Zeit vor dem Rebasing gibt, dann besitzen diese weiterhin eine Kopie dieses Commit, auch wenn es in Ihrem Repository verschoben wurde; sie müssen nun ebenfalls ihren CommitVerlauf anpassen.
192 | Kapitel 10: Commits verändern
Kapitel 11
KAPITEL 11
Entfernte Repositories
Bisher haben Sie fast ausschließlich in einem einzigen lokalen Repository gearbeitet. Nun ist es an der Zeit, die vielgepriesenen verteilten Eigenschaften von Git zu erkunden und zu lernen, wie man mit anderen Entwicklern über freigegebene Repositories zusammenarbeitet. Das Arbeiten mit mehreren und entfernten Repositories erweitert unser Git-Vokabular natürlich um einige neue Begriffe. Ein Klon ist eine Kopie eines Repository. Ein Klon enthält alle Objekte aus dem Original; das bedeutet, dass jeder Klon ein unabhängiges und autonomes Repository und einen echten, symmetrischen Partner des Originals bildet. Mit einem Klon kann jeder Entwickler lokal und unabhängig arbeiten, ohne dass er auf Zentralisierung, Polls oder Sperren angewiesen ist. Letztendlich ist es das Klonen, das es Git erlaubt, große und verteilte Projekte zu verarbeiten. Im Prinzip sind separate Repositories dann sinnvoll, ... • ... wenn ein Entwickler autonom arbeitet. • ... wenn Entwickler durch ein WAN (Wide Area Network, Weitverkehrsnetzwerk) getrennt sind. Eine Gruppe von Entwicklern am gleichen Ort könnte sich ein lokales Repository teilen. • ... wenn abzusehen ist, dass sich für ein Projekt völlig verschiedene Entwicklungswege ergeben. Der normale Verzweigungs- und Merging-Mechanismus, der in den vorangegangenen Kapiteln demonstriert wurde, kommt zwar mit beliebigen Mengen getrennter Entwicklungen zurecht, die daraus resultierende Komplexität könnte jedoch mehr Ärger verursachen, als sie wert ist. Separate Entwicklungswege können dagegen getrennte Repositories benutzen, die man bei Bedarf wieder zusammenführt. Das Klonen eines Repository ist nur der erste Schritt beim gemeinsamen Benutzen des Codes. Sie müssen die Repositories außerdem miteinander in Beziehung setzen, um
| 193
Wege zum Austauschen der Daten einzurichten. Git baut diese Repository-Verbindungen über Remotes auf. Ein Remote ist eine Referenz oder ein Handle auf ein anderes Repository. Man verwendet ein Remote als Kurznamen für eine ansonsten lange und komplizierte Git-URL. Man kann eine beliebige Anzahl von Remotes in einem Repository definieren und auf diese Weise ein raffiniertes Netzwerk gemeinsam benutzter Repositories erzeugen. Sobald ein Remote etabliert ist, kann Git die Daten zwischen den einzelnen Repositories mittels Push- oder Pull-Mechanismen übertragen. Es ist z.B. üblich, gelegentlich Daten von einem Original-Repository an dessen Klon zu übertragen, um den Klon synchron zu halten. Man kann auch ein Remote herstellen, um Daten vom Klon an das Original zu übertragen. Oder man konfiguriert Original und Klon so, dass sie hin und her Informationen austauschen. Um die Daten von anderen Repositories im Blick zu behalten, verwendet Git TrackingZweige. Jeder Tracking-Zweig in Ihrem Repository ist ein lokaler Zweig, der als Proxy, also als Stellvertreter, für einen bestimmten Zweig in einem entfernten Repository dient. Schließlich können Sie Ihr Repository anderen zur Verfügung stellen. Git bezeichnet das im Allgemeinen als Veröffentlichen eines Repository und hält dafür verschiedene Techniken bereit. In diesem Kapitel präsentieren wir Ihnen Beispiele und Techniken, um mehrere Repositories gemeinsam zu benutzen, zu überwachen und Daten zwischen ihnen auszutauschen.
Repository-Konzepte Bare- und Entwicklungs-Repositories Ein Git-Repository ist entweder ein Bare- oder ein Entwicklungs- (Nonbare-)Repository. Ein Entwicklungs-Repository wird für die normale, tägliche Entwicklung verwendet. Es kennt den aktuellen Zweig und liefert eine ausgecheckte Kopie des aktuellen Zweigs in ein Arbeitsverzeichnis. Alle bisher in diesem Buch erwähnten Repositories waren Entwicklungs-Repositories. Im Gegensatz dazu besitzt ein Bare-Repository kein Arbeitsverzeichnis und sollte nicht für die normale Entwicklung benutzt werden. Ein Bare-Repository hat auch keine Ahnung von einem ausgecheckten Zweig. Sie dürfen daher keine direkten Commits in ein Bare-Repository vornehmen. Ein Bare-Repository scheint keinen sichtbaren Nutzen zu haben, allerdings spielt es eine entscheidende Rolle für die gemeinsame Entwicklung. Andere Entwickler holen mit clone und fetch Daten vom Bare-Repository und schieben mit push aktualisierte Daten darauf.
194 | Kapitel 11: Entfernte Repositories
Wenn Sie den Befehl git clone mit der Option --bare aufrufen, erzeugt Git ein BareRepository; ansonsten wird ein Entwicklungs-Repository angelegt. Git aktiviert standardmäßig ein Reflog (eine Aufzeichnung der Änderungen an den Refs) in Entwicklungs-Repositories, nicht jedoch in Bare-Repositories. Das hat wiederum damit zu tun, dass die Entwicklung in der ersten Form des Repository stattfindet und nicht in der zweiten. Mit der gleichen Begründung werden in einem Bare-Repository keine Remotes erzeugt. Ein Repository, in das die Entwickler ihre Änderungen schieben sollen, muss als BareRepository eingerichtet werden. Im Prinzip handelt es sich hier um einen Sonderfall der allgemeineren Verfahrensweise, dass ein veröffentlichtes Repository ein Bare-Repository sein sollte.
Repository-Klone Der Befehl git clone erzeugt auf der Grundlage eines angegebenen Originals ein neues Git-Repository. Git muss nicht alle Informationen aus dem Original in den Klon kopieren. Stattdessen ignoriert Git Informationen, die sich nur auf das Original-Repository beziehen. Bei der normalen Anwendung von git clone werden aus den lokalen Entwicklungszweigen des Original-Repository, die in refs/heads/ gespeichert sind, die entfernten TrackingZweige im neuen Klon unter refs/remotes/. Entfernte Tracking-Zweige innerhalb von refs/ remotes/ des Original-Repository werden nicht geklont. (Der Klon muss nicht wissen, was das Original gegebenenfalls beobachtet.) Tags aus dem Original-Repository werden in den Klon kopiert, genau wie alle Objekte, die von den kopierten Refs aus erreichbar sind. Allerdings werden Repository-spezifische Informationen wie Hooks (siehe Kapitel 14), Konfigurationsdateien, das Reflog und das Lager des Original-Repository nicht im Klon reproduziert. »Eine Kopie Ihres Repository herstellen« auf Seite 28 hat gezeigt, wie Sie mit git clone eine Kopie des public_html-Repository anlegen können: $ git clone public_html meine_website
public_html wird hier als das ursprüngliche, »entfernte« Repository betrachtet. Der neue, resultierende Klon ist meine_website. git clone kann gleichermaßen dazu verwendet werden, eine Kopie eines Repository von einem Standort im Netzwerk zu klonen: # Alles auf einer Zeile ... $ git clone \ git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
Standardmäßig besitzt jeder neue Klon über ein Remote namens origin einen »Verweis« zurück auf sein Eltern-Repository. Allerdings weiß das Original-Repository nichts von
Repository-Konzepte
| 195
einem Klon – und besitzt auch keinen Verweis auf einen solchen. Es ist eine einseitige Beziehung.1 Der Name »origin« ist überhaupt nichts Besonderes. Wenn Sie ihn nicht benutzen wollen, dann geben Sie während der Klonoperation einfach mit der Option --origin Name einen anderen Namen an. Git konfiguriert außerdem das vorgegebene origin-Remote mit einer Standard-fetchReferenzspezifikation: fetch = +refs/heads/*:refs/remotes/origin/*
Durch das Einrichten dieser Referenzspezifikation (Refspec) wird darauf vorbereitet, dass Sie Ihr lokales Repository weiterhin aktualisieren wollen, indem Sie die Änderungen vom Ursprungs-Repository holen. In diesem Fall sind die Zweige des entfernten Repository im Klon unter Zweignamen verfügbar, die mit origin/ beginnen, wie etwa origin/master, origin/dev oder origin/maint.
Remotes Das Repository, an dem Sie momentan arbeiten, wird als lokales oder aktuelles Repository bezeichnet, ein Repository, mit dem Sie Dateien austauschen, heißt entferntes (remote) Repository. Dieser zweite Begriff ist allerdings ein wenig irreführend, weil dieses Repository sich nicht zwingend an einer anderen Stelle bzw. auf einer anderen Maschine befinden muss; es könnte sogar ein anderes Repository auf einem lokalen Dateisystem sein. Git verwendet sowohl den Remote- als auch den Tracking-Zweig, um die »Verbindung« zu einem anderen Repository zu referenzieren und zu unterstützen. Das Remote bietet einen freundlichen Namen für das Repository und kann anstelle der eigentlichen Repository-URL benutzt werden. Ein Remote bildet darüber hinaus einen Teil der Namensbasis für die Tracking-Zweige für dieses Repository. Verwenden Sie den Befehl git remote, um ein Remote zu erzeugen, zu entfernen, zu manipulieren und anzuschauen. Alle Remotes, die Sie einführen, werden in der Datei .git/ config aufgezeichnet und können mit git config verändert werden. Neben git clone gibt es weitere Git-Befehle für entfernte Repositories: git fetch
Holt Objekte und deren Metadaten von einem entfernten Repository. git pull
Wie git fetch, überführt aber außerdem die Änderungen in einen dazugehörigen Zweig.
1
Natürlich kann später mit dem Befehl git remote eine bidirektionale Remote-Verbindung eingerichtet werden.
196 | Kapitel 11: Entfernte Repositories
git push
Überträgt Objekte und deren Metadaten auf ein entferntes Repository. git ls-remote
Zeigt Referenzen innerhalb eines Remote.
Tracking-Zweige Nachdem Sie ein Repository geklont haben, können Sie sich über die Änderungen im ursprünglichen Quell-Repository auf dem Laufenden halten, selbst wenn Sie lokale Commits ausführen und lokale Zweige anlegen. Sie können sogar einen lokalen Zweig namens test erzeugen, ohne zu merken, dass ein Entwickler, der in dem Quell- oder Upstream-Repository arbeitet, ebenfalls einen Zweig namens test angelegt hat. Git erlaubt Ihnen, über einen Tracking-Zweig die Entwicklung in beiden test-Zweigen zu verfolgen. Während einer Klonoperation erzeugt Git für jeden Topic-Zweig im Original-Repository einen entfernten Tracking-Zweig. Das lokale Repository verwendet seine TrackingZweige zum Überwachen oder Nachvollziehen von Änderungen im entfernten Repository. Die entfernten Tracking-Zweige werden in einen neuen, separaten Namensraum eingebracht, der speziell auf das geklonte Remote zugeschnitten ist. Sie erinnern Sie möglicherweise aus »Refs und Symrefs« auf Seite 74«, dass ein lokaler Topic-Zweig, den Sie dev nennen, in Wirklichkeit refs/heads/ dev heißt. Entfernte Tracking-Zweige verbleiben gleichermaßen im Namensraum refs/remotes/. Das heißt, dass der entfernte Tracking-Zweig origin/master eigentlich refs/remotes/origin/master heißt.
Da Tracking-Zweige in ihre eigenen Namensräume gelegt werden, gibt es eine klare Trennung zwischen Zweigen, die von Ihnen in einem Repository erzeugt wurden (TopicZweige) und solchen Zweigen, die tatsächlich auf einem anderen, entfernten Repository basieren (Tracking-Zweige). Die getrennten Namensräume sind einfach eine Konvention und haben sich bewährt. Sie sollen Ihnen dabei helfen, versehentliche Konflikte zu vermeiden. Alle Operationen, die Sie auf einem normalen Topic-Zweig ausführen können, sind auch auf einem Tracking-Zweig möglich. Sie müssen jedoch einige Einschränkungen und Hinweise beachten. Da Tracking-Zweige ausschließlich dazu dienen, die Änderungen aus einem anderen Repository zu überwachen, sollten Sie keine Merges oder Commits in einem TrackingZweig ausführen. Dadurch wäre der Tracking-Zweig nicht mehr synchron mit dem entfernten Repository. Was noch schlimmer wäre: Alle künftigen Updates vom entfernten Repository aus würden wahrscheinlich Merges erfordern, wodurch sich die Verwaltung des Klons zunehmend erschweren würde. Die richtige Verwaltung von Tracking-Zweigen wird weiter unten in diesem Kapitel ausführlicher behandelt.
Repository-Konzepte
| 197
Um zu bestätigen, dass es nicht gut ist, direkt auf einem Tracking-Zweig Commits auszuführen, sollten Sie wissen, dass das Auschecken eines Trackings-Zweigs einen abgesonderten HEAD verursacht. Wie in »Abgesonderte HEAD-Zweige« auf Seite 111 erwähnt, ist ein abgesonderter HEAD im Prinzip ein anonymer Zweigname. Es ist möglich, Commits an einem abgesonderten HEAD auszuführen, Sie sollten dann jedoch nicht Ihren TrackingZweig-HEAD mit lokalen Commits aktualisieren, damit Sie später keinen Schaden erleiden, wenn Sie neue Updates von diesem Remote holen. Falls Sie merken, dass Sie solche Commits brauchen, erzeugen Sie mit git checkout -b mein_Zweig einen neuen lokalen Zweig, in dem Sie Ihre Änderungen entwickeln.
Auf andere Repositories verweisen Um Ihr Repository mit einem anderen Repository zu koordinieren, definieren Sie ein Remote. Ein Remote ist ein benanntes Objekt, das in der Konfigurationsdatei eines Repository gespeichert wird. Es besteht aus zwei unterschiedlichen Teilen. Der erste Teil eines Remote gibt in Form einer URL den Namen des anderen Repository an. Der zweite Teil, der als Refspec bezeichnet wird, legt fest, wie ein Ref (das normalerweise einen Zweig repräsentiert) vom Namensraum des einen Repository in den Namensraum des anderen Repository abgebildet werden soll. Schauen wir uns die einzelnen Komponenten genauer an.
Auf entfernte Repositories verweisen Git unterstützt mehrere Formen des Uniform Resource Locator (URL), mit dem man entfernte Repositories benennen kann. Diese Formen geben sowohl das Zugriffsprotokoll als auch den Ort oder die Adresse der Daten an. Technisch gesehen sind Gits URL-Formen weder URLs noch URIs, da beide nicht völlig mit RFC 1738 bzw. RFC 2396 konform gehen. Da sie sich jedoch so hervorragend zum Verweisen auf Git-Repositories eignen, werden die Git-Varianten normalerweise als GitURLs bezeichnet. Darüber hinaus nutzt auch die Datei .git/config den Namen url. Wie Sie gesehen haben, bezieht sich die einfachste Form der Git-URL auf ein Repository auf einem lokalen Dateisystem, sei es ein echtes physisches Dateisystem oder ein virtuelles, das lokal über das Network File System (NFS) gemountet ist. Es gibt zwei Möglichkeiten, die URL darzustellen: /Pfad/auf/repo.git file:///Pfad/auf/repo.git
Im Prinzip sind diese beiden Formate identisch, es gibt allerdings einen kleinen, aber wichtigen Unterschied zwischen ihnen: Die erste Form verwendet harte Links innerhalb des Dateisystems, um auf diese Weise exakt dieselben Objekte zwischen dem aktuellen und dem entfernten Repository gemeinsam zu nutzen, während die zweite Form die
198 | Kapitel 11: Entfernte Repositories
Objekte kopiert, anstatt sie direkt gemeinsam zu nutzen. Um Probleme zu vermeiden, die im Zusammenhang mit gemeinsam genutzten Repositories auftreten können, wird die Form file:// empfohlen. Die anderen Formen der Git-URL verweisen auf Repositories auf entfernten Systemen. Wenn man ein wirklich entferntes Repository hat, dessen Daten über das Netzwerk geholt werden müssen, wird die effizienteste Form der Datenübertragung oft als natives Git-Protokoll bezeichnet. Damit ist das Git-eigene Protokoll gemeint, das intern von Git verwendet wird, um die Daten zu übertragen. Hier sehen Sie Beispiele für die entsprechenden URLs: git://example.com/Pfad/auf/repo.git git://example.com/~Benutzer/Pfad/auf/repo.git
Diese Formen werden vom git-daemon benutzt, um Repositories für anonyme Lesevorgänge zu veröffentlichen. Sie können mithilfe dieser URL-Formen sowohl klonen als auch Daten holen. Clients, die diese Formate benutzen, werden nicht authentifiziert; es wird auch kein Passwort abgefragt. Das heißt: Während ein ~Benutzer-Format verwendet werden kann, um auf das Home-Verzeichnis eines Benutzers zu verweisen, besitzt ein reines ~ keinen Kontext für eine Erweiterung; es gibt keinen authentifizierten Benutzer, dessen Home-Verzeichnis verwendet werden kann. Außerdem funktioniert die ~Benutzer-Form nur, wenn die Serverseite sie mit der Option --user-path erlaubt. Für sichere, authentifizierte Verbindungen kann das Git-eigene Protokoll über eine SSHVerbindung getunnelt werden. Dabei kommen folgende URL-Templates zum Einsatz: ssh:///[Benutzer@]example.com[:port]/Pfad/auf/repo.git ssh://[Benutzer@]example.com/Pfad/auf/repo.git ssh://[Benutzer@]example.com/~Benutzer2/Pfad/auf/repo.git ssh://[Benutzer@]example.com/~/Pfad/auf/repo.git
Die dritte Form bietet die Möglichkeit zweier unterschiedlicher Benutzernamen. Der erste Benutzer ist derjenige, unter dem die Sitzung authentifiziert wird; auf das HomeVerzeichnis des zweiten Benutzers wird dann zugegriffen. Eine Anwendung einfacher SSH-URLs finden Sie in »Repositories mit kontrolliertem Zugriff« auf Seite 227. Git unterstützt darüber hinaus eine URL-Form mit einer scp-artigen Syntax. Sie ist identisch mit den SSH-Formen, bietet aber keine Möglichkeit, einen Port-Parameter anzugeben: [Benutzer@]example.com:/Pfad/auf/repo.git [Benutzer@]example.com:~Benutzer/Pfad/auf/repo.git [Benutzer@]example.com:Pfad/auf/repo.git
Die HTTP- und HTTPS-URL-Varianten werden ebenfalls vollständig unterstützt, sind allerdings nicht so effizient wie das Git-eigene Protokoll: http://example.com/Pfad/auf/repo.git https://example.com/Pfad/auf/repo.git
Auf andere Repositories verweisen | 199
Schließlich kann auch das rsync-Protokoll angegeben werden: rsync://example.com/Pfad/auf/repo.git
Die Verwendung von rsync wird nicht empfohlen, da es den anderen Optionen unterlegen ist. Falls es absolut notwendig ist, dann sollte es nur für einen ersten Klon benutzt werden, wobei der Verweis auf das entfernte Repository anschließend in einen der anderen Mechanismen geändert werden sollte. Benutzt man das rsync-Protokoll für spätere Updates, kann es zum Verlust von lokal erzeugten Daten kommen.
Die Refspec Im Abschnitt »Refs und Symrefs« auf Seite 74 wurde erläutert, wie die Referenz (kurz: Ref) ein bestimmtes Commit innerhalb des Verlaufs des Repository anspricht. Normalerweise handelt es sich bei einem Ref um den Namen eines Zweigs. Eine Refspec bildet die Zweignamen im entfernten Repository auf die Zweignamen innerhalb Ihres lokalen Repository ab. Da eine Refspec gleichzeitig Zweige aus dem lokalen Repository und dem entfernten Repository benennen muss, sind in einer Refspec komplette Zweignamen üblich und werden oft auch gefordert. In einer Refspec findet man typischerweise die Namen der Entwicklungszweige mit dem Präfix refs/heads/ und die Namen der Tracking-Zweige mit dem Präfix refs/remotes/. Die Syntax einer Refspec lautet [+]Quelle:Ziel
Sie besteht vorrangig aus einer Quellreferenz, einem Doppelpunkt (:) und einer Zielreferenz. Dem ganzen Format kann optional ein Pluszeichen (+) voranstehen. Falls das Pluszeichen vorhanden ist, zeigt es an, dass während der Übertragung die normale schnelle Sicherheitsüberprüfung nicht durchgeführt wird. Ein Asterisk (*) erlaubt außerdem eine beschränkte Form von Wildcard-Filterung auf den Zweignamen. In manchen Anwendungsfällen ist der Quellverweis optional, in anderen sind der Doppelpunkt und der Zielverweis optional. Der Trick beim Benutzen einer Refspec besteht darin, den Datenfluss zu verstehen, der damit festgelegt wird. Die Refspec selbst gibt immer Quelle:Ziel an, allerdings hängen die Rollen von Quelle und Ziel von der ausgeführten Git-Operation ab. Diese Beziehung wird in Tabelle 11-1 zusammengefasst. Tabelle 11-1: Refspec-Datenfluss Operation
Quelle
Ziel
push
Lokale Ref, die geschoben wird
Entfernte Ref, die aktualisiert wird
fetch
Entfernte Ref, die abgerufen wird
Lokale Ref, die aktualisiert wird
200 | Kapitel 11: Entfernte Repositories
Ein typischer git fetch-Befehl verwendet so eine Refspec: +refs/heads/*:refs/remotes/remote/*
Diese Refspec kann folgendermaßen beschrieben werden: Alle Quellzweige aus einem entfernten Repository im Namensraum refs/heads/ werden (i) mittels eines Namen, der aus dem Remote-Namen konstruiert wird, auf Ihr lokales Repository abgebildet und (ii) im Namensraum refs/remotes/remote platziert.
Wegen des Asterisk gilt diese Refspec für mehrere Zweige, die im refs/heads/* des Remote zu finden sind. Genau diese Spezifikation sorgt dafür, dass die Topic-Zweige des Remote auf die lokalen Topic-Zweige abgebildet werden und in Teilnamen auf der Grundlage des Remote-Namens unterteilt werden. Es ist zwar nicht zwingend erforderlich, aber eine Konvention und verbreitete Praxis, die Zweignamen für ein bestimmtes Remote unter refs/remotes/remote/* abzulegen. Benutzen Sie git show-ref, um die Referenzen innerhalb Ihres aktuellen Repository aufzulisten. Mit git ls-remote Repository wiederum listen Sie die Referenzen in einem entfernten Repository auf.
Refspecs werden sowohl von git fetch als auch von git push benutzt. Da der erste Schritt von git pull fetch ist, gelten die fetch-Refspecs gleichermaßen für git pull. Auf den git fetch- und git push-Kommandozeilen können mehrere Refspecs angegeben werden. Innerhalb einer Remote-Definition können mehrere fetch-Refspecs, mehrere Push-Refspecs oder eine Kombination aus beiden angegeben werden. Sie sollten auf einem Tracking-Zweig, der auf der rechten Seite einer pulloder fetch-Refspec angegeben ist, keine Commits oder Merges ausführen. Diese Refs werden als Tracking-Zweige benutzt.
Während einer git push-Operation werden Sie typischerweise die Änderungen bereitstellen und veröffentlichen, die Sie an Ihren lokalen Topic-Zweigen vorgenommen haben. Damit andere Ihre Änderungen in dem entfernten Repository finden, nachdem Sie sie hochgeladen haben, müssen Ihre Änderungen in diesem Repository als Topic-Zweige auftauchen. Das bedeutet, dass während eines typischen git push-Befehls die Quellzweige aus Ihrem Repository mithilfe einer solchen Refspec an das entfernte Repository gesandt werden: +refs/heads/*:refs/heads/*
Diese Refspec kann folgendermaßen umschrieben werden: Nimm aus dem lokalen Repository jeden Zweignamen, der unter dem Quellnamensraum refs/heads/ zu finden ist, und platziere ihn in einen ebenso benannten, passenden Zweig unter dem Zielnamensraum refs/heads/ im entfernten Repository.
Auf andere Repositories verweisen | 201
Das erste refs/heads/ bezieht sich auf Ihr lokales Repository (weil Sie einen Push ausführen), das zweite verweist auf das entfernte Repository. Die Asteriske sorgen dafür, dass alle Zweige repliziert werden.
Ein Beispiel mit entfernten Repositories Sie haben nun die Grundlage für das verteilte Arbeiten mit Git gelegt. Ohne Beschränkung der Allgemeinheit und um die Ausführung der Beispiele auf Ihrem System zu erleichtern, zeigt dieser Abschnitt mehrere Repositories auf einer physischen Maschine. In der Realität würden diese sich möglicherweise auf unterschiedlichen Hosts im Internet befinden. Es können andere Formen der Spezifikation entfernter URLs verwendet werden, da für Repositories auf physisch getrennten Maschinen die gleichen Mechanismen gelten. Wir wollen einen häufig vorkommenden Anwendungsfall von Git untersuchen. Zur Verdeutlichung richten wir ein Repository ein, das alle Entwickler als autoritativ ansehen, obwohl es sich technisch nicht von anderen Repositories unterscheidet. Mit anderen Worten: Die Autorität liegt in einer Übereinkunft über die Behandlung des Repository begründet, nicht in irgendwelchen technischen oder sicherheitsbezogenen Maßnahmen. Diese vereinbarte autoritative Kopie wird oft in einem speziellen Verzeichnis abgelegt, dem sogenannten Depot. (Vermeiden Sie es, Begriffe wie »Master« oder »Repository« zu verwenden, wenn Sie das Depot meinen, weil diese in Git etwas anderes bedeuten.) Oft gibt es gute Gründe, ein Depot einzurichten, z.B. könnte Ihre Einrichtung dadurch zuverlässig und professionell die Dateisysteme eines großen Servers sichern. Sie ermutigen Ihre Mitarbeiter, alles in die Hauptkopie im Depot einzuchecken, um verheerende Verluste zu vermeiden. Das Depot bildet den entfernten Ursprung (remote origin) für alle Entwickler. In den folgenden Abschnitten erfahren Sie, wie man ein erstes Repository in dem Depot ablegt, die Entwicklungs-Repositories aus dem Depot heraus klont, Entwicklungsarbeit in ihnen durchführt und sie dann wieder mit dem Depot synchronisiert. Um die parallele Entwicklung in diesem Repository zu demonstrieren, wird ein zweiter Entwickler es klonen, mit seinem Repository arbeiten und dann seine Änderungen wieder in das Depot schieben, damit auch die anderen damit arbeiten können.
Ein autoritatives Repository einrichten Sie können Ihr autoritatives Depot an einer beliebigen Stelle im Dateisystem ablegen; für dieses Beispiel benutzen wir /tmp/Depot. Direkt im Verzeichnis /tmp/Depot oder in einem seiner Repositories darf keine direkte Entwicklungsarbeit stattfinden. Stattdessen soll die individuelle Arbeit in einem lokalen Klon erfolgen. Der erste Schritt besteht darin, in /tmp/Depot ein Ausgangs-Repository zu schaffen. Nehmen Sie an, Sie wollen an einer Website arbeiten, deren Inhalt bereits als Git-Repository
202 | Kapitel 11: Entfernte Repositories
in ~/public_html vorliegt. Erzeugen Sie dazu eine Kopie des ~/public_html-Repository und platzieren Sie diese in /tmp/Depot/public_html: # Angenommen, ~/public_html ist bereits ein Git-Repository $ cd /tmp/Depot/ $ git clone --bare ~/public_html public_html.git Initialized empty Git repository in /tmp/Depot/public_html.git/
Dieser clone-Befehl kopiert das entfernte Git-Repository von ~/public_html in das aktuelle Arbeitsverzeichnis /tmp/Depot. Das letzte Argument gibt dem Repository einen neuen Namen: public_html.git. Es gilt als Konvention, dass Bare-Repositories mit dem Suffix .git versehen werden. Das ist nicht zwingend erforderlich, hat sich aber als praktisch erwiesen. Im ursprünglichen Entwicklungs-Repository wurde auf der obersten Ebene ein vollständiger Satz von Projektdateien ausgecheckt, der Objektspeicher und alle Konfigurationsdateien befinden sich im Unterverzeichnis .git: $ cd $ ls ./ ../
~/public_html/ -aF fuzzy.txt index.html .git/ poem.html
$ ls -aF .git ./ ../ branches/ COMMIT_EDITMSG
config description FETCH_HEAD HEAD
techinfo.txt
hooks/ index info/ logs/
objects/ ORIG_HEAD packed-refs refs/
Da ein Bare-Repository kein Arbeitsverzeichnis besitzt, haben seine Dateien einen einfacheren Aufbau: $ cd /tmp/Depot/ $ ls -aF public_html.git ./ branches/ description ../ config HEAD
hooks/ info/
objects/ packed-refs
refs/
Sie können nun dieses Bare-Repository /tmp/Depot/public_html.git als autoritative Version benutzen. Da Sie während dieser Klonoperation die Option --bare verwendet haben, hat Git nicht das normale, standardmäßige origin-Remote eingeführt. Hier ist die Konfiguration in dem neuen Bare-Repository: # In /tmp/Depot/public_html.git $ cat config [core] repositoryformatversion = 0 filemode = true bare = true
Ein Beispiel mit entfernten Repositories
| 203
Erzeugen Sie Ihr eigenes origin-Remote Sie haben nun zwei Repositories, die praktisch identisch sind, mit der Ausnahme, dass das Ausgangs-Repository ein Arbeitsverzeichnis besitzt und der Bare-Klon nicht. Da überdies das ~/public_html-Repository in Ihrem Home-Verzeichnis mittels git init erzeugt wurde und nicht über ein clone, fehlt ihm ein origin. Um genau zu sein, wurde dafür überhaupt kein Remote konfiguriert. Es ist allerdings nicht schwierig, eines hinzuzufügen. Und es wird gebraucht, falls das Ziel darin besteht, mehr Entwicklung in Ihrem Ausgangs-Repository durchzuführen und dann diese Entwicklung in das neu etablierte, autoritative Repository im Depot zu schieben. In gewissem Sinne müssen Sie Ihr Ausgangs-Repository manuell in einen abgeleiteten Klon umwandeln. Ein Entwickler, der aus dem Depot klont, erhält automatisch ein origin-Remote. Würden Sie es jetzt z.B. einfach tun, dann würde auch für Sie ein solches Remote eingerichtet. Der Befehl zum Manipulieren von Remotes ist git remote. Diese Operation bringt einige neue Einstellungen in die Datei .git/config ein: $ cd ~/public_html $ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true $ git remote add origin /tmp/Depot/public_html $ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [remote "origin"] url = /tmp/Depot/public_html fetch = +refs/heads/*:refs/remotes/origin/*
git remote hat hier einen neuen remote-Abschnitt namens origin in unsere Konfiguration eingeführt. Der Name origin ist nicht magisch oder speziell. Sie hätten auch jeden ande-
ren Namen nehmen können, allerdings wird das Remote, das zurück auf das Basis-Repository verweist, gemäß Konvention origin genannt. Das Remote stellt eine Verbindung von Ihrem aktuellen Repository zu dem gefundenen entfernten Repository her, in diesem Fall unter /tmp/Depot/public_html.git, wie im urlWert aufgezeichnet. Der Name origin kann als Kurzreferenz für das entfernte Repository
204 | Kapitel 11: Entfernte Repositories
verwendet werden, das im Depot zu finden ist. Beachten Sie, dass außerdem eine Standard-Fetch-Refspec hinzugefügt wurde, die den Konventionen zur Zuordnung von Zweignamen entspricht. Wir wollen das Einrichten des origin-Remote abschließen, indem wir neue TrackingZweige im Original-Repository herstellen, die die Zweige aus dem entfernten Repository repräsentieren. Zunächst gibt es, wie erwartet, nur einen Zweig namens master: # Alle Zweige auflisten $ git branch -a * master
Benutzen Sie nun git remote update: $ git remote update Updating origin From /tmp/Depot/public_html * [new branch] master
-> origin/master
$ git branch -a * master origin/master
Git hat in das Repository einen neuen Zweig namens origin/master eingeführt. Es ist ein Tracking-Zweig innerhalb des origin-Remote. Niemand entwickelt in diesem Zweig. Seine Aufgabe besteht stattdessen darin, die Commits aufzunehmen und zu überwachen, die im master-Zweig des entfernten origin-Repository vorgenommen werden. Sie könnten ihn als Proxy Ihres lokalen Repository für Commits betrachten, die in dem Remote erfolgen; irgendwann einmal können Sie ihn verwenden, um diese Commits in Ihr Repository zu bringen. Die Phrase Updating origin, die von git remote update erzeugt wurde, bedeutet nicht, dass das entfernte Repository aktualisiert wurde. Stattdessen bedeutet sie, dass die Vorstellung des lokalen Repository von dem origin auf der Grundlage der Informationen, die von dem entfernten Repository hereingekommen sind, aktualisiert wurde. Der Befehl git remote update hat jedes Remote innerhalb dieses Repository dazu veranlasst, sich zu aktualisieren, indem es in jedem Repository, das in einem Remote genannt ist, nach neuen Commits sucht und diese dann holt. Anstatt ganz allgemein alle Remotes zu aktualisieren, können Sie die Fetch-Operation auf ein einzelnes Remote beschränken, indem Sie die Option -f angeben, wenn das Remote zu Anfang hinzugefügt wird: git remote add -f origin repository
Jetzt haben Sie es geschafft, Ihr Repository mit dem entfernten Repository in Ihrem Depot zu verbinden.
Ein Beispiel mit entfernten Repositories
| 205
In Ihrem Repository entwickeln Wir wollen ein wenig Entwicklungsarbeit im Repository durchführen und ein weiteres Gedicht, fuzzy.txt, hinzufügen: $ cd ~/public_html $ git show-branch -a [master] Merge von Zweig 'master' aus ../meine_website $ cat fuzzy.txt Fuzzy Wuzzy was a bear Fuzzy Wuzzy had no hair Fuzzy Wuzzy wasn't very fuzzy, Was he? $ git add fuzzy.txt $ git commit Created commit 6f16880: Ein haariges Gedicht hinzufügen 1 files changed, 4 insertions(+), 0 deletions(-) create mode 100644 fuzzy.txt $ git show-branch -a * [master] Ein haariges Gedicht hinzufügen ! [origin/master] Merge von Zweig 'master' aus ../meine_website -* [master] Ein haariges Gedicht hinzufügen -- [origin/master] Merge von Zweig 'master' aus ../meine_website
An dieser Stelle besitzt Ihr Repository ein Commit mehr als das Repository in /tmp/ Depot. Interessanter ist wahrscheinlich, dass Ihr Repository zwei Zweige hat, einen (master) mit dem neuen Commit darin und den anderen (origin/master), der das entfernte Repository überwacht.
Ihre Änderungen auf das entfernte Repository schieben Alle Änderungen, die Sie mit einem Commit bestätigen, liegen zunächst nur lokal in Ihrem Repository vor und sind im entfernten Repository noch nicht vorhanden. Eine bequeme Möglichkeit, das Commit auf das entfernte Repository zu bekommen, bietet der Befehl git push: $ git push origin Counting objects: 4, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 400 bytes, done. Total 3 (delta 0), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. To /tmp/Depot/public_html 0d4ce8a..6f16880 master -> master
Diese Ausgabe bedeutet, dass Git die Änderungen von Ihrem master-Zweig genommen und an das entfernte Repository namens origin geschickt hat. Git hat hier sogar noch
206 | Kapitel 11: Entfernte Repositories
einen weiteren Schritt durchgeführt: Es hat diese Änderungen auch in den origin/masterZweig Ihres Repository eingefügt. Im Prinzip hat Git dafür gesorgt, dass die Änderungen, die sich ursprünglich in Ihrem master-Zweig befanden, an das entfernte Repository geschickt wurden; anschließend hat es sie wieder angefordert, um sie in den origin/master-Tracking-Zweig einzufügen. Eigentlich schickt Git die Änderungen nicht hin und her. Schließlich befinden sich die Commits bereits in Ihrem Repository. Git ist schlau genug, um stattdessen einfach den Tracking-Zweig »vorzuspulen«. Nun zeigen beide lokalen Zweige, master und origin/master, dasselbe Commit innerhalb Ihres Repository: $ git show-branch -a * [master] Ein haariges Gedicht hinzufügen ! [origin/master] Ein haariges Gedicht hinzufügen -*+ [master] Ein haariges Gedicht hinzufügen
Sie können das entfernte Repository testen, um zu verifizieren, dass es ebenfalls aktualisiert wurde. Falls sich – wie in diesem Fall – Ihr entferntes Repository auf einem lokalen Dateisystem befindet, begeben Sie sich dazu einfach in das Depot-Verzeichnis: $ cd /tmp/Depot/public_html.git $ git show-branch [master] Ein haariges Gedicht hinzufügen
Befindet sich das entfernte Repository auf einer anderen Maschine, dann hilft Ihnen ein Plumbing-Befehl dabei, die Zweiginformationen des entfernten Repository zu ermitteln: # In das entfernte Repository gehen und es abfragen $ git ls-remote origin 6f168803f6f1b987dffd5fff77531dcadf7f4b68 6f168803f6f1b987dffd5fff77531dcadf7f4b68
HEAD refs/heads/master
Sie können dann zeigen, dass diese Commit-IDs Ihren aktuellen lokalen Zweigen entsprechen, indem Sie einen Befehl wie git rev-parse HEAD oder git show Commit-id benutzen.
Einen neuen Entwickler hinzufügen Sobald Sie ein autoritatives Repository eingerichtet haben, ist es ganz einfach, einen neuen Entwickler zu einem Projekt hinzuzufügen. Dazu erlauben Sie dem Entwickler einfach, das Repository zu klonen und mit der Arbeit zu beginnen. Wir wollen Bob in das Projekt einführen, indem wir ihm sein eigenes geklontes Repository geben, in dem von nun an gearbeitet wird: $ cd /tmp/bob $ git clone /tmp/Depot/public_html.git Initialized empty Git repository in /tmp/public_html/.git/
Ein Beispiel mit entfernten Repositories
| 207
$ ls public_html $ cd public_html $ ls fuzzy.txt
index.html poem.html techinfo.txt
$ git branch * master $ git log -1 commit 6f168803f6f1b987dffd5fff77531dcadf7f4b68 Author: Jon Loeliger <[email protected]> Date: Sun Sep 14 21:04:44 2008 -0500 Ein haariges Gedicht hinzufügen
Sie erkennen an dem ls sofort, dass der Klon ein Arbeitsverzeichnis mit allen Dateien besitzt, die der Versionskontrolle unterliegen. Das bedeutet, dass Bobs Klon ein Entwicklungs-Repository und kein Bare-Repository ist. Gut. Bob wird ebenfalls etwas entwickeln. Die git log-Ausgabe zeigt Ihnen, dass das neueste Commit in Bobs Repository zur Verfügung steht. Da Bobs Repository außerdem aus einem Eltern-Repository geklont wurde, besitzt es ein vorgegebenes Remote namens origin. Bob kann innerhalb seines Repository mehr Informationen über das origin-Remote finden: $ git remote show origin * remote origin URL: /tmp/Depot/public_html.git Remote branch merged with 'git pull' while on branch master master Tracked remote branch master
Der vollständige Inhalt der Konfigurationsdatei nach einem normalen Klonen zeigt auch das origin-Remote: $ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [remote "origin"] url = /tmp/Depot/public_html.git fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master
Neben dem origin-Remote hat Bob in seinem Repository auch einige Zweige. Mit dem Befehl git branch -a kann er alle Zweige auflisten:
208 | Kapitel 11: Entfernte Repositories
$ git branch -a * master origin/HEAD origin/master
Der master-Zweig ist Bobs Hauptentwicklungszweig. Es ist der normale lokale TopicZweig. Der Zweig origin/master ist ein Tracking-Zweig, mit dem die Commits aus dem master-Zweig des origin-Repository überwacht werden. Der origin/HEAD-Zweig zeigt durch einen symbolischen Namen an, welchen Zweig das Remote als aktiven Zweig betrachtet. Der Asterisk schließlich neben dem Zweignamen master gibt an, dass dies der aktuelle, ausgecheckte Zweig in seinem Repository ist. Bob soll nun einmal ein Commit ausführen, das das haarige Gedicht verändert, und es dann mit push in das Hauptdepot-Repository schieben. Bob ist der Meinung, dass die letzte Zeile des Gedichts »Wuzzy?« lauten sollte, ändert die Zeile entsprechend und bestätigt die Änderung dann: $ git diff diff --git a/fuzzy.txt b/fuzzy.txt index 0d601fa..608ab5b 100644 --- a/fuzzy.txt +++ b/fuzzy.txt @@ -1,4 +1,4 @@ Fuzzy Wuzzy was a bear Fuzzy Wuzzy had no hair Fuzzy Wuzzy wasn't very fuzzy, -Was he? +Wuzzy? $ git commit fuzzy.txt Created commit 3958f68: Das Wortspiel mit dem Namen vervollständigen! 1 files changed, 1 insertions(+), 1 deletions(-)
Um Bobs Entwicklungszyklus abzuschließen, schiebt er die Änderungen wie gehabt mit git push in das Depot. $ git push Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 377 bytes, done. Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. To /tmp/Depot/public_html.git 6f16880..3958f68 master -> master
Repository-Updates erhalten Wir wollen nun annehmen, dass Bob in den Urlaub geht und Sie in der Zwischenzeit weitere Änderungen vornehmen und in das Depot-Repository schieben. Nehmen wir weiterhin an, dass Sie das getan haben, nachdem Bobs letzte Änderungen eingegangen sind.
Ein Beispiel mit entfernten Repositories
| 209
Ihr Commit sieht so aus: $ cd ~/public_html $ git diff diff --git a/index.html b/index.html index 40b00ff..063ac92 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,7 @@ Meine Website ist da! +
+Lesen Sie ein haariges Gedicht! $ git commit -m"Link auf das haarige Gedicht anlegen." index.html Created commit 55c15c8: Link auf das haarige Gedicht anlegen. 1 files changed, 2 insertions(+), 0 deletions(-)
Schieben Sie Ihr Commit mithilfe der Standard-Push-Refspec in das Depot: $ git push Counting objects: 5, done Compressing objects: 100% (3/3), done. Unpacking objects: 100% (3/3), done. Writing objects: 100% (3/3), 348 bytes, done. Total 3 (delta 1), reused 0 (delta 0) To /tmp/Depot/public_html 3958f68..55c15c8 master -> master
Wenn Bob dann zurückkehrt, möchte er seinen Repository-Klon auf den neuesten Stand bringen. Der wichtigste Befehl dafür ist git pull: $ git pull remote: Counting objects: 5, done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 1), reused 0 (delta 0) Unpacking objects: 100% (3/3), done. From /tmp/Depot/public_html 3958f68..55c15c8 master -> origin/master Updating 3958f68..55c15c8 Fast forward index.html | 2 ++ 1 files changed, 2 insertions(+), 0 deletions(-)
Der vollständige git pull-Befehl erlaubt sowohl die Angabe des Repository als auch mehrerer Refspecs: git pull Optionen Repository Refspecs. Wenn das Repository auf der Kommandozeile nicht angegeben wird, also weder als GitURL noch indirekt durch einen Remote-Namen, wird das vorgegebene Remote origin verwendet. Falls Sie auf der Kommandozeile keine Refspec angeben, wird die Fetch-Refspec
210 | Kapitel 11: Entfernte Repositories
des Remote benutzt. Geben Sie ein Repository (direkt oder über ein Remote), aber keine Refspec an, holt Git das HEAD-Ref des Remote.
Die git pull-Operation besteht im Grunde aus zwei Schritten, die jeweils durch einen eigenen Git-Befehl implementiert werden. Und zwar impliziert git pull git fetch, gefolgt von entweder git merge oder git rebase. Der zweite Schritt ist standardmäßig merge, da das fast immer das gewünschte Verhalten ist. Bevor Sie den git pull --rebase-Mechanismus verwenden, sollten Sie die verlaufsändernden Effekte einer Rebasing-Operation (beschrieben in Kapitel 10) und deren Implikationen für andere Benutzer (siehe Kapitel 12) vollständig verstanden haben.
Da pull auch den zweiten merge- oder rebase-Schritt ausführt, werden git push und git pull nicht als Gegensätze betrachtet. Das ist vielmehr bei git push und git fetch der Fall. Sowohl push als auch fetch sind für das Übertragen von Daten zwischen Repositories verantwortlich, allerdings in entgegengesetzte Richtungen. Manchmal möchten Sie vielleicht git fetch und git merge als zwei getrennte Operationen ausführen, z.B. wollen Sie Updates in Ihr Repository holen, um sie zu untersuchen, haben aber nicht vor, diese sofort mittels eines Merge zusammenzuführen. In diesem Fall können Sie das Fetch einfach ausführen und dann andere Operationen auf dem Tracking-Zweig durchführen, etwa git log, git diff oder sogar gitk. Wenn Sie später dann bereit dazu sind (falls überhaupt), können Sie nach Belieben den Merge durchführen. Selbst wenn Sie die Fetch- und Merge-Schritte niemals trennen, führen Sie möglicherweise komplexe Operationen durch, die es erforderlich machen zu wissen, was in den einzelnen Schritten passiert. Schauen wir sie uns deshalb genauer an.
Der fetch-Schritt Im ersten fetch-Schritt sucht Git das entfernte Repository. Da auf der Kommandozeile keine direkte Repository-URL und kein direkter Remote-Name angegeben war, wird der Standardname für ein Remote, origin, angenommen. Die Information für dieses Remote befindet sich in der Konfigurationsdatei: [remote "origin"] url = /tmp/Depot/public_html.git fetch = +refs/heads/*:refs/remotes/origin/*
Git weiß nun, dass es die URL /tmp/Depot/public_html als Quell-Repository benutzen soll. Als Nächstes führt Git eine Protokollverhandlung mit dem Quell-Repository durch, um festzustellen, welche neuen Commits sich in dem entfernten Repository befinden und in Ihrem Repository fehlen. Grundlage dafür ist die Absicht, alle refs/heads/*-Refs zu holen, wie in der Fetch-Refspec angegeben.
Ein Beispiel mit entfernten Repositories
| 211
Sie müssen nicht alle Topic-Zweige mithilfe der refs/heads/*-WildcardForm aus dem entfernten Repository holen. Falls Sie nur einen oder zwei bestimmte Zweige haben wollen, dann listen Sie sie explizit auf: [remote "newdev"] url = /tmp/Depot/public_html.git fetch = +refs/heads/dev:refs/remotes/origin/dev fetch = +refs/heads/stable:refs/remotes/origin/stable
Die Ausgabe, der remote: vorangestellt ist, spiegelt die Verhandlung, die Komprimierung und das Übertragungsprotokoll wider und teilt Ihnen mit, dass neue Commits in Ihr Repository kommen. Git packt die neuen Commits in Ihrem Repository in einen passenden Tracking-Zweig und sagt Ihnen dann, welche Zuordnung es verwendet, um festzustellen, wohin die neuen Commits gehören: From /tmp/Depot/public_html 3958f68..55c15c8 master
-> origin/master
Diese Zeilen zeigen an, dass Git in das entfernte Repository /tmp/Depot/public_html geschaut, dessen master-Zweig genommen, seinen Inhalt wieder in Ihr Repository gebracht und in Ihren origin/master-Zweig gelegt hat. Dieser Vorgang ist das Kernstück der Zweigüberwachung. Die dazugehörigen Commit-IDs sind ebenfalls aufgeführt, für den Fall, dass Sie die Änderungen direkt untersuchen wollen. Damit ist der fetch-Schritt abgeschlossen.
Der merge- oder rebase-Schritt Im zweiten Schritt der pull-Operation führt Git standardmäßig eine merge- oder rebaseOperation durch. In diesem Beispiel überführt Git den Inhalt des Tracking-Zweigs, origin/master, in Ihren master-Zweig. Dabei verwendet es eine besondere Art von Merge namens Fast-Forward. Woher wusste Git aber, wie es diese speziellen Zweige zusammenführen soll? Die Antwort liefert die Konfigurationsdatei: [branch "master"] remote = origin merge = refs/heads/master
Im Prinzip liefert das Git zwei wesentliche Informationen: Wenn master der aktuelle, ausgecheckte Zweig ist, dann benutze origin als StandardRemote, von dem Updates während eines fetch (oder pull) geholt werden. Während des merge-Schrittes von git pull benutze außerdem refs/heads/master vom Remote als Standardzweig für das Zusammenführen in den master-Zweig.
212 | Kapitel 11: Entfernte Repositories
Für Leser, die auf die Details achten, ist der erste Teil dieser Erläuerung der eigentliche Mechanismus, mit dem Git feststellt, dass origin während dieses parameterlosen git pull-Befehls das Remote sein soll. Der Wert des merge-Feldes im branch-Abschnitt der Konfigurationsdatei (branch.*.merge) wird wie der Remote-Teil einer Refspec behandelt und muss einem der Quell-Refs entsprechen, die gerade während des git pull-Befehls geholt wurden. Es ist ein bisschen verworren, aber stellen Sie es sich einfach als einen Hinweis vor, der vom fetch-Schritt an den merge-Schritt eines pull-Befehls gegeben wird. Da der merge-Konfigurationswert nur während git pull gilt, muss eine manuelle Anwendung von git merge an dieser Stelle den Merge-Quellzweig auf der Kommandozeile nennen. Der Zweig ist wahrscheinlich ein Tracking-Zweigname: # Oder vollständig angegeben: refs/remotes/origin/master $ git merge origin/master Updating 3958f68..55c15c8 Fast forward index.html | 2 ++ 1 files changed, 2 insertions(+), 0 deletions(-)
Und damit ist auch der merge-Schritt erledigt. Es gibt leichte semantische Unterschiede zwischen dem Merging-Verhalten von Zweigen, wenn mehrere Refspecs auf der Kommandozeile angegeben werden und wenn sie in einem Remote-Eintrag zu finden sind. Die erste Variante verursacht einen Octopus-Merge, die zweite dagegen nicht. Lesen Sie dazu die git pull-Manpage!
Auf die gleiche Weise, auf die Ihr master-Zweig praktisch die Entwicklung »erweitert«, die auf dem origin/master-Zweig eingebracht wurde, können Sie auf der Grundlage eines beliebigen entfernten Tracking-Zweigs einen neuen Zweig erzeugen und damit diese Entwicklungslinie »erweitern«. Wenn Sie einen neuen Zweig erzeugen, der auf einem entfernten Tracking-Zweig beruht, fügt Git automatisch einen branch-Eintrag hinzu, der anzeigt, dass der Tracking-Zweig mit einem Merge in Ihren neuen Zweig eingebracht werden soll: # Erzeugt mydev auf der Grundlage von origin/master $ git branch mydev origin/master Branch mydev set up to track remote branch refs/remotes/origin/master.
Der gezeigte Befehl veranlasst Git, die folgenden Konfigurationswerte für Sie hinzuzufügen: [branch "mydev"] remote = origin merge = refs/heads/master
Ein Beispiel mit entfernten Repositories
| 213
Sie können wie üblich git config oder einen Texteditor benutzen, um die branch-Einträge in der Konfigurationsdatei zu manipulieren. Wenn der merge-Wert eingerichtet ist, ist Ihr Entwicklungszweig fertig konfiguriert, um Ihre Commits aus diesem Repository unterzubringen und Änderungen aus dem dazugehörigen Tracking-Zweig einzubinden. Falls Sie sich entschließen, anstelle eines Merge ein Rebase durchzuführen, wird Git die Änderungen in Ihrem Topic-Zweig stattdessen auf den neu geholten HEAD des dazugehörigen entfernten Tracking-Zweigs vorwärtsportieren. Die Operation ist identisch mit den Darstellungen in Abbildung 10-12 und Abbildung 10-13. Um rebase zur normalen Operation für einen Zweig zu machen, setzen Sie die Konfigurationsvariable rebase auf true: [branch "mydev"] remote = origin merge = refs/heads/master rebase = true
Entfernte Repository-Operationen in Bildern Wir wollen uns einen Augenblick Zeit nehmen, um zu visualisieren, was während der clone- und pull-Operationen passiert. Einige Bilder sollen außerdem die oft verwirrenden Anwendungen desselben Namens in unterschiedlichen Kontexten klären. Beginnen wir mit dem einfachen Repository aus Abbildung 11-1 als Grundlage für die Diskussion. Repository master
A
B
Abbildung 11-1: Einfaches Repository mit Commits
Wie bei all unseren Commit-Graphen verlaufen die Commits von links nach rechts, und das master-Label zeigt auf den HEAD des Zweigs. Die beiden neuesten Commits sind mit A und B gekennzeichnet. Wir folgen diesen beiden Commits, führen einige weitere Commits ein und beobachten, was passiert.
Ein Repository klonen Ein git clone-Befehl ergibt zwei getrennte Repositories, wie in Abbildung 11-2 zu sehen ist.
214 | Kapitel 11: Entfernte Repositories
Original repository master
A
B
git clone
Cloned repository origin/master
A
B
master
Abbildung 11-2: Geklontes Repository
Dieses Bild verdeutlicht einige wichtige Ergebnisse der Klonoperation: • Alle Commits aus dem Original-Repository werden in Ihren Klon kopiert; Sie könnten nun in Ihrem eigenen Repository ganz leicht auf frühere Projektstadien zugreifen. • Der Entwicklungszweig namens master aus dem Original-Repository wird in Ihren Klon in einem neuen Tracking-Zweig namens origin/master eingeführt. • Innerhalb des neuen Klon-Repository wird der neue origin/master-Zweig so initialisiert, dass er auf das master-HEAD-Commit verweist; in diesem Bild ist das B. • In Ihrem Klon wird ein neuer Entwicklungszweig namens master erzeugt. • Der neue master-Zweig wird so initialisiert, dass er auf origin/HEAD verweist, den aktiven Zweig-HEAD des Original-Repository. Das ist origin/master, sodass er ebenfalls auf genau dasselbe Commit verweist, nämlich B. Nach dem Klonen wählt Git den neuen master-Zweig zum aktuellen Zweig und checkt ihn für Sie aus. Das bedeutet, dass alle Änderungen, die Sie nach einem clone vornehmen, Ihren master beeinflussen, wenn Sie nicht irgendwann die Zweige wechseln. In all diesen Diagrammen werden die Entwicklungszweige sowohl im Original-Repository als auch im abgeleiteten Klon-Repository durch einen leicht schattierten Hintergrund angedeutet und die Tracking-Zweige durch einen dunkler schattierten Hintergrund. Seien Sie sich darüber im Klaren, dass sowohl die Entwicklungs- als auch die Tracking-Zweige privat sind und lokal in ihren jeweiligen Repositories vorliegen. Im Hinblick auf die Git-Implementierung jedoch gehören die heller schattierten Zweig-Label zum Namensraum refs/heads/, während die dunkleren zu refs/remotes/ gehören.
Entfernte Repository-Operationen in Bildern | 215
Alternative Verläufe Nachdem Sie Ihr Entwicklungs-Repository geklont und geholt haben, können zwei getrennte Entwicklungspfade entstehen. Erstens könnten Sie die Entwicklung in Ihrem Repository durchführen und neue Commits in Ihrem master-Zweig vornehmen, wie Abbildung 11-3 zeigt. In diesem Bild erweitert Ihre Entwicklung den master-Zweig um zwei neue Commits, X und Y, die auf B basieren. Origin master
A
B
Yours origin/master
A
B X
Y
master
Abbildung 11-3: Commits in Ihrem Repository
In der Zwischenzeit könnte jeder andere Entwickler, der Zugriff auf das Original-Repository hat, weiter entwickelt und seine Änderungen in dieses Repository geschoben haben. Diese Änderungen werden in Abbildung 11-4 durch die zusätzlichen Commits C und D dargestellt. In dieser Situation sprechen wir davon, dass sich die Verläufe der Repositories bei Commit B geteilt oder verzweigt haben. So wie lokale Verzweigungen innerhalb eines Repository bei einem Commit alternative Verläufe verursachen, können auch ein Repository und sein Klon als Ergebnis separater Aktionen von möglicherweise unterschiedlichen Leuten in verschiedene Verläufe auseinanderdriften. Das ist absolut in Ordnung. Keiner der Verläufe ist »richtiger« als der andere. Der ganze Sinn der Merge-Operation besteht also darin, diese unterschiedlichen Verläufe wieder zusammenzuführen und ineinander aufzulösen. Schauen wir uns an, wie Git das schafft!
216 | Kapitel 11: Entfernte Repositories
Origin master
A
B
C
D
Yours origin/master
A
B X
Y
master
Abbildung 11-4: Commits im Original-Repository
Nicht-Fast-forward-Pushes Wenn Sie in einem Repository-Modell entwickeln, in dem Sie die Möglichkeit haben, Ihre Änderungen mit git push in das origin-Repository zu schieben, werden Sie versucht sein, das zu einem beliebigen Zeitpunkt auch zu tun. Das könnte allerdings Probleme verursachen, falls ein anderer Entwickler zuvor Commits hochgeschoben hat. Dieses Risiko besteht vor allem, wenn Sie ein gemeinsames Repository-Entwicklungsmodell haben, in dem alle Entwickler ihre eigenen Commits und Updates zu beliebigen Zeitpunkten in das gemeinsam genutzte Repository schieben dürfen. Schauen wir uns noch einmal Abbildung 11-3 an. Dort haben Sie die neuen Commits X und Y eingebracht, die auf B beruhen. Wenn Sie Ihre Commits X und Y an dieser Stelle nach oben schieben, wäre das kein Problem. Git würde Ihre Commits in das origin-Repository übertragen und unter B dem Verlauf hinzufügen. Git würde dann auf dem master-Zweig eine besondere Art von Merge-Operation namens Fast-forward durchführen, Ihre Änderungen einbringen und das Ref so anpassen, dass es auf Y verweist. Ein Fast-forward (Vorspulen oder schneller Vorlauf) ist im Prinzip eine einfache lineare Operation zum Vorrücken des Verlaufs. Eingeführt wurde diese Operation im Abschnitt »Degenerierte Merges« auf Seite 151.
Entfernte Repository-Operationen in Bildern | 217
Nehmen Sie andererseits an, dass ein anderer Entwickler bereits einige Commits in das origin-Repository geschoben hat und das Bild daher eher Abbildung 11-4 ähnelt, wenn Sie versuchen, Ihren Verlauf in das origin-Repository zu schieben. Eigentlich versuchen Sie, Ihren Verlauf in das gemeinsam genutzte Repository zu senden, wenn dort bereits ein anderer Verlauf vorliegt. Der origin-Verlauf lässt sich nicht einfach von B weiterrücken. Diese Situation wird als Nicht-Fast-forward-Push-Problem bezeichnet. Wenn Sie Ihren Push versuchen, wird dieser von Git abgewiesen. Git informiert Sie mit einer solchen Meldung über den Konflikt: $ git push To /tmp/Depot/public_html ! [rejected] master -> master (non-fast-forward) error: failed to push some refs to '/tmp/Depot/public_html'
Was tun Sie hier eigentlich gerade? Wollen Sie die Arbeit des anderen Entwicklers überschreiben? Oder wollen Sie beide Verlaufsmengen einbeziehen? Nur zu, falls Sie die gesamten anderen Änderungen überschreiben wollen! Benutzen Sie dazu einfach die Option -f für Ihren git push-Befehl. Wir hoffen nur, dass Sie diesen alternativen Verlauf nicht mehr brauchen!
Meist werden Sie nicht versuchen, den vorhandenen origin-Verlauf auszuradieren, sondern möchten Ihre eigenen Änderungen hinzufügen. In diesem Fall müssen Sie einen Merge der beiden Verläufe in Ihrem Repository durchführen, bevor Sie sie hochschieben.
Den alternativen Verlauf holen Damit Git einen Merge zwischen zwei alternativen Verläufen durchführen kann, müssen beide innerhalb eines Repository in zwei unterschiedlichen Zweigen vorhanden sein. Zweige, die rein lokale Entwicklungszweige darstellen, bilden einen (degenerierten) Sonderfall, weil sie sich schon im selben Repository befinden. Liegen die alternativen Verläufe jedoch aufgrund des Klonens in verschiedenen Repositories vor, muss der entfernte Zweig über eine Fetch-Operation in Ihr Repository geholt werden. Sie können die Operation mithilfe eines direkten git fetch-Befehls oder als Teil eines git pull-Befehls ausführen, das spielt keine Rolle. In beiden Fällen bringt das Fetch die entfernten Commits, in unserem Fall C und D, in Ihr Repository. Die Ergebnisse sehen Sie in Abbildung 11-5. Auf keinen Fall ändert die Einführung des alternativen Verlaufs mit den Commits C und D den Verlauf, der von X und Y dargestellt wird; die beiden alternativen Verläufe existieren nun beide gleichzeitig in Ihrem Repository und formen einen komplexeren Graphen. Ihr Verlauf wird durch Ihren master-Zweig repräsentiert, der entfernte Verlauf durch den origin/master-Tracking-Zweig.
218 | Kapitel 11: Entfernte Repositories
Origin master
A
B
C
D
git fetch
Yours origin/master
C A
B
X
D Y master
Abbildung 11-5: Den alternativen Verlauf holen
Verläufe zusammenführen Nachdem sich nun beide Verläufe in einem Repository befinden, ist nur noch ein Merge des origin/master-Zweigs in den master-Zweig erforderlich, um sie zu vereinen. Die Merge-Operation kann entweder mit einem direkten git merge origin/master-Befehl oder als zweiter Schritt in einer git pull-Anforderung initiiert werden. In beiden Fällen sind die Techniken für die Merge-Operation genau wie in Kapitel 9 beschrieben wurde. Abbildung 11-6 zeigt den Commit-Graphen in Ihrem Repository, nachdem der Merge erfolgreich die beiden Verläufe aus den Commits D und Y in ein neues Commit M integriert hat. Das Ref für origin/master zeigt weiterhin auf D, weil es sich nicht geändert hat, allerdings wurde master auf das Merge-Commit M aktualisiert, um anzuzeigen, dass der Merge in den master-Zweig erfolgt ist; das ist die Stelle, wo das neue Commit durchgeführt wurde.
Merge-Konflikte Gelegentlich kommt es zwischen den alternativen Verläufen zu Konflikten. Ungeachtet des Ergebnisses des Merge ist es zu einem Fetch gekommen. Alle Commits aus dem entfernten Repository sind weiterhin im Tracking-Zweig Ihres Repository vorhanden.
Entfernte Repository-Operationen in Bildern | 219
Origin master
A
B
C
D
Yours origin/master
C A
B
X
D Y
M master
Abbildung 11-6: Verläufe zusammenführen
Sie können sich entscheiden, den Merge normal aufzulösen, wie in Kapitel 9 beschrieben, oder den Merge abzubrechen und Ihren master-Zweig mit dem Befehl git reset --hard ORIG_HEAD auf seinen vorherigen ORIG_HEAD-Zustand zurückzusetzen. In diesem Beispiel würde master auf den vorherigen HEAD-Wert Y verschoben und Ihr Arbeitsverzeichnis entsprechend angepasst. origin/master würde außerdem bei Commit D bleiben. Frischen Sie Ihr Wissen über die Bedeutung von ORIG_HEAD auf, indem Sie noch einmal einen Blick in den Abschnitt »Refs und Symrefs« auf Seite 74 werfen; über seine Anwendung erfahren Sie außerdem etwas in »Einen Merge abbrechen oder neustarten« auf Seite 148.
Einen zusammengeführten Verlauf mit Push verschieben Wenn Sie alle gezeigten Schritte bisher ausgeführt haben, enthält Ihr Repository nun die neuesten Änderungen aus dem origin-Repository sowie aus Ihrem Repository. Umgekehrt ist es jedoch nicht so: Das origin-Repository hat Ihre Änderungen noch nicht erhalten. Sollte Ihr Ziel nur darin bestehen, die neuesten Updates von origin in Ihr Repository zu übernehmen, dann sind Sie nach dem Auflösen des Merge fertig. Andererseits kann ein einfaches git push den vereinten und zusammengeführten Verlauf wieder von Ihrem mas-
220 | Kapitel 11: Entfernte Repositories
ter-Zweig zurück in das origin-Repository bringen. Abbildung 11-7 zeigt die Ergebnisse nach Ihrem git push. Origin master
C A
B
X
D Y
M
Yours origin/master
C A
B
X
D Y
M master
Abbildung 11-7: Zusammengeführte Verläufe nach einem Push
Beachten Sie schließlich, dass das origin-Repository selbst dann mit Ihrer Entwicklung aktualisiert wurde, wenn es anderen Änderungen unterzogen wurde, die von Ihnen erst übernommen werden müssen. Sowohl Ihr Repository als auch das origin-Repository wurden vollständig aktualisiert und sind jetzt synchron.
Entfernte Zweige hinzufügen und löschen Alle neuen Entwicklungen, die Sie in Zweigen in Ihrem lokalen Klon erzeugen, sind im Eltern-Repository erst dann sichtbar, wenn Sie ihre Verbreitung ausdrücklich anfordern. Auch das Löschen eines Zweigs in Ihrem Repository ist eine lokale Änderung; der Zweig wird erst dann aus dem Eltern-Repository entfernt, wenn Sie diese Aktion entsprechend einleiten. In Kapitel 7 haben Sie gelernt, wie Sie mit dem Befehl git branch neue Zweige hinzufügen und vorhandene Zweige aus Ihrem Repository entfernen. Dieser Befehl arbeitet nur in einem lokalen Repository.
Entfernte Zweige hinzufügen und löschen
| 221
Um vergleichbare Operationen zum Hinzufügen und Löschen von Zweigen in einem entfernten Repository durchzuführen, müssen Sie unterschiedliche Formen von Refspecs in einem git push-Befehl angeben. Wie Sie wissen, lautet die Syntax einer Refspec: [+]Quelle:Ziel
Pushes, die eine Refspec nur mit einer Quellreferenz benutzen (also ohne Zielref), erzeugen in dem entfernten Repository einen neuen Zweig: $ cd ~/public_html $ git checkout -b foo Switched to a new branch "foo" $ git push origin foo Total 0 (delta 0), reused 0 (delta 0) To /tmp/Depot/public_html * [new branch] foo -> foo
Pushes, die eine Refspec nur mit einer Zielref benutzen (also ohne Quellref), sorgen dafür, dass die Zielref aus dem entfernten Repository gelöscht wird. Um das Ref als Ziel zu kennzeichnen, muss der Doppelpunkttrenner angegeben werden: $ git push origin :foo To /tmp/Depot/public_html - [deleted] foo
Remote-Konfiguration Es kann ziemlich aufreibend und schwierig sein, alle Informationen über ein entferntes Repository im Blick zu behalten: Sie müssen sich die vollständige URL für das Repository merken, Sie müssen jedesmal, wenn Sie Updates holen wollen, Remote-Referenzen und Refspecs auf der Kommandozeile eintippen, Sie müssen die Zweigzuordnungen rekonstruieren usw. Das ständige Wiederholen der Informationen birgt natürlich die Gefahr, dass sich Fehler einschleichen. Sie fragen sich vielleicht, wie sich Git die URL für das Remote vom ersten Klon merkt, um sie in späteren Fetch- oder Push-Operationen mit origin zu benutzen. Git bietet drei Mechanismen zum Einrichten und Pflegen von Informationen über Remotes: den Befehl git remote, den Befehl git config und das direkte Bearbeiten der Datei .git/config. Alle drei Mechanismen führen schließlich zu Konfigurationsinformationen, die in der Datei .git/config aufgezeichnet werden.
git remote Der Befehl git remote bildet eine speziellere Schnittstelle, besonders für Remotes, zum Bearbeiten der Konfigurationsdatei. Er besitzt mehrere Unterbefehle mit relativ intuitiven Namen. Es gibt zwar keine »Hilfe«-Option, aber man kann sich mithilfe des »unbekann-
222 | Kapitel 11: Entfernte Repositories
ter Unterbefehl-Tricks« (unknown subcommand) eine Meldung mit den Namen der Unterbefehle anzeigen lassen: $ git remote xyzzy error: Unknown subcommand: xyzzy usage: git remote or: git remote add or: git remote rm or: git remote show or: git remote prune or: git remote update [group] -v, --verbose
be verbose
Sie haben die git remote add- und update-Befehle in »Erzeugen Sie Ihr eigenes originRemote« auf Seite 204 und show in »Einen neuen Entwickler hinzufügen« auf Seite 207 gesehen. git remote add origin haben Sie benutzt, um dem neu erzeugten Eltern-Repository im Depot ein neues Remote namens origin hinzuzufügen, und den Befehl git remote show origin haben Sie ausgeführt, um alle Informationen über das Remote origin zu erhalten. Schließlich haben Sie den Befehl git remote update eingesetzt, um alle Updates, die im entfernten Repository zur Verfügung stehen, in Ihr lokales Repository zu holen. Der Befehl git remote rm löscht das angegebene Remote und alle dazugehörigen Tracking-Zweige aus Ihrem lokalen Repository. Im entfernten Repository kann es Zweige geben, die durch Aktionen anderer Entwickler gelöscht werden, deren Kopien jedoch weiterhin in Ihrem Repository herumlungern. Mit dem prune-Befehl kann man die Namen dieser (in Bezug auf das tatsächliche entfernte Repository) veralteten Tracking-Zweige aus Ihrem lokalen Repository entfernen.
git config Mit dem git config-Befehl lassen sich die Einträge in Ihrer Konfigurationsdatei direkt manipulieren. Das schließt verschiedene Konfigurationsvariablen für Remotes ein. Um z.B. mit einer Push-Refspec für alle Zweige, die Sie veröffentlichen wollen, ein neues Remote namens publish hinzuzufügen, gehen Sie folgendermaßen vor: $ git config remote.publish.url 'ssh://git.example.org/pub/repo.git' $ git config remote.publish.push '+refs/heads/*:refs/heads/*'
Jeder der vorangegangenen Befehle fügt eine Zeile in die .git/config-Datei ein. Wenn noch kein publish-Remote-Abschnitt existiert, erzeugt der erste Befehl, den Sie ausführen und der sich auf dieses Remote bezieht, einen entsprechenden Abschnitt in der Datei. Daher enthält Ihre .git/config-Datei teilweise die folgende Remote-Definition: [remote "publish"] url = ssh:git.example.com/pub/repo.git push = +refs/heads/*:refs/heads/*
Remote-Konfiguration
| 223
Nehmen Sie die Option -l (klein geschriebenes L) à la git config -l, um den Inhalt der Konfigurationsdatei mit allen Variablennamen aufzulisten: # Aus einem Klon von git.git-Quellen $ git config -l core.repositoryformatversion=0 core.filemode=true core.bare=false core.logallrefupdates=true remote.origin.url=git://git.kernel.org/pub/scm/git/git.git remote.origin.fetch=+refs/heads/*:refs/remotes/origin/* branch.master.remote=origin branch.master.merge=refs/heads/master
Manuelle Bearbeitung Anstatt sich mit den Befehlen git remote oder git config herumzuschlagen, geht es manchmal einfacher oder schneller, die Datei mit dem bevorzugten Texteditor zu bearbeiten. Das ist nicht weiter schlimm, birgt aber die Gefahr von Fehlern in sich und wird normalerweise nur von solchen Entwicklern gemacht, die mit dem Verhalten und der Konfigurationsdatei von Git sehr vertraut sind. Da Sie aber die Bereiche der Datei gesehen haben, die für die verschiedenen Verhaltensweisen von Git zuständig sind, sowie die Änderungen, die die Befehle hervorrufen, sollten Sie grundsätzlich in der Lage sein, die Konfigurationsdatei zu verstehen und zu manipulieren.
Mehrere entfernte Repositories Operationen wie git remote add Remote Repository-URL können mehrfach ausgeführt werden, um Ihrem Repository mehrere neue Remotes hinzuzufügen. Bei mehreren Remotes können Sie anschließend Commits aus mehreren Quellen holen und in Ihrem Repository kombinieren. Diese Eigenschaft erlaubt Ihnen darüber hinaus, mehrere Push-Ziele einzurichten, die Ihr Repository teilweise oder ganz empfangen können. In Kapitel 12 zeigen wir Ihnen, wie Sie während Ihrer Entwicklung mehrere Repositories in unterschiedlichen Szenarien einsetzen.
Bare-Repositories und git push Als Folge der Peer-to-Peer-Semantik von Git-Repositories weisen alle Repositories die gleiche Gestalt auf. Sie können Daten gleichermaßen in Entwicklungs- und Bare-Repositories schieben und aus ihnen holen; es gibt keinen grundlegenden Unterschied in der Implementierung dieser beiden Arten. Dieses symmetrische Design ist ausgesprochen
224 | Kapitel 11: Entfernte Repositories
wichtig für Git, führt aber auch zu unerwarteten Effekten, wenn Sie versuchen, Bare- und Entwicklungs-Repositories als exakt gleichwertig zu behandeln. Sie erinnern sich sicher, dass der Befehl git push die Dateien im empfangenden Repository nicht auscheckt. Er überträgt einfach nur Objekte aus dem Quell-Repository in das empfangende Repository und aktualisiert anschließend die entsprechenden Refs auf der Empfängerseite. In einem Bare-Repository kann man nicht mehr erwarten, da es kein Arbeitsverzeichnis gibt, das mit ausgecheckten Dateien aktualisiert werden kann. Das ist gut so. In einem Entwicklungs-Repository hingegen, das der Empfänger einer Push-Operation ist, kann dieses Verhalten später zu Verwirrung bei jemandem führen, der dieses Repository benutzen möchte. Die Push-Operation kann den Repository-Zustand einschließlich des HEAD-Commit aktualisieren. Das heißt: Obwohl der Entwickler am entfernten Ende nichts getan hat, ändern sich die Zweigs-Refs und HEAD, wodurch sie nicht mehr synchron mit den ausgecheckten Dateien und dem Index sind. Ein Entwickler, der aktiv in einem Repository arbeitet, in das ein asynchrones Push vorgenommen wird, sieht das Push nicht. Ein nachfolgendes Commit von diesem Entwickler findet jedoch auf einem unerwarteten HEAD statt, wodurch ein seltsamer Verlauf entsteht. Ein erzwungenes Push lässt mit Push übertragene Commits des anderen Entwicklers verloren gehen. Der Entwickler in diesem Repository wird außerdem nicht in der Lage sein, seinen Verlauf mit einem Upstream-Repository oder einem DownstreamKlon abzugleichen, da das nicht länger die einfachen Fast-forwards sind, die sie sein sollten. Und er wird nicht wissen, wieso: Das Repository hat sich hinterrücks stillschweigend geändert. Feuer und Wasser zusammen. Das ist schlecht. Daraus folgt, dass Sie Pushes nur in ein Bare-Repository durchführen sollten. Diese Regel ist nicht unumstößlich, bildet aber eine gute Richtlinie für den gwöhnlichen Entwickler und hat sich bewährt. Es gibt einige Beispiele und Anwendungsfälle, bei denen Sie ein Push in ein Entwicklungs-Repository durchführen könnten, allerdings sollten Sie sich der Implikationen vollständig bewusst sein. Wenn Sie unbedingt ein Push in ein Entwicklungs-Repository ausführen wollen, sollten Sie einem der folgenden zwei grundlegenden Ansätze folgen. Im ersten Szenario wollen Sie wirklich ein Arbeitsverzeichnis mit einem ausgecheckten Zweig im empfangenden Repository haben. Sie wissen z.B., dass andere Entwickler dort niemals aktiv etwas entwickeln werden und daher auch nicht von stillschweigenden Änderungen, die in dieses Repository geschoben werden, betroffen sein können. In diesem Fall könnten Sie ein Hook in das empfangende Repository aktivieren, um das Checkout eines Zweigs, möglicherweise des gerade geschobenen, in das Arbeitsverzeichnis durchzuführen. Um zu verifizieren, dass sich das empfangende Repository vor einem automatischen Checkout in einem vernünftigen Zustand befindet, müsste das Hook
Bare-Repositories und git push
| 225
sicherstellen, dass das Arbeitsverzeichnis des Nicht-Bare-Repository keine Änderungen oder bearbeiteten Dateien enthält und dass sich in seinem Index keine Dateien befinden, die bereitgestellt, aber noch nicht bestätigt sind, wenn das Push eintritt. Wenn diese Bedingungen nicht erfüllt sind, laufen Sie Gefahr, diese Änderungen zu verlieren, da der Checkout sie überschreiben würde. Es gibt ein weiteres Szenario, bei dem das Verschieben in ein Nicht-Bare-Repository einigermaßen gut funktionieren kann. Laut Konvention muss jeder Entwickler, der Änderungen mit einem Push verschiebt, das in einen nicht-ausgecheckten Zweig tun, der einfach als »empfangender« Zweig betrachtet wird. Ein Entwickler schiebt nie in einen Zweig, der möglicherweise ausgecheckt ist. Dabei muss es einer der Entwickler steuern, welcher Zweig wann ausgecheckt wird. Diese Person ist dann dafür verantwortlich, die empfangenden Zweige zu verwalten und sie mit einem Master-Zweig zusammenzuführen, bevor dieser ausgecheckt wird.
Repositories veröffentlichen Unabhängig davon, ob Sie eine Open Source-Entwicklungsumgebung einrichten, in der viele Menschen über das Internet ein Projekt entwickeln, oder ein Projekt für die interne Entwicklung innerhalb einer privaten Gruppe aufstellen, bleiben die Mechanismen der Zusammenarbeit prinzipiell gleich. Der Hauptunterschied zwischen den beiden Szenarien besteht in der Lage des Repository und im Zugriff darauf. Der Ausdruck »Commit-Rechte« ist wirklich irreführend in Git. Git versucht nicht, Zugriffsrechte zu verwalten, sondern überlässt dieses Problem anderen Werkzeugen, etwa SSH, die besser dafür geeignet sind. Man kann in jedem Repository ein Commit ausführen, für das man den (Unix-) Zugriff hat, und zwar über SSH und einen Wechsel mit cd in dieses Repository oder über den normalen Lese-, Schreib- und Ausführungszugriff. Das Konzept sollte besser als »Kann ich das veröffentlichte Repository aktualisieren?« umschrieben werden. In diesem Ausdruck erkennen Sie das tatsächliche Problem: »Kann ich Änderungen in das veröffentlichte Repository schieben?«
Weiter vorn im Abschnitt »Auf entfernte Repositories verweisen« auf Seite 198 habe ich Sie davor gewarnt, die Remote-Repository-URL aus /Pfad/auf/repo.git zu verwenden, weil sich dabei Probleme zeigen können, die den Repositories mit gemeinsam genutzten Dateien innewohnen. Andererseits ist es nicht ungewöhnlich, ein gemeinsames Depot einzurichten, aus dem mehrere Repositories angeboten werden, um einen gemeinsamen Objektspeicher zu haben. In diesem Fall würde man erwarten, dass die Größe der Repositories monoton ansteigt, wenn keine Objekte und Refs daraus entfernt werden. Hier kann es günstig sein, dass ein Großteil des Objektspeichers von vielen Repositories benutzt wird, wodurch man eine Menge Festplattenplatz spart. Um diese Platzersparnis
226 | Kapitel 11: Entfernte Repositories
zu erreichen, sollten Sie beim Einrichten des Bare-Repository-Klons für die veröffentlichten Repositories die Optionen --reference repository oder --local oder --shared benutzen.
Repositories mit kontrolliertem Zugriff Wie bereits erwähnt, könnte es für Ihr Projekt reichen, wenn Sie ein Bare-Repository an einer bekannten Stelle auf einem Dateisystem in Ihrer Einrichtung veröffentlichen, auf das jeder zugreifen darf. Natürlich bedeutet »Zugriff« in diesem Zusammenhang, dass alle Entwickler das Dateisystem auf ihren Maschinen sehen können und traditionelle Unix-Besitz- und SchreibLese-Rechte haben. In diesen Fällen reicht wahrscheinlich eine Dateinamen-URL wie /Pfad/auf/Depot/project.git oder file://Pfad/auf/Depot/project.git. Auch wenn die Leistung nicht überragend ist, kann ein NFS-gemountetes Dateisystem eine solche Unterstützung bieten. Ein etwas komplexerer Zugriff wird erforderlich, wenn mehrere Entwicklungsmaschinen benutzt werden sollen. Innerhalb eines Unternehmens könnte z.B. die IT-Abteilung einen zentralen Server für das Repository-Depot anbieten und die Sicherung dieses Servers übernehmen. Jeder Entwickler hätte dann einen Desktoprechner für die Entwicklung. Wenn ein direkter Dateisystemzugriff wie etwa NFS nicht zur Verfügung steht, könnte man Repositories verwenden, die mit SSH-URLs benannt sind. Allerdings erfordert auch das, dass jeder Entwickler einen Zugang auf dem zentralen Server hat. Immer wenn Sie ein Repository veröffentlichen müssen, wird dringend empfohlen, dazu ein Bare-Repository zu nehmen. Im folgenden Beispiel wird auf genau das Repository, das weiter vorn in /tmp/Depot/ public_html.git veröffentlicht wurde, von einem Entwickler zugegriffen, der SSH-Zugriff auf die Host-Maschine hat: desktop$ cd /tmp desktop$ git clone ssh://example.com/tmp/Depot/public_html.git Initialize public_html/.git Initialized empty Git repository in /tmp/public_html/.git/ [email protected]'s password: remote: Counting objects: 27, done. Receiving objects: 100% (27/27), done.objects: 3% (1/27) Resolving deltas: 100% (7/7), done. remote: Compressing objects: 100% (23/23), done. remote: Total 27 (delremote: ta 7), reused 0 (delta 0)
Wenn dieser Klon fertig ist, zeichnet er das Quell-Repository mit der URL ssh://example. com/tmp/Depot/public_html.git auf. Andere Befehle, wie git fetch und git push, können nun ebenfalls über das Netzwerk benutzt werden:
Repositories veröffentlichen
| 227
desktop$ git push [email protected]'s password: Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 385 bytes, done. Total 3 (delta 1), reused 0 (delta 0) To ssh://example.com/tmp/Depot/public_html.git 55c15c8..451e41c master -> master
In beiden Beispielen wird als Passwort das normale Unix-Login-Passwort für die entfernte Host-Maschine abgefragt. Falls Sie Netzwerkzugriff mit authentifizierten Entwicklern gewährleisten müssen, aber keinen Login-Zugriff auf dem Host-Server geben wollen, dann schauen Sie sich Tommi Vertanens gitosis-Projekt unter git://eagain. net/gitosis.git an.
Auch hier kann ein solcher SSH-Zugriff sich entweder auf eine Gruppe oder ein Unternehmen beschränken oder über das gesamte Internet verfügbar sein – je nach dem gewünschten Umfang des Zugriffs.
Repositories mit anonymem Lesezugriff Wenn Sie Code freigeben wollen, sollten Sie wahrscheinlich einen Host-Server zum Veröffentlichen der Repositories einrichten und anderen erlauben, sie zu klonen. Um diese Repositories zu klonen oder zu holen, benötigen Entwickler lediglich Lesezugriff. Eine verbreitete und einfache Lösung besteht darin, sie mit git-daemon und vielleicht noch einem HTTP-Daemon zu exportieren. Der tatsächliche Zugriffsbereich, über den Sie Ihr Repository veröffentlichen, kann genauso breit oder eingeschränkt sein wie der Zugriff auf Ihre HTTP-Seiten oder Ihren git-daemon. Das heißt: Wenn Sie diese Befehle auf einer quasiöffentlichen Maschine anbieten, kann jeder etwas aus Ihren Repositories klonen und holen. Platzieren Sie die Maschine hinter Ihre Unternehmens-Firewall, dann haben nur die Leute aus Ihrem Unternehmen Zugriff darauf (unter normalen Umständen, also solange die Sicherheit nicht verletzt wurde).
Repositories mittels git-daemon veröffentlichen git-daemon erlaubt Ihnen, Ihre Repositories über das Git-eigene Protokoll zu exportieren.
Sie müssen die Repositories auf irgendeine Weise als »bereit zum Export« kennzeichnen. Typischerweise erzeugt man dazu die Datei git-daemon-export-ok im obersten Verzeichnis des Bare-Repository. Dieser Mechanismus bietet Ihnen eine fein abgestimmte Kontrolle darüber, welche Repositories der Daemon exportieren kann. Anstatt alle Repositories einzeln zu markieren, können Sie auch git-daemon mit der Option --export-all ausführen, um alle identifizierbaren Repositories (die also sowohl ein
228 | Kapitel 11: Entfernte Repositories
objects- als auch ein refs-Unterverzeichnis besitzen) zu veröffentlichen, die in seiner Liste der Verzeichnisse zu finden sind. Es gibt viele git-daemon-Optionen, mit denen man einschränken und konfigurieren kann, welche Repositories exportiert werden. Eine gebräuchliche Methode besteht darin, den git-daemon-Daemon auf einem Server einzurichten, um ihn als inetd-Dienst zu aktivieren. Dabei müssen Sie dann auch dafür sorgen, dass Ihre /etc/services-Datei einen Eintrag für Git enthält. Der vorgegebene Port ist 9418, obwohl Sie natürlich auch einen anderen Port benutzen können. Ein typischer Eintrag sähe so aus: git
9418/tcp
# Git Versionskontrollsystem
Nachdem Sie diese Zeile in die Datei /etc/services geschrieben haben, müssen Sie mit einem Eintrag in /etc/inetd.conf festlegen, wie git-daemon aufgerufen werden soll. Ein typischer Eintrag könnte so aussehen: # In eine einzige Zeile in /etc/inetd.conf setzen git stream tcp nowait nobody /usr/bin/git-daemon git-daemon --inetd --verbose --export-all --base-path=/pub/git
Wenn Sie xinetd anstelle von inetd verwenden, dann setzen Sie eine ähnliche Konfiguration in die Datei /etc/xinetd.d/git-daemon: # Beschreibung: Der service git { disable type port socket_type wait user server server_args log_on_failure }
git-Server bietet Zugriff auf die Git-Repositories
= no = UNLISTED = 9418 = stream = no = nobody = /usr/bin/git-daemon = --inetd --export-all --base-path=/pub/git += USERID
Sie können es durch einen Trick, der von git-daemon unterstützt wird, so aussehen lassen, als würden sich die Repositories auf separaten Hosts befinden, obwohl sie in Wirklichkeit nur in eigenen Verzeichnissen auf einem einzigen Host liegen. Der folgende Beispieleintrag erlaubt es einem Server, mehrere, gewissermaßen auf einzelnen Hosts vorliegende Git-Daemons anzubieten: # In eine einzige Zeile in /etc/inetd.conf setzen git stream tcp nowait nobody /usr/bin/git-daemon git-daemon --inetd --verbose --export-all --interpolated-path=/pub/%H%D
Repositories veröffentlichen
| 229
In dem gezeigten Befehl ersetzt git-daemon das %H durch einen vollqualifizierten Hostnamen und das %D durch den Verzeichnispfad des Repository. Da %H ein logischer Hostname sein kann, können von einem physischen Server unterschiedliche Gruppen von Repositories angeboten werden. Meist wird eine weitere Ebene in der Verzeichnisstruktur, wie /software oder /scm, eingesetzt, um die angebotenen Repositories zu organisieren. Wenn Sie --interpolated-path=/ pub/%H%D mit einem /software-Repository-Verzeichnispfad kombinieren, werden sich die zu veröffentlichen Bare-Repositories auf dem Server physisch in solchen Verzeichnissen befinden: /pub/git.example.com/software/ /pub/www.example.org/software/
Sie geben die Verfügbarkeit Ihrer Repositories dann unter solchen URLs bekannt: git://git.example.com/software/repository.git git://www.example.org/software/repository.git
Hier wurde %H durch den Host git.example.com oder www.example.org ersetzt; statt %D stehen hier die vollständigen Repository-Namen wie /software/repository.git. Dieses Beispiel zeigt vor allem, wie ein einziger git-daemon mehrere getrennte Sammlungen von Git-Repositories pflegen und veröffentlichen kann, die sich physisch auf einem Server befinden, aber als logisch getrennte Hosts präsentiert werden. Diese Repositories, die von einem Host aus verfügbar sind, können sich von denjenigen unterscheiden, die von einem anderen Host angeboten werden.
Repositories mit einem HTTP-Daemon veröffentlichen Manchmal lassen sich Repositories mit anonymem Lesezugriff leichter veröffentlichen, indem man sie über einen HTTP-Daemon zur Verfügung stellt. Wenn Sie außerdem gitweb einrichten, können Besucher eine URL in ihre Webbrowser laden, eine Indexauflistung Ihres Repository sehen und mithilfe der vertrauten Klicks und des Zurück-Buttons des Browsers arbeiten. Besucher müssen nicht Git ausführen, um Dateien herunterzuladen. Sie müssen an Ihrem Bare-Repository eine Konfigurationsanpassung vornehmen, bevor es korrekt von einem HTTP-Daemon angeboten werden kann. Aktivieren Sie die Option hooks/post-update: $ cd /Pfad/auf/bare/repo.git $ mv hooks/post-update.sample hooks/post-update
Stellen Sie sicher, dass das post-update-Skript ausführbar ist, oder wenden Sie zur Sicherheit chmod 755 darauf an. Kopieren Sie dann schließlich dieses Bare-Repository in ein Verzeichnis, das von Ihrem HTTP-Daemon angeboten wird. Sie können nun bekanntgeben, dass Ihr Projekt unter einer solchen URL zur Verfügung steht: http://www.example.org/software/repository.git
230 | Kapitel 11: Entfernte Repositories
Falls Sie eine Fehlermeldung wie ... not found: did you run git update-server-info on the server?
oder Perhaps git-update-server-info needs to be run there?
sehen, dann führen Sie wahrscheinlich den hooks/post-update-Befehl nicht richtig auf dem Server aus.
Mit Git und HTTP-Daemons veröffentlichen Die Benutzung von Webserver und -browser ist zwar sicher bequem, aber Sie sollten berücksichtigen, wie viel Verkehr Sie auf Ihrem Server einplanen. Entwicklungsprojekte können groß werden, und HTTP ist weniger effizient als das Git-eigene Protokoll. Sie können sowohl HTTP- als auch Git-Daemon-Zugriff anbieten, allerdings erfordert das einige Anpassungen und Abstimmungen zwischen Ihrem Git-Daemon und Ihrem HTTP-Daemon. Es könnte speziell eine Zuordnung mit der Option --interpolated-path für git-daemon und einer Alias-Option für Apache nötig sein, um eine nahtlose Integration der beiden Sichten auf dieselben Daten zu gewährleisten. Weitere Details würden den Rahmen dieses Buchs sprengen.
Repositories mit anonymem Schreibzugriff Technisch gesehen könnten Sie die URL-Formen des Git-eigenen Protokolls verwenden, um das anonyme Schreiben in Repositories zu erlauben, die von git-daemon angeboten werden. Dazu müssen Sie die receive-pack-Option in den veröffentlichten Repositories aktivieren: [daemon] receivepack = true
In einem privaten LAN, wo alle Entwickler vertrauenswürdig sind, könnten Sie das tun, allerdings wird es nicht empfohlen. Stattdessen sollten Sie ihren Git-Push-Verkehr über eine SSH-Verbindung tunneln.
Repositories veröffentlichen
| 231
Kapitel 12
KAPITEL 12
Repository-Verwaltung
Dieses Kapitel präsentiert zwei Ansätze für das Verwalten und Veröffentlichen von Repositories für eine gemeinsame Entwicklung. Ein Ansatz zentralisiert das Repository, beim anderen wird das Repository verteilt. Beide Lösungen besitzen ihre Berechtigung, und welche für Sie und Ihr Projekt passt, hängt von Ihren Anforderungen und Ihrer Philosophie ab. Aber ungeachtet des gewählten Ansatzes implementiert Git ein verteiltes Entwicklungsmodell. Selbst wenn Ihr Team z.B. das Repository zentralisiert, hat jeder Entwickler eine vollständige private Kopie des Repository und kann unabhängig von den anderen arbeiten. Die Arbeit ist verteilt, wenn auch durch ein zentrales, gemeinsames Repository koordiniert. Das Repository-Modell und das Entwicklungsmodell sind orthogonale Eigenschaften.
Die Repository-Struktur Die gemeinsame Repository-Struktur Manche Versionskontrollsysteme verwenden einen zentralisierten Server, um ein Repository zu unterhalten. In diesem Modell ist jeder Entwickler ein Client des Servers, der die autoritative Version des Repository vorhält. Da der Server die Entscheidungsgewalt hat, muss er bei fast jeder Versionierungsoperation kontaktiert werden, um Repository-Informationen zu holen oder zu aktualisieren. Damit also zwei Entwickler Daten gemeinsam nutzen können, müssen alle Informationen durch einen zentralisierten Server laufen; es ist nicht möglich, Daten zwischen den Entwicklern direkt auszutauschen. Mit Git ist dagegen ein gemeinsam genutztes, autoritatives, zentralisiertes Repository lediglich eine Konvention. Jeder Entwickler besitzt weiterhin einen Klon des Repository aus dem Depot, sodass es nicht nötig ist, für jede Anforderung oder Abfrage an einen zentralisierten Server heranzutreten. So können etwa einfache Abfragen an den Logverlauf von jedem Entwickler privat und offline durchgeführt werden.
| 233
Einer der Gründe dafür, dass einige Operationen lokal ausgeführt werden können, besteht darin, dass ein Checkout nicht nur die spezielle Version holt, die Sie haben wollen – wie bei den meisten zentralisierten Versionskontrollsystemen –, sondern den gesamten Verlauf. Daher können Sie jede Version einer Datei aus dem lokalen Repository rekonstruieren. Darüber hinaus hindert nichts einen Entwickler daran, entweder auf einer Peer-to-PeerBasis mit anderen Entwicklern ein alternatives Repository einzurichten und verfügbar zu machen oder Inhalt in Form von Patches und Zweigen freizugeben. Zusammengefasst lässt sich also feststellen, dass Gits Vorstellung von einem »gemeinsamen, zentralisierten« Repository-Modell rein auf sozialen Konventionen und Abmachungen beruht.
Verteilte Repository-Struktur Große Projekte verfügen oft über ein stark verteiltes Entwicklungsmodell, das aus einem zentralen, einzelnen, aber dennoch logisch aufgeteilten Repository besteht. Das Repository existiert zwar als physische Einheit, logische Teilbereiche werden aber an unterschiedliche Leute oder Teams verwiesen, die hauptsächlich oder vollständig unabhängig arbeiten. Wenn gesagt wird, dass Git ein verteiltes Repository-Modell unterstützt, dann bedeutet das nicht, dass ein einzelnes Repository in separate Teile aufgeteilt und auf viele Hosts verteilt wird. Stattdessen ist das verteilte Repository lediglich eine Folge des verteilten Entwicklungsmodells von Git. Jeder Entwickler hat sein eigenes Repository, das vollständig und eigenständig ist. Die einzelnen Entwickler und ihre jeweiligen Repositories können sich irgendwo im Netzwerk befinden.
Wie das Repository partitioniert oder auf unterschiedliche Maintainer gelegt wird, ist im Großen und Ganzen unwesentlich für Git. Die Repositories könnten tiefergehend organisiert oder breiter strukturiert sein. So könnten z.B. unterschiedliche Entwicklungsteams für bestimmte Teile einer Codebasis verantwortlich sein, wobei die Aufteilung in Untermodule, Bibliotheken oder Funktionen erfolgen kann. Jedes Team könnte einen Mitarbeiter zum Maintainer oder Verwalter seines Teils der Codebasis machen und als Team übereinkommen, alle Änderungen über diesen anerkannten Maintainer zu leiten. Die Struktur kann sich mit der Zeit weiterentwickeln oder ändern, wenn andere Leute oder Gruppen zu dem Projekt hinzukommen. Ein Team könnte außerdem ZwischenRepositories bilden, die Kombinationen aus anderen Repositories enthalten, und zwar mit oder ohne weitere Entwicklung. Es könnte z.B. spezielle »stabile« oder »Release«Repositories geben, die jeweils ein dazugehöriges Entwicklungsteam und einen Verwalter haben.
234 | Kapitel 12: Repository-Verwaltung
Sicher ist es eine gute Idee, es der großangelegten Repository-Iteration und dem Datenfluss zu erlauben, sich auf natürliche Weise und entsprechend der Kritik und den Vorschlägen der Mitarbeiter zu entwickeln, anstatt bereits im Vorfeld einen möglicherweise künstlichen Aufbau aufzuzwingen. Git ist flexibel: Falls also die Entwicklung in einem Layout oder Datenfluss nicht zu funktionieren scheint, ist es relativ einfach, in eine bessere Konstellation zu wechseln. Wie die Repositories eines großen Projekts organisiert sind oder wie sie zusammenwachsen und sich verbinden, ist ebenfalls größtenteils unwesentlich für das Funktionieren von Git; Git unterstützt eine ganze Reihe von Organisationsmodellen. Merken Sie sich, dass die Repository-Struktur nicht absolut ist. Überdies ist die Verbindung zwischen zwei Repositories nicht vorgeschrieben. Git-Repositories sind Peers, also gleichberechtigte Partner. Wie wird nun eine Repository-Struktur über die Zeit aufrechterhalten, wenn es keine technischen Maßnahmen gibt, die diese Struktur erzwingen? Im Prinzip bildet die Struktur ein Web of Trust (Vertrauensnetzwerk) für die Akzeptanz von Änderungen. Repository-Organisation und Datenfluss zwischen Repositories werden von Vereinbarungen bestimmt. Die Frage ist, ob der Verwalter eines Ziel-Repository Ihre Änderungen akzeptiert. Haben Sie andererseits genug Vertrauen in die Daten des Quell-Repository, um sie in Ihr eigenes Repository zu holen?
Beispiele für die Repository-Struktur Das Linux-Kernel-Projekt ist das kanonische Beispiel für ein stark verteiltes Repository und einen entsprechend stark verteilten Entwicklungsprozess. An jedem Linux-KernelRelease sind etwa 800 bis 1.100 einzelne Mitwirkende aus ungefähr 100 bis 200 unterschiedlichen Unternehmen beteiligt. Im Laufe der letzten Kernel-Releases (2.6.24 bis 2.6. 26) führten die Entwickler etwa 10.000 bis 13.500 Commits pro Release durch. Das sind zwischen vier und sechs Commits pro Stunde an jedem Tag der Entwicklung irgendwo auf dem Planeten.1 Obwohl Linus Torvalds ein offizielles Repository auf der »Spitze des Haufens« unterhält, das die meisten Leute als verbindlich betrachten, sind viele abgeleitete Neben-Repositories in Gebrauch. So nehmen z.B. viele Linux-Distributoren das offizielle, markierte Release von Linus, testen es, bringen Bug-Fixes ein, passen es an ihre Distribution an und veröffentlichen es als ihr offizielles Release. (Mit ein bisschen Glück werden die BugFixes zurückgeschickt und fließen in Linus’ Linux-Repository ein, sodass alle etwas davon haben.) 1
Die Kernel-Statistiken stammen von http://www.kernel.org/pub/linux/kernel/people/gregkh/kernel_history/gregkh-ols-2007.pdf, http://www.linuxfoundation.org/publications/linuxkerneldevelopment.php und http://mirror. celinuxforum.org/gitstat.
Die Repository-Struktur | 235
Während eines Kernel-Entwicklungszyklus werden Hunderte von Repositories veröffentlicht und von Hunderten von Verwaltern moderiert, und werden sie von Tausenden von Entwicklern benutzt, um Änderungen für das Release zu sammeln. Allein die Haupt-Kernel-Website, http://www.kernel.org/, veröffentlicht etwa 500 Repositories, die etwas mit dem Linux-Kernel zu tun haben und von etwa 150 einzelnen Verwaltern organisiert werden. Es gibt mit Sicherheit Tausende, wenn nicht gar Zehntausende von Klonen dieser Repositories auf der ganzen Welt, die die Grundlage der Patches oder Anwendungen der einzelnen Mitstreiter bilden. In Ermangelung einer abgefahrenen Schnappschusstechnik und irgendwelcher statistischen Analysen gibt es eigentlich keine Möglichkeit festzustellen, wie diese Repositories verbunden sind. Man kann lediglich sagen, dass es sich um ein Gewebe oder Netzwerk handelt, das überhaupt nicht streng hierarchisch ist. Seltsamerweise jedoch gibt es einen sozialen Antrieb, um Patches und Änderungen in das Repository von Linus zu bekommen, wodurch dieses im Prinzip tatsächlich so behandelt wird, als sei es die Spitze des Haufens! Wenn Linus alle Patches oder Änderungen einzeln für sein Repository akzeptieren müsste, würde er einfach nicht mehr hinterherkommen. Denken Sie daran: Die Änderungen kommen gesammelt je etwa alle 10 bis 15 Minuten während des gesamten Entwicklungszyklus eines Release in seinen Baum. Nur mithilfe der Maintainer – die die Unter-Repositories moderieren und Patches sammeln und anwenden – kann Linus überhaupt Schritt halten. Es ist so, als würden die Maintainer eine pyramidenartige Struktur erzeugen, über die sie Patches in Linus’ vereinbartes Master-Repository leiten. Um genau zu sein: Unter den Verwaltern, aber nahe am »oberen« Bereich der LinuxRepository-Struktur, gibt es viele Unterverwalter und einzelne Entwickler, die ebenfalls als Verwalter und Entwicklerpartner tätig sind. Die Arbeit am Linux-Kernel ist ein riesiges, mehrlagiges Gewebe aus kooperierenden Menschen und Repositories. Es ist nicht so, dass das eine phänomenal große Codebasis wäre, die das Verständnis einiger Leute oder Teams überschreitet. Der Trick besteht darin, dass diese vielen Teams über die ganze Welt verstreut sind und es trotzdem schaffen, mit Blick auf ein gleichbleibendes, langfristiges Ziel eine gemeinsame Codebasis zu koordinieren, zu entwickeln und wieder zusammenzuführen, und das, indem sie alle die Möglichkeiten von Git für die verteilte Entwicklung ausnutzen. Am anderen Ende des Spektrums wird die Freedesktop.org-Entwicklung völlig mit einem gemeinsamen, zentralisierten Repository-Modell durchgeführt, das von Git ermöglicht wird. In diesem Entwicklungsmodell darf jeder Entwickler seine Änderungen direkt in ein Repository unter http://cgit.freedesktop.org/ schieben. Das X.org-Projekt selbst stellt im Zusammenhang mit X unter http://cgit.freedesktop.org/ etwa 350 Repositories bereit, wobei es mehrere Hundert weitere für einzelne Benutzer
236 | Kapitel 12: Repository-Verwaltung
gibt. Die Mehrheit der X-bezogenen Repositories sind verschiedene Untermodule aus dem gesamten X-Projekt, die eine funktionelle Aufgliederung der Anwendungen, X-Server, verschiedenen Fonts usw. bilden. Einzelne Entwickler werden ebenfalls ermutigt, Zweige für Funktionen zu schaffen, die noch nicht für eine allgemeine Veröffentlichung bereit sind. Diese Zweige erlauben es, die Änderungen (oder vorgeschlagenen Änderungen) anderen Entwicklern zum Verwenden, Testen und Verbessern zur Verfügung zu stellen. Wenn schließlich die Zweige mit den neuen Funktionen bereit für den allgemeinen Einsatz sind, werden sie in die entsprechenden Hauptentwicklungszweige aufgenommen. Ein Entwicklungsmodell, das es einzelnen Entwicklern erlaubt, Änderungen direkt in ein Repository zu schieben, ist natürlich nicht frei von Risiken. Ohne förmlichen Prüfvorgang vor einem Push ist es möglich, dass schlechte Änderungen stillschweigend in ein Repository gelangen und eine Zeit lang unentdeckt bleiben. Wohlgemerkt besteht keine echte Gefahr, dass Daten verloren gehen oder man nicht wieder einen guten Zustand herstellen könnte, weil ja der komplette Repository-Verlauf immer noch verfügbar ist. Das Problem besteht darin, dass es Zeit kostet, den Fehler zu entdecken und zu beheben. Wie Keith Packard schrieb: Wir lehren die Leute so langsam, ihre Patches zur Beurteilung an die xorg-Mailingliste zu schicken, was manchmal sogar geschieht. Und manchmal ziehen wir einfach etwas zurück. Git ist so robust, dass wir uns nie Sorgen um Datenverluste machen müssen, allerdings ist der Zustand der Baumspitze nicht immer ideal. Es hat aber besser funktioniert, als wenn man dafür CVS genommen hätte …2
In einer verteilten Entwicklung leben Den öffentlichen Verlauf ändern Wenn Sie ein Repository veröffentlicht haben, von dem andere einen Klon erzeugen können, sollten Sie es als statisch betrachten und davon absehen, den Verlauf eines der Zweige zu ändern. Diese Regel ist zwar nicht unumstößlich, allerdings erleichtern Sie anderen Leuten, die Ihr Repository klonen, das Leben, indem Sie es vermeiden, den veröffentlichten Verlauf »zurückzuspulen« und zu ändern. Nehmen Sie einmal an, Sie veröffentlichen ein Repository, das einen Zweig mit den Commits A, B, C und D hat. Jeder, der Ihr Repository klont, erhält diese Commits. Stellen Sie sich nun vor, dass Alice Ihr Repository klont und anschließend auf der Grundlage Ihres Zweigs weiterentwickelt. 2
Private E-Mail, 23. März 2008.
In einer verteilten Entwicklung leben
| 237
In der Zwischenzeit beschließen Sie aus irgendeinem Grund, etwas in Commit C zu ändern. Die Commits A und B bleiben gleich, allerdings ändert sich beginnend mit Commit C die Auffassung des Zweigs vom Verlauf der Commits. Sie könnten C etwas ändern oder ein völlig neues Commit, X, ausführen. In beiden Fällen würden die Commits A und B bei einem erneuten Veröffentlichen des Repository so bleiben, wie sie sind, es würden stattdessen dann aber X und anschließend Y angeboten werden. Alice’ Arbeit wird dadurch stark beeinflusst. Alice kann Ihnen keine Patches senden, keine Anforderungen zum Ziehen von Änderungen ausführen und Ihnen auch keine Änderungen in das Repository schieben, weil ihre Entwicklung auf Commit D beruht. Patches gelten nicht, weil sie auf Commit D basieren. Stellen Sie sich vor, Alice startet eine Pull-Anforderung und Sie versuchen, ihre Änderungen zu ziehen; möglicherweise können Sie sie in Ihr Repository holen (je nach Ihren Tracking-Zweigen für das entfernte Repository von Alice), aber die Merges werden mit hoher Sicherheit Konflikte zeigen. Das Fehlschlagen dieses Push hat seine Ursache in einem Nicht-Fast-forward-Push-Problem. Kurz gesagt, die Basis von Alice’ Entwicklung hat sich verändert. Sie haben ihr den Commit-Teppich unter den Entwicklungsfüßen weggezogen. Die Situation ist allerdings nicht ausweglos. Git kann Alice helfen, vor allem, wenn sie den Befehl git rebase --onto benutzt, um ihre Änderungen auf Ihren neuen Zweig umzulagern, nachdem sie den neuen Zweig in ihr Repository geholt hat. Manchmal ist es außerdem gar nicht so verkehrt, wenn man einen Zweig mit einem dynamischen Verlauf hat. Z.B. gibt es innerhalb des Git-Repository selbst einen sogenannten Proposed-Updates-Zweig pu (vorgeschlagene Änderungen), der speziell so gekennzeichnet ist, dass er häufig »zurückgespult«, »rebased« oder »umgeschrieben« wird. Sie als Klonender können diesen Zweig als Basis für Ihre Entwicklung benutzen, müssen sich aber des Zwecks dieses Zweigs bewusst sein und besondere Anstrengungen unternehmen, um ihn effektiv einzusetzen. Weshalb sollte nun jemand einen Zweig mit einem dynamischen Commit-Verlauf veröffentlichen? Ein Grund ist häufig, dass man andere Entwickler über mögliche und sich schnell ändernde Entwicklungen in Kenntnis setzen möchte, die ein anderer Zweig nehmen könnte. Man kann einen solchen Zweig aber auch rein zu dem Zweck anlegen, anderen Entwicklern eine veröffentlichte Änderungsmenge zur Verfügung zu stellen – selbst wenn das nur temporär geschehen sollte.
Getrennte Commit- und Veröffentlichungsschritte Einer der klaren Vorteile eines verteilten Versionskontrollsystems ist die Trennung von Commit und Veröffentlichung. Ein Commit sichert nur den Zustand in Ihrem privaten Repository; das Veröffentlichen durch Patches oder Push/Pull macht die Änderung öffentlich, wodurch im Prinzip der Repository-Verlauf eingefroren wird. Andere Versi-
238 | Kapitel 12: Repository-Verwaltung
onskontrollsysteme, etwa CVS oder SVN, haben keine solche konzeptuelle Trennung. Um ein Commit zu machen, müssen Sie gleichzeitig veröffentlichen. Aufgrund der Trennung von Commit und Veröffentlichung wird der Entwickler viel eher exakte, durchdachte, kleine, logische Schritte mit Patches vornehmen. Tatsächlich kann man eine beliebige Anzahl kleiner Änderungen vornehmen, ohne ein anderes Repository oder einen anderen Entwickler zu beeinflussen. Die Commit-Operation ist in dem Sinne offline, dass sie keinen Netzwerkzugriff erfordert, um positive, vorwärts gerichtete Schritte innerhalb Ihres eigenen Repository aufzuzeichnen. Git bietet darüber hinaus Mechanismen, mit denen sich Commits vor dem Veröffentlichen zu hübschen, sauberen Sequenzen verfeinern und verbessern lassen. Sobald Sie dann so weit sind, können die Commits in einer eigenen Operation öffentlich gemacht werden.
Nicht ein einziger wahrer Verlauf Entwicklungsprojekte innerhalb einer verteilten Umgebung weisen einige auf den ersten Blick nicht ganz so offensichtliche Marotten auf. Diese Marotten sind zunächst verwirrend, und ihre Behandlung weicht oft vom Vorgehen bei anderen, nicht verteilten Versionskontrollsystemen ab, Git jedoch geht mit ihnen auf klare und logische Weise um. Wenn die Entwicklung eines Projekts parallel bei verschiedenen Entwicklern stattfindet, erzeugt jeder von ihnen einen für ihn korrekten Commit-Verlauf. In der Folge gibt es mein Repository und meinen Commit-Verlauf, Dein Repository und Deinen CommitVerlauf und möglicherweise noch mehrere andere, die sich gleichzeitig entwickelt haben. Jeder Entwickler hat seine eigene Vorstellung vom Verlauf, und jeder Verlauf ist korrekt. Es gibt nicht nur einen »wahren« Verlauf. Sie können nicht auf einen zeigen und behaupten, »Das ist der echte Verlauf«. Vermutlich haben sich die unterschiedlichen Entwicklungsverläufe aus einem bestimmten Grund gebildet, und am Ende werden die verschiedenen Repositories und unterschiedlichen Commit-Verläufe zu einem gemeinsamen Repository zusammengeführt. Schließlich soll sich ja alles zu einem gemeinsamen Ziel entwickeln. Wenn die verschiedenen Zweige aus den unterschiedlichen Repositories zusammengeführt wurden, sind alle Variationen vorhanden. Im Prinzip besagt das zusammengeführte Ergebnis: »Der zusammengeführte Verlauf ist besser als einer allein.« Git drückt diese »Ambivalenz« gegenüber Zweigvariationen aus, wenn es den CommitDAG durchläuft. Falls also Git ein Merge-Commit erreicht, wenn es versucht, die Commit-Sequenz zu linearisieren, muss es den einen oder den anderen Zweig zuerst auswählen. Welche Kriterien würde es geltend machen, um einen der Zweige zu bevorzugen? Die Schreibweise des Autorennachnamens? Vielleicht den Zeitstempel eines Commit? Das könnte sinnvoll sein.
In einer verteilten Entwicklung leben
| 239
Selbst wenn Sie beschließen, Zeitstempel zu verwenden, und sich auf die Benutzung des UTC und extrem präziser Werte einigen, hilft das nichts. Sogar dieses Rezept ist absolut unzuverlässig! (Die Uhren im Computer eines Entwicklers könnten absichtlich oder versehentlich falsch gehen.) Grundsätzlich ist es Git egal, was zuerst kam. Die einzig echte, zuverlässige Beziehung, die zwischen Commits aufgebaut werden kann, ist eine direkte Elternbeziehung, die in den Commit-Objekten aufgezeichnet wird. Zeitstempel bieten bestenfalls einen sekundären Anhaltspunkt, normalerweise begleitet von verschiedenen Heuristiken, um Fehler wie nicht gestellte Uhren zu tolerieren. Kurz gesagt, funktionieren weder Zeit noch Raum auf wohldefinierte Weise, sodass Git auf die Effekte der Quantenphysik zurückgreifen muss.
Git als Peer-to-Peer-Backup Linus Torvalds sagte einmal: »Nur Feiglinge machen Backups auf Band: Echte Männer laden ihren wichtigen Kram auf einen ftp-Server und lassen den vom Rest der Welt spiegeln.« Das Hochladen der Dateien ins Internet und das Erstellen von Kopien durch andere Leute bildete jahrelang die »Sicherungsmethode« für den Quellcode des LinuxKernels. Und es hat funktioniert! In gewisser Weise ist Git nur eine Erweiterung dieses Konzepts. Wenn Sie heutzutage den Quellcode des Linux-Kernels herunterladen und dabei Git benutzen, laden Sie nicht nur die neueste Version, sondern den gesamten Verlauf, der zu dieser Version geführt hat. Dadurch machen Sie Linus’ Backups besser als je zuvor. Dieses Konzept wird durch Projekte ausgenutzt, die es Systemadministratoren erlauben, ihre /etc-Konfigurationsverzeichnisse mit Git zu verwalten, und es sogar den Benutzern ermöglichen, ihre Home-Verzeichnisse zu verwalten und zu sichern. Denken Sie daran: Nur weil Sie Git benutzen, sind Sie noch lange nicht gezwungen, Ihre Repositories freizugeben; es erleichtert Ihnen aber, Ihre Repositories wie mit einer Versionskontrolle direkt auf Ihre NAS-Box (Networked Attached Storage) zu kopieren und damit zu sichern.
Wissen, wo Sie stehen Wenn man an einem verteilten Entwicklungsprojekt teilnimmt, ist es wichtig zu wissen, wie man selbst, sein Repository und seine Entwicklungsanstrengungen in das Große und Ganze passen. Abgesehen von der offensichtlichen potenziellen Möglichkeit, die Entwicklung in unterschiedliche Richtungen voranzutreiben, und der Erfordernis einer grundlegenden Koordination, haben die Mechanismen der Benutzung von Git und seiner Funktionen starken Einfluss darauf, wie geschmeidig sich die eigenen Arbeiten in die der anderen Entwickler einfügen, die ebenfalls an dem Projekt arbeiten.
240 | Kapitel 12: Repository-Verwaltung
Diese Fragen können vor allem in umfangreichen verteilten Entwicklungen problematisch werden, die oft in Open Source-Projekten zu finden sind. Indem man seine Rolle im Gesamtbild erkennt und versteht, wer die Produzenten und wer die Empfänger der Änderungen sind, lassen sich viele der Fragen leicht beantworten.
Upstream- und Downstream-Flüsse Es gibt keine strenge Beziehung zwischen zwei Repositories, die voneinander geklont wurden. Allerdings ist es üblich, das Eltern-Repository in Bezug auf das neue, geklonte Repository als »upstream« (stromaufwärts) zu bezeichnen. Entsprechend liegt das neue, geklonte Repository dann »downstream« (stromabwärts) in Bezug auf das ursprüngliche Eltern-Repository. Die Upstream-Beziehung erweitert sich dann vom Eltern-Repository aus nach »oben« auf alle Repositories, von denen dieses möglicherweise geklont wurde. In gleicher Weise erweitert sich die Downstream-Beziehung nach »unten« auf alle Repositories, die vielleicht von Ihrem geklont werden. Man muss allerdings erkennen, dass dieser Begriff von upstream und downstream nicht direkt mit der Klonoperation zusammenhängt. Git unterstützt ein vollkommen willkürliches Netzwerk zwischen Repositories. Neue Remote-Verbindungen können hinzugefügt werden, und Ihr Original-Klon-Remote kann entfernt werden, um beliebige neue Beziehungen zwischen den Repositories zu schaffen. Wenn eine Hierarchie etabliert wird, geschieht das allein auf der Grundlage von Vereinbarungen. Bob stimmt zu, seine Änderungen an Sie zu schicken; im Gegenzug stimmen Sie zu, Ihre Änderungen an jemanden weiter oben zu senden usw. Wichtig an einer Beziehung zwischen Repositories ist, wie die Daten zwischen ihnen ausgetauscht werden. Das heißt, jedes Repository, an das Sie Änderungen schicken, wird normalerweise von Ihnen aus gesehen als upstream betrachtet. Ebenso wird jedes Repository, das Sie als seine Basis verwendet, als downstream angesehen. Es ist rein subjektiv, wird aber so vereinbart. Git selbst kümmert sich nicht um diesen Richtungsbegriff. Das Konzept von upstream und downstream hilft uns dabei, uns vorzustellen, wohin die Patches gehen. Natürlich können Repositories auch gleichberechtigte Partner sein. Wenn zwei Entwickler Patches austauschen oder Änderungen zwischen ihren Repositories hin- und herschieben, dann ist keines der beiden wirklich upstream oder downstream.
Die Rollen von Maintainer und Entwickler Zwei gebräuchliche Rollen sind Maintainer (Verwalter) und Entwickler. Der Maintainer dient hauptsächlich als Integrator oder Moderator, der Entwickler wiederum generiert die Änderungen. Der Maintainer sammelt und koordiniert die Änderungen von mehre-
Wissen, wo Sie stehen
| 241
ren Entwicklern und stellt sicher, dass sie alle einem gewissen Standard folgen. Anschließend macht der Maintainer die gesamte Menge der Updates für andere verfügbar. Das heißt, der Maintainer ist auch der Herausgeber der Updates. Ziel des Maintainers sollte es sein, Änderungen zu sammeln, zu moderieren, zu akzeptieren oder abzuweisen und dann schließlich Zweige zu veröffentlichen, die die Projektentwickler benutzen können. Um für ein geschmeidiges Entwicklungsmodell zu sorgen, sollten Maintainer einen Zweig nicht mehr ändern, nachdem er veröffentlicht wurde. Andererseits erwartet ein Maintainer von den Entwicklern, dass sie ihm Änderungen schicken, die relevant sind und zu den veröffentlichten Zweigen passen. Ziel eines Entwicklers ist neben der Verbesserung des Projekts, dass der Maintainer seine Änderungen akzeptiert. Schließlich nützen Änderungen, die in einem privaten Repository bleiben, niemandem etwas. Die Änderungen müssen vom Maintainer akzeptiert und für andere zum Benutzen und Verwerten zur Verfügung gestellt werden. Die Entwickler müssen ihre Arbeit auf den veröffentlichten Zweigen in den Repositories aufbauen, die vom Maintainer angeboten werden. Im Kontext eines abgeleiteten Klon-Repository wird der Maintainer normalerweise »upstream« in Bezug auf die Entwickler angenommen. Da Git völlig symmetrisch ist, hält nichts einen Entwickler davon ab, sich selbst als Maintainer für andere Entwickler zu betrachten, die sich weiter »unten« befinden. Er muss sich aber darüber im Klaren sein, dass er sich in der Mitte sowohl eines Upstreamals auch eines Downstream-Datenflusses befindet, und muss daher in dieser doppelten Rolle sowohl den Maintainer- als auch den Entwicklervertrag beachten (siehe nächster Abschnitt). Da eine doppelte oder gemischte Rolle möglich ist, können die Orte Upstream und Downstream strenggenommen nicht nur als Produzent oder Abnehmer angesehen werden. Man kann Änderungen erzeugen, die entweder upstream oder downstream gehen sollen.
Die Maintainer-Entwickler-Interaktion Die Beziehung zwischen einem Maintainer und einem Entwickler ist oft locker und schlecht definiert, allerdings gibt es einen impliziten Vertrag zwischen den beiden: Der Maintainer veröffentlicht Zweige, die der Entwickler als Basis verwenden kann. Sobald sie veröffentlicht sind, geht der Maintainer die unausgesprochene Verpflichtung ein, die veröffentlichten Zweige nicht zu verändern, da das die Basis stören würde, auf der die Entwicklung stattfindet. In der entgegengesetzten Richtung stellt der Entwickler sicher (indem er die veröffentlichten Zweige als seine Basis verwendet), dass seine Änderungen sauber ohne Probleme oder Konflikte angewandt werden, wenn er sie zur Integration an den Maintainer schickt.
242 | Kapitel 12: Repository-Verwaltung
Es mag so aussehen, als würde das einen exklusiven, mit Sperren arbeitenden Prozess erfordern. Einmal veröffentlicht, kann der Maintainer nichts tun, bis der Entwickler wieder Änderungen schickt. Und dann, nachdem der Maintainer die Updates von einem Entwickler angewendet hat, hat sich der Zweig zwangsläufig geändert und verletzt daher den »darf den Zweig nicht ändern«-Vertrag für andere Entwickler. Wenn das so wäre, könnte niemals eine wirklich verteilte, parallele und unabhängige Arbeit stattfinden. Zum Glück ist es nicht gar so schlimm! Stattdessen ist Git in der Lage, auf den CommitVerlauf der betroffenen Zweige zurückzuschauen, die Merge-Basis zu ermitteln, die als Ausgangspunkt für die Änderungen eines Entwicklers verwendet wurde, und sie selbst dann anzuwenden, wenn in der Zwischenzeit Änderungen von anderen Entwicklern durch den Maintainer eingebaut wurden. Wenn mehrere Entwickler unabhängig voneinander Änderungen vornehmen und diese dann in einem gemeinsamen Repository zusammengeführt werden, kann es zu Konflikten kommen. Der Maintainer muss darüber entscheiden und solche Probleme lösen. Der Maintainer kann diese Konflikte entweder direkt auflösen oder die Änderungen von einem Entwickler zurückweisen, falls sie Konflikte verursachen.
Rollendualität Es gibt zwei grundlegende Mechanismen zum Übertragen von Commits zwischen einem Upstream- und einem Downstream-Repository. Der erste verwendet git push oder git pull, um die Commits direkt zu übertragen, während der zweite git format-patch und git am einsetzt, um Repräsentationen der Commits zu senden und zu empfangen. Welche Methode Sie benutzen, wird hauptsächlich von den Abmachungen innerhalb Ihres Entwicklungsteams und zu einem gewissen Grad von direkten Zugriffsrechten bestimmt (wie in Kapitel 11 besprochen). Wenn man git format-patch und git am benutzt, um Patches anzuwenden, erreicht man den gleichen Blob- und Baumobjektinhalt, als wenn man die Änderungen über git push abgeliefert oder über git pull eingebaut hätte. Allerdings unterscheidet sich das eigentliche Commit-Objekt, da die Metadateninformationen für das Commit zwischen einem Push oder Pull und einer entsprechenden Anwendung eines Patch unterschiedlich sind. Mit anderen Worten: Indem man Push oder Pull benutzt, um die Änderungen von einem Repository zu einem anderen zu verbreiten, kopiert man dieses Commit exakt, während beim Einsatz eines Patch nur die Datei- und Verzeichnisdaten exakt kopiert werden. Darüber hinaus können Push und Pull auch Merge-Commits zwischen Repositories verbreiten. Merge-Commits können nicht als Patches verschickt werden. Da Git die Baum- und Blob-Objekte vergleicht und verarbeitet, ist es in der Lage zu verstehen, dass zwei unterschiedliche Commits für die gleiche zugrunde liegende Änderung in zwei verschiedenen Repositories oder sogar auf unterschiedlichen Zweigen innerhalb
Wissen, wo Sie stehen
| 243
desselben Repository in Wirklichkeit dieselbe Änderung repräsentieren. Es stellt daher für zwei verschiedene Entwickler kein Problem dar, den gleichen Patch anzuwenden, der per E-Mail an zwei unterschiedliche Repositories gesandt wurde. Solange der resultierende Inhalt gleich ist, behandelt Git die Repositories so, als hätten sie den gleichen Inhalt. Wir wollen einmal schauen, wie diese Rollen und Datenflüsse zusammenwirken, um eine Dualität zwischen Upstream- und Downstream-Produzenten und -Abnehmern zu formen: Upstream-Abnehmer Ein Upstream-Abnehmer ist ein Entwickler, der sich oberhalb (upstream) von Ihnen befindet und Ihre Änderungen entweder als Patches oder als Pull-Anforderungen entgegennimmt. Ihre Patches sollten mit einem Rebase auf den aktuellen Zweig-HEAD des Abnehmers verlagert werden. Ihre Pull-Anforderungen sollten entweder direkt Merge-fähig sein oder bereits von Ihnen in Ihrem Repository zusammengeführt worden sein. Ein Merge vor dem Pull stellt sicher, dass Konflikte bereits korrekt von Ihnen aufgelöst wurden, wodurch dem Upstream-Abnehmer diese Mühe erspart bleibt. Diese Upstream-Abnehmerrolle könnte ein Maintainer übernehmen, der sich umdreht und das veröffentlicht, was er gerade aufgenommen hat. Downstream-Abnehmer Ein Downstream-Abnehmer ist ein Entwickler, der sich unterhalb (downstream) von Ihnen befindet und Ihr Repository als Basis für seine Arbeit benutzt. Ein Downstream-Abnehmer möchte solide, veröffentlichte Topic-Zweige haben. Sie sollten den Verlauf eines veröffentlichten Zweigs nicht umschreiben, modifizieren oder mit einem Rebase umlagern. Upstream-Produzent/Herausgeber Ein Upstream-Herausgeber ist eine Person, die sich oberhalb (upstream) von Ihnen befindet und Repositories veröffentlicht, die die Grundlage für Ihre Arbeit bilden. Wahrscheinlich handelt es sich um einen Maintainer, wobei die stillschweigende Erwartung besteht, dass er Ihre Änderungen akzeptiert. Die Rolle des Upstream-Herausgebers besteht darin, Änderungen zu sammeln und Zweige zu veröffentlichen. Auch hier sollten die Verläufe der veröffentlichten Zweige nicht verändert werden, da sie die Grundlage für die weiter unterhalb gelagerte Entwicklung bilden. Ein Maintainer in dieser Rolle erwartet Entwickler-Patches zum Anwenden und PullAnforderungen zum sauberen Zusammenführen. Downstream-Produzent/Herausgeber Ein Downstream-Herausgeber ist ein Entwickler, der sich unterhalb (downstream) von Ihnen befindet und Änderungen entweder als Patch oder als Pull-Anforderung veröffentlicht hat. Ziel des Downstream-Herausgebers ist, dass Sie seine Änderungen in Ihrem Repository akzeptieren. Ein Downstream-Herausgeber nimmt TopicZweige von Ihnen ab und möchte, dass diese Zweige stabil bleiben, dass also der Verlauf nicht umgeschrieben oder verlagert wird. Downstream-Herausgeber sollten
244 | Kapitel 12: Repository-Verwaltung
regelmäßig Updates aus der Upstream-Richtung holen und Entwicklungs-TopicZweige ebenfalls regelmäßig mit Merges zusammenführen oder durch Rebasing verlagern, damit sichergestellt ist, dass sie zu den lokalen Upstream-Zweig-HEADs passen. Ein Downstream-Herausgeber kann seine eigenen lokalen Topic-Zweige jederzeit umlagern, weil es für einen Upstream-Abnehmer keine Rolle spielt, dass dieser Entwickler mehrere Iterationen gebraucht hat, um einen guten Patch-Satz herzustellen, der einen sauberen, unkomplizierten Verlauf aufweist.
Mit mehreren Repositories arbeiten Ihr eigener Arbeitsbereich Als Entwickler von Inhalten für ein Projekt mithilfe von Git sollten Sie ihre eigene private Kopie bzw. Ihren Klon eines Repository anlegen, um die Entwicklung durchzuführen. Dieses Entwicklungs-Repository sollte als Ihr eigener Arbeitsbereich dienen, in dem Sie Änderungen vornehmen können, ohne Gefahr zu laufen, dass Sie mit einem anderen Entwickler kollidieren, ihn unterbrechen oder anderweitig stören. Da außerdem jedes Git-Repository eine vollständige Kopie des gesamten Projekts sowie den gesamten Verlauf des Projekts enthält, können Sie es behandeln, als wäre es völlig und ausschließlich Ihr Repository. Im Prinzip ist es das ja auch! Ein Vorteil dieses Paradigmas besteht darin, dass es jedem Entwickler die vollständige Kontrolle über seinen Arbeitsverzeichnisbereich gibt, sodass er Änderungen an einem Teil oder sogar am kompletten System durchführen kann, ohne dass er sich Sorgen über Wechselwirkungen mit anderen Entwicklungsarbeiten machen muss. Wenn Sie einen Teil ändern müssen, haben Sie den Teil und können ihn in Ihrem Repository bearbeiten, ohne dass Sie andere Entwickler beeinflussen. Falls Sie später bemerken, dass Ihre Arbeit sinnlos oder irrelevant ist, können Sie sie wegwerfen, ohne dass Sie irgendjemanden oder ein anderes Repository beeinträchtigen. Wie bei jeder Softwareentwicklung soll das nicht zu wilden Experimenten ermutigen. Bedenken Sie immer die Konsequenzen Ihrer Änderungen, weil Sie sie irgendwann in das Master-Repository überführen müssen. In diesem Augenblick müssen Sie die Zeche zahlen, und da kann es passieren, dass wirre Änderungen sich rächen.
Wo Sie Ihr Repository starten sollten Angesichts der Masse an Repositories, die schließlich zu einem Projekt beitragen, scheint es schwierig zu sein festzustellen, wo Sie Ihre Entwicklung durchführen sollten. Sollten Ihre Beiträge direkt auf dem »Haupt«-Repository aufbauen? Oder vielleicht auf dem Repository, in dem sich andere Leute auf eine spezielle Funktion oder Eigenschaft konzentrieren? Oder vielleicht auf dem »stabilen« Zweig irgendeines Release-Repository?
Mit mehreren Repositories arbeiten
| 245
Ohne eine klare Vorstellung davon, wie Git Repositories anspricht, benutzt und verändert, geraten Sie möglicherweise in eine Form des »ich kann nicht anfangen, weil ich Angst habe, den falschen Ausgangspunkt zu wählen«-Dilemmas. Oder vielleicht haben Sie die Entwicklung bereits in einem Klon gestartet, der auf einem von Ihnen gewählten Repository beruht, und merken nun, dass es nicht das »richtige« war. Sicher, es hat mit dem Projekt zu tun und ist vielleicht sogar ein »guter« Startpunkt, aber es gibt eine fehlende Funktion, die in einem anderen Repository zu finden ist. Vielleicht sind Sie schon tief in Ihrem Entwicklungszyklus, bis Sie das beurteilen können. Ein anderes häufig auftretendes Startpunkt-Dilemma rührt daher, dass Projekteigenschaften benötigt werden, die aktiv in zwei unterschiedlichen Repositories entwickelt werden. Keines von ihnen bildet von sich aus die korrekte Klonbasis für Ihre Arbeit. Sie könnten sich einfach mit der Erwartung vorankämpfen, dass Ihre Arbeit und die Arbeit aus den verschiedenen anderen Repositories schließlich in einem Master-Repository vereint und zusammengeführt werden wird. Natürlich können Sie das. Aber bedenken Sie, dass einer der Vorteile einer verteilten Entwicklungsumgebung die Fähigkeit ist, gleichzeitige Entwicklungen durchzuführen. Nutzen Sie die Tatsache aus, dass die anderen veröffentlichten Repositories mit frühen Versionen ihrer Arbeit zur Verfügung stehen. Ein weiterer Fallstrick zeigt sich, wenn Sie mit einem Repository beginnen, das ganz an der Spitze Ihrer Entwicklung steht, und Sie feststellen, dass es zu instabil ist, um Ihre Arbeit zu unterstützen – oder dass es mitten in Ihrer Arbeit aufgegeben wurde. Zum Glück unterstützt Git ein Modell, bei dem Sie im Prinzip ein beliebiges Repository aus einem Projekt – selbst wenn es nicht perfekt ist – als Startpunkt wählen und dieses dann konvertieren, verwandeln oder erweitern können, bis es alle gewünschten Funktionen enthält. Wenn Sie später Ihre Änderungen wieder in die entsprechenden verschiedenen UpstreamRepositories aufteilen wollen, müssen Sie vernünftigen und sorgfältigen Gebrauch von separaten Topic-Zweigen und Merges machen, damit alles glattgeht. Einerseits können Sie Zweige aus mehreren entfernten Repositories holen und in Ihrem eigenen kombinieren, wodurch Sie die richtige Mischung aus Funktionen erhalten, die in anderen vorhandenen Repositories zur Verfügung stehen. Andererseits können Sie den Startpunkt in Ihrem Repository wieder zurück auf einen stabilen Punkt weiter vorn im Verlauf der Entwicklung des Projekts setzen.
In ein anderes Upstream-Repository konvertieren Am einfachsten funktioniert das Mischen und Zuordnen von Repositories, indem man das Basis-Repository (normalerweise als Klon-Origin bezeichnet) wechselt, also dasjenige, das Sie als Ihren Ursprung betrachten und mit dem Sie regelmäßig synchronisieren. Nehmen Sie z.B. an, dass Sie an Funktion F arbeiten müssen und beschließen, Ihr Repository von der Hauptlinie M zu klonen, wie in Abbildung 12-1 gezeigt wird.
246 | Kapitel 12: Repository-Verwaltung
M
F Abbildung 12-1: Einfacher Klon zum Entwickeln von Funktion F
Sie arbeiten eine Weile, bevor Sie merken, dass es einen besseren Ausgangspunkt gibt, der sich näher an dem befindet, was Sie wirklich wollen, allerdings liegt er in Repository P. Ein Grund für diese Art von Wechsel könnte sein, dass Sie Unterstützung für eine Funktionalität oder Eigenschaft erhalten wollen, die bereits in Repository P vorliegt. Ein anderer Grund hat eher etwas mit der Langzeitplanung zu tun. Irgendwann kommt der Zeitpunkt, an dem Sie die Entwicklung, die Sie in Repository F erledigt haben, wieder in irgendein Upstream-Repository übertragen müssen. Wird der Maintainer von Repository M Ihre Änderungen direkt akzeptieren? Vielleicht nicht. Wenn Sie zuversichtlich sind, dass der Maintainer von Repository P sie akzeptiert, dann sollten Sie es so einrichten, dass Ihre Patches stattdessen sofort in diesem Repository anwendbar sind. Wahrscheinlich wurde P einst von M geklont oder umgekehrt, wie Abbildung 12-2 zeigt. Letztendlich bauten P und M an irgendeinem Punkt in der Vergangenheit auf demselben Repository für dasselbe Projekt auf. M P F Abbildung 12-2: Zwei Klone eines Repository
Die oft gestellte Frage lautet, ob Repository F, das ursprünglich auf M beruhte, so konvertiert werden kann, dass es nun auf Repository P basiert (siehe Abbildung 12-3). Mit Git lässt sich das leicht bewerkstelligen, weil es eine Peer-to-Peer-Beziehung zwischen Repositories unterstützt und die Fähigkeit besitzt, Zweige mit einem Rebase umzulagern. Ein praktisches Beispiel: Die Kernel-Entwicklung für eine bestimmte Architektur könnte direkt aus der Hauptlinie des Linus-Kernel-Repository heraus erfolgen. Allerdings würde Linus das nicht übernehmen. Falls Sie etwa an PowerPC-Änderungen arbeiten und das nicht wissen, werden Ihre Änderungen mit hoher Wahrscheinlichkeit nicht einfach so akzeptiert werden.
Mit mehreren Repositories arbeiten
| 247
M P F Abbildung 12-3: Funktion F, umstrukturiert für Repository P
Die PowerPC-Architektur wird jedoch momentan von Ben Herrenschmidt verwaltet; er ist dafür verantwortlich, die PowerPC-spezifischen Änderungen zu sammeln und wiederum nach oben an Linus zu senden. Damit Ihre Änderungen in das Haupt-Repository gelangen, müssen Sie zuerst Bens Repository durchlaufen. Sie sollten es daher so einrichten, dass Ihre Patches direkt auf sein Repository angewandt werden können – und dazu ist es niemals zu spät. In gewisser Weise weiß Git, wie es zwischen Repositories unterscheidet. Im Rahmen des Peer-to-Peer-Protokolls zum Abrufen von Zweigen aus einem anderen Repository kommt es zum Austausch von Informationen, die besagen, welche Änderungen ein Repository besitzt und welche ihm fehlen. Entsprechend ist Git ist der Lage, nur die fehlenden oder neuen Änderungen zu holen und in Ihr Repository zu übertragen. Git kann außerdem den Verlauf der Zweige untersuchen und feststellen, wo sich die gemeinsamen Vorfahren der unterschiedlichen Zweige befinden, selbst wenn sie aus unterschiedlichen Repositories stammen. Wenn sie einen gemeinsamen Commit-Vorfahren haben, findet Git ihn und konstruiert eine große, vereinheitlichte Ansicht des Commit-Verlaufs mit allen Repository-Änderungen.
Mehrere Upstream-Repositories benutzen Nehmen Sie als weiteres Beispiel an, dass die allgemeine Repository-Struktur aussieht wie in Abbildung 12-4. Hier wird in einem Haupt-Repository namens M schließlich die gesamte Entwicklung für zwei unterschiedliche Funktionen aus den Repositories F1 und F2 gesammelt. M
F1 Abbildung 12-4: Zwei Funktions-Repositories
248 | Kapitel 12: Repository-Verwaltung
F2
Sie müssen jedoch eine Superfunktion S entwickeln, die Aspekte von Eigenschaften umfasst, die nur in F1, sowie von Eigenschaften, die nur in F2 zu finden sind. Sie könnten warten, bis F1 in M überführt wurde, und anschließend, bis F2 ebenfalls mit M vereinigt wurde. Auf diese Weise haben Sie dann ein Repository mit der korrekten Gesamtbasis für Ihre Arbeit. Solange allerdings das Projekt nicht unbedingt einen Projektlebenszyklus verlangt, der Merges als bekannte Intervalle erfordert, kann man nicht sagen, wie lange dieser Vorgang dauern wird. Sie könnten Ihr Repository S auf der Grundlage der Funktionen starten, die in F1 oder alternativ in F2 zu finden sind, wie in Abbildung 12-5 gezeigt wird. Mit Git ist es jedoch möglich, stattdessen ein Repository S zu konstruieren, das sowohl F1 als auch F2 enthält; dieses ist in Abbildung 12-6 zu sehen. M
or
F1
M
F1
F2
F2
S
S
Abbildung 12-5: Mögliche Start-Repositories für S
M
F1
F2
S Abbildung 12-6: Kombiniertes Start-Repository für S
In diesen Bildern ist es unklar, ob Repository S aus der Gesamtheit von F1 und F2 entstanden ist oder nur aus einem Teil der beiden. Git unterstützt beide Szenarien. Nehmen Sie
Mit mehreren Repositories arbeiten
| 249
an, Repository F2 hat die Zweige F2A und F2B mit den Funktionen A bzw. B (siehe Abbildung 12-7). Falls Ihre Entwicklung Funktion A benötigt, nicht aber B, können Sie selektiv nur diesen Zweig F2A in Ihr Repository S holen, sowie den Teil von F1, der ebenfalls gebraucht wird. M
F1
F2 F2A F2B S
Abbildung 12-7: Zwei Funktionszweige in F2
Wieder demonstriert die Struktur des Linux-Kernels dieses Problem. Nehmen wir einmal an, Sie arbeiten an einem neuen Netzwerktreiber für ein neues PowerPC-Board. Wahrscheinlich haben Sie architekturspezifische Änderungen für das Board, die Code aus dem PowerPC-Repository brauchen, das von Ben verwaltet wird. Außerdem müssen Sie wahrscheinlich das »netdev«-Repository für die Netzwerkentwicklung bemühen, das von Jeff Garzik gepflegt wird. Git holt und erzeugt ein gemeinsames Repository mit Zweigen aus den Repositories von Ben und Jeff. Wenn beide Basiszweige sich in Ihrem Repository befinden, können Sie sie zusammenführen und weiter auf ihnen entwickeln.
Projekte aufspalten Das Klonen eines Repository kann als Aufspalten (Forking) des Projekts betrachtet werden. Das Aufspalten oder Aufgabeln ist funktionell gesehen äquivalent zum »Verzweigen« in einigen anderen Versionskontrollsystemen, allerdings kennt Git außerdem noch ein gesondertes Konzept namens »Verzweigen«, weshalb Sie es nicht so nennen sollten. Im Gegensatz zu einem Zweig besitzt ein Git-Fork nicht direkt einen Namen. Stattdessen verweisen Sie einfach über das Dateisystemverzeichnis (oder den entfernten Server oder die URL) darauf, in das Sie geklont haben. Der Begriff »Fork« (Gabelung) stammt von der Idee, dass man beim Erzeugen eines Fork zwei simultane Pfade herstellt, denen die Entwicklung folgt. Es ist praktisch eine Gabelung in der Straße der Entwicklung. Wie Sie sich vorstellen können, beruht der Begriff
250 | Kapitel 12: Repository-Verwaltung
»Zweig« auf einer ähnlichen Analogie in Bezug auf Bäume. Es gibt im Prinzip keinen Unterschied zwischen den Metaphern der Verzweigung und der Gabelung – die Begriffe erfassen einfach zwei Ziele. Konzeptuell gesehen, besteht der Unterschied darin, dass das Verzweigen normalerweise innerhalb eines einzigen Repository geschieht, während das Aufgabeln auf der gesamten Repository-Ebene stattfindet. Man kann zwar ein Projekt mit Git leicht aufspalten, allerdings liegt dieser Aktion dann eher eine soziale oder politische Entscheidung zugrunde als eine technische. Für öffentliche oder Open Source-Projekte ist der mögliche Zugang zu einer Kopie oder einem Klon des gesamten Repository mitsamt seinem Verlauf sowohl eine Ermunterung zum Aufspalten als auch eine Abschreckung davor. GitHub.com (http://github.com/guides/home), ein online verfügbarer GitHosting-Dienst, treibt diese Idee in das logische Extrem: Jedermanns Version wird als Fork betrachtet, und alle Forks werden zusammen am selben Ort gezeigt.
Ist das Aufspalten eines Projekts nicht etwas Schlechtes? In der Vergangenheit war das Aufspalten eines Projekts oft durch das Gefühl von Machtgier, den Unwillen zur Kooperation oder die Ablehnung eines Projekts motiviert. Eine schwierige Person im Mittelpunkt eines zentralisierten Projekts konnte im Prinzip alles zum Stillstand bringen. Es konnte sich ein Spalt auftun zwischen denen, die für das Projekt verantwortlich waren, und denen, die es nicht waren. Oft bestand die scheinbar einzige Lösung darin, ein neues Projekt abzuspalten. In einem solchen Szenario war es mitunter schwierig, eine Kopie des Projektverlaufs zu erhalten und wieder anzufangen. Aufspalten ist der traditionelle Begriff für das, was passiert, wenn ein Entwickler eines Open Source-Projekts mit dem wesentlichen Fortgang der Entwicklung nicht mehr zufrieden ist, eine Kopie des Quellcodes nimmt und damit beginnt, seine eigene Version zu basteln. In diesem Sinne wurde das Aufspalten als etwas Negatives betrachtet; es bedeutet, dass der unzufriedene Entwickler keine Möglichkeit finden konnte, vom Hauptprojekt das Gewünschte zu erhalten. Deshalb zieht er von dannen und versucht, es besser zu machen. Allerdings gibt es nun zwei Projekte, die fast gleich sind. Offensichtlich ist keines gut genug für die Allgemeinheit, sonst hätte man ja eines davon abgebrochen. Aus diesem Grund unternehmen die meisten Open Source-Projekte heldenhafte Anstrengungen, das Aufspalten zu vermeiden. Aufspalten kann »schlecht« sein oder auch nicht. Einerseits ist vielleicht eine alternative Sicht und eine neue Führung genau das, was nötig ist, um ein Projekt wieder zum Leben zu erwecken. Andererseits könnte es in einer Entwicklung auch einfach nur Unfrieden und Verwirrung stiften.
Mit mehreren Repositories arbeiten
| 251
Forks wieder in Übereinstimmung bringen Im Gegensatz dazu versucht Git, das Stigma des Aufspaltens zu entfernen. Das wirkliche Problem beim Aufspalten eines Projekts ist nicht die Schaffung eines alternativen Entwicklungspfades. Immer wenn ein Entwickler eine Kopie eines Projekts herunterlädt oder klont und anfängt, darin herumzuprogrammieren, hat er einen »alternativen Entwicklungspfad« erzeugt, wenn auch nur temporär. Bei seiner Arbeit am Linux-Kernel merkte Linus Torvalds irgendwann, dass das Aufspalten nur dann ein Problem darstellt, wenn die Forks am Ende nicht wieder zusammengeführt werden. Daher hat er Git so entworfen, dass es einen völlig anderen Blickwinkel auf das Aufspalten hat: Git ermutigt zum Aufspalten. Allerdings macht es Git auch jedermann leicht, zwei Forks wieder zusammenzuführen. In technischer Hinsicht wird das Abstimmen eines aufgespaltenen Projekts mit Git durch seine Unterstützung für das groß angelegte Abrufen und Importieren eines Repository in ein anderes und für das außerordentlich einfache Zusammenführen von Zweigen befördert. Es bleiben zwar möglicherweise noch viele soziale Probleme, aber vollständig verteilte Repositories scheinen die Spannung zu reduzieren, indem die vorgebliche Bedeutung der Person im Zentrum eines Projekts vermindert wird. Da ein ambitionierter Entwickler leicht ein Projekt und dessen kompletten Verlauf übernehmen kann, glaubt er möglicherweise, dass es ausreicht zu wissen, dass die Person im Zentrum notfalls ersetzt und die Entwicklung fortgeführt werden könnte!
252 | Kapitel 12: Repository-Verwaltung
Kapitel 13
KAPITEL 13
Patches
Das als Peer-to-Peer-Versionskontrollsystem entworfene Git erlaubt das direkte Übertragen der Entwicklungsarbeit von einem Repository zum nächsten, und zwar mithilfe sowohl eines Push- als auch eines Pull-Modells. Git implementiert sein eigenes Übertragungsprotokoll zum Austauschen der Daten zwischen Repositories. Aus Effizienzgründen (um Zeit und Geld zu sparen) führt das GitÜbertragungsprotokoll einen kleinen Handshake aus, stellt fest, welche Commits aus dem Quell-Repository im Ziel fehlen, und überträgt schließlich eine binäre, komprimierte Form der Commits. Das empfangende Repository baut die neuen Commits in seinen lokalen Verlauf ein, erweitert seinen Commit-Graphen und aktualisiert seine Zweige und Tags, wo es nötig ist. In Kapitel 11 wurde erwähnt, dass für den Austausch von Entwicklungen zwischen Repositories auch HTTP verwendet werden kann. HTTP ist nicht annähernd so effizient wie das Git-eigene Protokoll, kann aber auch Commits hin- und herbewegen. Beide Protokolle sorgen dafür, dass ein übertragenes Commit in den Quell- und Ziel-Repositories identisch bleibt. Allerdings bilden das Git-eigene Protokoll und das HTTP-Protokoll nicht die einzigen Mechanismen zum Austauschen von Commits und zum Synchronhalten von verteilten Repositories. Manchmal kommt es sogar vor, dass diese Protokolle gar nicht benutzt werden können. Zurückgreifend auf bewährte Methoden aus einer früheren Unix-Entwicklungsära unterstützt Git auch eine »Patchen-und-Anwenden«-Operation, bei der der Datenaustausch typischerweise per E-Mail erfolgt. Git implementiert drei spezielle Befehle, um den Austausch eines Patch zu ermöglichen: • git format-patch generiert einen Patch in E-Mail-Form. • git send-email sendet einen Git-Patch über einen SMTP-Feed. • git am wendet einen Patch an, der in einer E-Mail gefunden wird.
| 253
Das zugrunde liegende Anwendungsszenario ist relativ einfach. Sie und ein oder mehrere Entwickler beginnen mit einem Klon eines gemeinsamen Repository und starten Ihre Zusammenarbeit. Sie erledigen einige Dinge, führen einige Commits in Ihre Kopie des Repository aus und beschließen irgendwann, dass es Zeit wird, Ihre Änderungen an Ihre Partner zu übermitteln. Sie wählen die Commits aus, die Sie freigeben möchten, und entscheiden, mit wem Sie Ihre Arbeit teilen wollen. Da die Patches per E-Mail verschickt werden, kann jeder der vorgesehenen Empfänger entscheiden, ob er keinen, einige oder alle Patches anwendet. In diesem Kapitel wird erläutert, wann Sie Patches einsetzen könnten und wie Sie einen Patch generieren, senden und (falls Sie ein Empfänger sind) anwenden.
Wozu Patches benutzen? Obwohl das Git-Protokoll viel effizienter ist, gibt es wenigstens zwei zwingende Gründe, sich der zusätzlichen Mühe zu unterziehen, die der Einsatz von Patches erfordert (einen technischen und einen sozialen). • Manchmal kann weder das Git-eigene Protokoll noch das HTTP-Protokoll verwendet werden, um Daten zwischen Repositories in Push- oder Pull-Richtung oder in beiden Richtungen auszutauschen. Z.B. könnte es eine Unternehmens-Firewall verbieten, eine Verbindung zu einem externen Server mit dem Git-Protokoll oder -Port zu öffnen. Möglicherweise ist SSH auch keine Option. Und selbst wenn HTTP erlaubt ist, was meist der Fall ist, könnten Sie zwar Repositories herunterladen und Updates holen, aber keine Änderungen zurückschieben. In solchen Situationen bilden E-Mails das perfekte Medium zum Übermitteln von Patches. • Einer der großen Vorteile des Peer-to-Peer-Entwicklungsmodells ist die Zusammenarbeit. Patches, vor allem solche, die an eine öffentliche Mailingliste geschickt werden, stellen eine Methode dar, mit der vorgesehene Änderungen zur allgemeinen Begutachtung verteilt werden können. Bevor die Patches dauerhaft auf ein Repository angewandt werden, können andere Entwickler die veröffentlichten Patches diskutieren, kritisieren, umarbeiten, testen und entweder annehmen oder ablehnen. Da die Patches genaue Änderungen repräsentieren, lassen sich akzeptable Patches direkt in einem Repository einsetzen. Selbst wenn Ihnen Ihre Entwicklungsumgebung bequemerweise einen direkten Push- oder Pull-Austausch erlaubt, könnten Sie auf das »Patchen-E-Mail-BewertenAnwenden«-Paradigma zurückgreifen, um die Vorteile einer allgemeinen Bewertung auszunutzen. Sie könnten sogar eine Projektrichtlinie in Betracht ziehen, bei der die Änderungen der Entwickler sich zuerst als Patches einer Bewertung auf einer Mailingliste unterziehen müssen, bevor sie direkt mit git pull oder git push in das Repository über-
254 | Kapitel 13: Patches
führt werden. Sozusagen das Beste aus beiden Welten: alle Vorteile der Überprüfung durch die Kollegen sowie das einfache direkte Einbinden der Änderungen! Und es gibt noch weitere Gründe, Patches zu benutzen. Genau wie Sie ein Commit aus einem Ihrer eigenen Zweige auswählen und auf einen anderen Zweig anwenden, können Sie mit Patches selektiv Commits aus dem Repository eines anderen Entwicklers auswählen, ohne dass Sie alles holen und mit einem Merge bei sich einbauen müssen. Natürlich könnten Sie den anderen Entwickler bitten, die gewünschten Commits in einen eigenen Zweig zu packen, und dann nur diesen Zweig holen und in Ihr Repository überführen, oder Sie könnten sein gesamtes Repository holen und sich nur die gewünschten Commits aus den Tracking-Zweigen herauspicken. Aber vielleicht haben Sie ja Ihre Gründe, nicht das Repository zu holen. Wenn Sie ein gelegentliches oder explizites Commit – etwa einen einzelnen Bug-Fix oder eine bestimmte Funktion – haben wollen, stellt das Anwenden des vorhandenen Patch die direkteste Methode dar, um diese spezielle Verbesserung zu erhalten.
Patches generieren Der Befehl git format-patch generiert einen Patch in Form einer E-Mail-Nachricht. Er erzeugt eine E-Mail für jedes Commit, das Sie angeben. Sie können die Commits mit einer der Techniken festlegen, die im Abschnitt »Commits festlegen« auf Seite 73 besprochen wurden. Das hier sind gebräuchliche Anwendungsfälle: • Eine festgelegte Anzahl von Commits, etwa -2 • Ein Commit-Bereich, etwa master~4..master~2 • Ein einzelnes Commit, oft der Name eines Zweigs, etwa origin/master Im Kern des git format-patch-Befehls ist zwar die Git-Diff-Maschinerie am Werk, aber sie unterscheidet sich in zwei wesentlichen Beziehungen von git diff: • Während git diff einen Patch mit den kombinierten Unterschieden aller ausgewählten Commits generiert, erzeugt git format-patch eine E-Mail-Nachricht für jedes ausgewählte Commit. • git diff stellt keine E-Mail-Header her. Zusätzlich zu dem eigentlichen Diff-Inhalt generiert git format-patch eine E-Mail-Nachricht, komplett mit Headern, die den Commit-Autor, das Commit-Datum und die Commit-Lognachricht auflistet, die mit der Änderung verknüpft sind. git format-patch und git log dürften sehr ähnlich wirken. Vergleichen Sie als interessantes Experiment einmal die Ausgabe der folgenden beiden Befehle: git format-patch -1 und git log -p -1 --pretty=email.
Patches generieren
| 255
Wir wollen mit einem relativ einfachen Beispiel beginnen. Nehmen Sie an, Sie haben ein Repository mit nur einer Datei darin, die Datei heißt. Der Inhalt dieser Datei besteht aus einer Reihe von Großbuchstaben, und zwar A bis D. Die Buchstaben wurden zeilenweise in die Datei eingefügt und mit einer Lognachricht bestätigt, die dem Buchstaben entspricht: $ $ $ $ $ $ $
git init echo A > Datei git add Datei git commit -mA echo B >> Datei ; git commit -mB Datei echo C >> Datei ; git commit -mC Datei echo D >> Datei ; git commit -mD Datei
Der Commit-Verlauf zeigt daher vier Commits: $ git show-branch --more=4 master [master] D [master^] C [master~2] B [master~3] A
Am einfachsten generiert man Patches für die neuesten n Commits mit der Option -n: $ git format-patch -1 0001-D.patch $ git format-patch -2 0001-C.patch 0002-D.patch $ git format-patch -3 0001-B.patch 0002-C.patch 0003-D.patch
Standardmäßig generiert Git jeden Patch in seiner eigenen Datei, deren Name von der Commit-Logmeldung abgeleitet und außerdem durchnummeriert ist. Der Befehl gibt die Dateinamen während der Ausführung aus. Um mehrere Commits als Patches zu formatieren, können Sie auch einen CommitBereich angeben. Nehmen Sie einmal an, dass Sie davon ausgehen, dass die Repositories anderer Entwickler auf Commit B Ihres Repository basieren. Nun wollen Sie deren Repositories mit allen Änderungen versorgen, die Sie zwischen B und D vorgenommen haben. Ausgehend von der vorangegangenen Ausgabe von git show-branch sehen Sie, dass B den Versionsnamen master~2 und D den Versionsnamen master trägt. Geben Sie diese Namen als Commit-Bereich im Befehl git format-patch an. Obwohl Sie drei Commits in den Bereich einschließen (B, C und D), erhalten Sie nur zwei E-Mail-Nachrichten, die zwei Commits repräsentieren: Die erste enthält die Diffs zwischen B und C, die zweite die zwischen C und D (siehe Abbildung 13-1).
256 | Kapitel 13: Patches
Hier ist die Ausgabe des Befehls: $ git format-patch master~2..master 0001-C.patch 0002-D.patch
Jede Datei ist eine einzelne E-Mail, die netterweise in der Reihenfolge nummeriert ist, in der sie nachfolgend angewandt werden sollte. Hier ist der erste Patch: $ cat 0001-C.patch From 69003494a4e72b1ac98935fbb90ecca67677f63b Mon Sep 17 00:00:00 2001 From: Jon Loeliger <[email protected]> Date: Sun, 28 Dec 2008 12:10:35 -0600 Subject: [PATCH] C --Datei | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diff --git a/Datei b/Datei index 35d242b..b1e6722 100644 --- a/Datei +++ b/Datei @@ -1,2 +1,3 @@ A B +C -1.6.0.90.g436ed
Revision
master~2
master~1
master
Commit
D
C
D
Diff
diff between B and C
diff between C and D
Patch
0001-C.patch
0002-D.patch
Abbildung 13-1: git format-patch mit einem Commit-Bereich
Und hier der zweite: $ cat 0002-D.patch From 73ac30e21df1ebefd3b1bca53c5e7a08a5ef9e6f Mon Sep 17 00:00:00 2001 From: Jon Loeliger <[email protected]> Date: Sun, 28 Dec 2008 12:10:42 -0600
Patches generieren
| 257
Subject: [PATCH] D --Datei | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diff --git a/Datei b/Datei index b1e6722..8422d40 100644 --- a/Datei +++ b/Datei @@ -1,3 +1,4 @@ A B C +D -1.6.0.90.g436ed
Lassen Sie uns mit dem Beispiel fortfahren. Es wird nun etwas komplexer, da wir den weiteren Zweignamen alt hinzufügen, der auf Commit B basiert. Während der master-Entwickler mit den Zeilen C und D einzelne Commits in den masterZweig eingebracht hat, hat der alt-Entwickler die Commits (und Zeilen) X, Y und Z zu seinem Zweig hinzugefügt: # Zweig alt bei Commit B erzeugen $ git checkout -b alt e587667 $ echo X >> Datei ; git commit -mZ Datei $ echo Y >> Datei ; git commit -mY Datei $ echo Z >> Datei ; git commit -mZ Datei
Der Commit-Graph sieht so aus wie Abbildung 13-2. A
B
C
X
D
Y
master
Z
alt
Abbildung 13-2: Patch-Graph mit alt-Zweig
Sie können mithilfe der Option --all einen ASCII-Graphen mit all Ihren Refs zeichnen: $ * * * | |
git log --graph --pretty=oneline --abbrev-commit --all 62eb555... Z 204a725... Y d3b424b... X * 73ac30e... D * 6900349... C
258 | Kapitel 13: Patches
|/ * e587667... B * 2702377... A
Nehmen Sie weiterhin an, dass der master-Entwickler den alt-Zweig bei Commit Z in den master bei Commit D überführt hat, um das Merge-Commit E zu formen. Schließlich hat er eine weitere Änderung vorgenommen, die F zum master-Zweig hinzugefügt hat: $ git checkout master $ git merge alt # Konflikte nach Belieben auflösen # Ich habe die Folge A, B, C, D, X, Y, Z benutzt $ git add Datei $ git commit -m'Alle Zeilen' Created commit a918485: Alle Zeilen $ echo F >> Datei ; git commit -mF Datei Created commit 3a43046: F 1 files changed, 1 insertions(+), 0 deletions(-)
Der Commit-Graph sieht nun so aus wie Abbildung 13-3. A
B
C
X
D
Y
E
Z
F
master
alt
Abbildung 13-3: Verlauf zweier Zweige
Der Verlauf des Commit-Zweiges sieht so aus: $ git show-branch --more=10 ! [alt] Z * [master] F -* [master] F +* [alt] Z +* [alt^] Y +* [alt~2] X * [master~2] D * [master~3] C +* [master~4] B +* [master~5] A
Das Patching kann überraschend flexibel sein, wenn Sie einen komplizierten Revisionsbaum haben. Schauen wir uns das einmal an.
Patches generieren
| 259
Beim Angeben eines Commit-Bereichs müssen Sie aufpassen, besonders wenn er einen Merge enthält. Im aktuellen Beispiel würden Sie vielleicht erwarten, dass der Bereich D..F die beiden Commits für E und F umfasst, und das tut er auch. Allerdings enthält das Commit E den gesamten Inhalt, der in ihm aus all seinen zusammengeführten Zweigen zusammengeführt wurde: # Formatieren der Patches D..F $ git format-patch master~2..master 0001-X.patch 0002-Y.patch 0003-Z.patch 0004-F.patch
Denken Sie daran: Ein Commit-Bereich enthält laut Definition alle Commits, die bis zu dem Bereichsendpunkt führen, aber nicht die Commits, die bis zu einschließlich dem Bereichsstartpunktzustand führen. Im Fall von D..F bedeutet das, dass alle Commits, die zu F beitragen (alle Commits im Beispielgraphen) enthalten sind, aber alle Commits, die bis zu einschließlich D führen (A, B, C und D) eliminiert werden. Das Merge-Commit selbst generiert kein Patch.
Ein ausführliches Beispiel für die Bereichsauflösung Gehen Sie folgendermaßen vor, um einen Bereich zu ermitteln: Beginnen Sie am Endpunkt-Commit und nehmen Sie es auf. Arbeiten Sie sich zurück zu jedem Eltern-Commit, das dazu beiträgt, und beziehen Sie es mit ein. Nehmen Sie rekursiv die Eltern jedes Commit mit auf, das Sie bisher einbezogen haben. Wenn Sie damit fertig sind, alle Commits einzubinden, die zum Endpunkt beitragen, gehen Sie zurück und beginnen Sie mit dem Startpunkt. Entfernen Sie den Startpunkt. Arbeiten Sie sich zurück zu jedem Eltern-Commit, das zum Startpunkt beiträgt, und entfernen Sie auch dies. Entfernen Sie rekursiv alle Eltern-Commits der Commits, die Sie bisher entfernt haben. Im Fall unseres D..F-Bereichs beginnen Sie mit F und nehmen es auf. Gehen Sie zurück zum Eltern-Commit E und nehmen Sie es auf. Schauen Sie sich dann E an und beziehen Sie seine Eltern D und Z mit ein. Erfassen Sie nun rekursiv die Eltern von D, was C und dann B und A ergibt. Entlang der Z-Linie nehmen Sie nun rekursiv Y und X, dann wieder B und schließlich wieder A auf. (Technisch gesehen, werden B und A nicht wieder aufgenommen; die Rekursion kann stoppen, wenn sie einen bereits aufgenommenen Knoten sieht.) Im Prinzip sind nun alle Commits aufgenommen. Gehen Sie zurück, beginnen Sie mit Startpunkt D und entfernen Sie ihn. Entfernen Sie sein Eltern-Commit C und rekursiv dessen Eltern-Commit B und dessen Eltern-Commit A. Es sollten nun noch F E Z Y X übrig sein. E ist allerdings ein Merge, weshalb Sie es entfernen und F Z Y X hinterlassen, genau das Gegenteil der generierten Menge.
260 | Kapitel 13: Patches
Führen Sie git rev-list --no-merges -v von..bis aus, um die Menge der Commits zu verifizieren, für die Patches erzeugt werden, bevor Sie tatsächlich Ihre Patches herstellen.
Sie können als Variation des git format-patch-Commit-Bereichs auch auf ein einzelnes Commit verweisen. Allerdings ist Gits Interpretation eines solchen Befehls nicht unbedingt intuitiv. Normalerweise interpretiert Git ein einzelnes Commit-Argument als »alle Commits, die bis zu dem angegebenen Commit führen und zu ihm beitragen«. Im Gegensatz dazu behandelt git format-patch einen einzelnen Commit-Parameter so, als hätten Sie den Bereich commit..HEAD genannt. Ihr Commit wird als Startpunkt, HEAD als Endpunkt benutzt. Die generierte Patch-Serie liegt also implizit im Kontext des aktuellen, ausgecheckten Zweigs. Wenn in unserem aktuellen Beispiel der master-Zweig ausgecheckt und ein Patch erzeugt wird, für den Commit A angegeben wird, werden alle sieben Patches hergestellt: $ git branch alt * master # Von Commit A $ git format-patch master~5 0001-B.patch 0002-C.patch 0003-D.patch 0004-X.patch 0005-Y.patch 0006-Z.patch 0007-F.patch
Wenn allerdings der alt-Zweig ausgecheckt wird und der Befehl das gleiche A-Commit angibt, dann werden nur solche Patches verwendet, die zur Spitze des alt-Zweiges beitragen: $ git checkout alt Switched to branch "alt" $ git branch * alt master $ git format-patch master~5 0002-B.patch 0003-X.patch 0004-Y.patch 0005-Z.patch
Patches generieren
| 261
Und obwohl Commit A angegeben wurde, erhalten Sie keinen Patch dafür. Das rootCommit ist an dieser Stelle etwas Besonderes, da es keinen vorausgegangenen bestätigten Zustand gibt, zu dem ein Diff berechnet werden kann. Stattdessen stellt ein Patch dafür im Prinzip den kompletten anfänglichen Inhalt dar. Falls Sie wirklich Patches für jedes Commit einschließlich des anfänglichen root-Commit bis zu einem genannten End-Commit generieren wollen, benutzen Sie die Option --root: $ git format-patch --root end-commit
Das Anfangs-Commit generiert einen Patch, der aussieht, als würden alle Dateien darin auf /dev/null beruhen: $ cat 0001-A.patch From 27023770db3385b23f7631363993f91844dd2ce0 Mon Sep 17 00:00:00 2001 From: Jon Loeliger <[email protected]> Date: Sun, 28 Dec 2008 12:09:45 -0600 Subject: [PATCH] A --Datei | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 file diff --git a/Datei b/Datei new file mode 100644 index 0000000..f70f10e --- /dev/null +++ b/Datei @@ -0,0 +1 @@ +A -1.6.0.90.g436ed
Es mag ungewöhnlich aussehen, wenn ein einzelnes Commit so behandelt wird, als hätten Sie Commit..HEAD angegeben, allerdings gibt es für diesen Ansatz in einer bestimmten Situation eine sinnvolle Anwendung. Wenn Sie ein Commit auf einem anderen als Ihrem gerade ausgecheckten Zweig angeben, liefert der Befehl Patches, die in Ihrem aktuellen Zweig liegen, nicht jedoch in dem genannten Zweig. Mit anderen Worten, er generiert eine Menge an Patches, die den anderen Zweig mit Ihrem aktuellen Zweig synchronisieren können. Um diese Eigenschaft zu verdeutlichen, nehmen Sie an, dass Sie den master-Zweig ausgecheckt haben: $ git branch alt * master
Jetzt geben Sie den alt-Zweig als Commit-Parameter an: $ git format-patch alt 0001-C.patch 0002-D.patch 0003-F.patch
262 | Kapitel 13: Patches
Die Patches für die Commits C, D und F bilden exakt die Menge der Patches im masterZweig, aber nicht im alt-Zweig. Die Leistung dieses Befehls, gepaart mit einem einzelnen Commit-Parameter, wird ersichtlich, wenn das angegebene Commit das HEAD-Ref eines Tracking-Zweigs aus irgendeinem Repository ist. Wenn Sie z.B. Alice’ Repository klonen und Ihre master-Entwicklung auf Alice’ master basiert, dann haben Sie einen Tracking-Zweig, der alice/master heißt. Nachdem Sie einige Commits auf Ihrem master-Zweig vorgenommen haben, generiert der Befehl git format-patch alice/master die Menge der Patches, die Sie ihr senden müssen, um sicherzugehen, dass ihr Repository wenigstens Ihren kompletten master-Inhalt besitzt. Alice besitzt möglicherweise schon weitere Änderungen aus anderen Quellen, aber das ist hier nicht wichtig. Sie haben die Menge aus Ihrem Repository isoliert (den master-Zweig), die sich bekanntermaßen nicht in Alice’ Repository befindet. Das heißt, git format-patch dient speziell dazu, Patches für Commits zu erzeugen, die in Ihrem Repository in einem Entwicklungszweig liegen, aber noch nicht im UpstreamRepository.
Patches und topologische Sortierungen Patches, die von git format-patch generiert werden, liegen in topologischer Reihenfolge vor. Bei einem angegebenen Commit werden die Patches für alle Eltern-Commits generiert und ausgegeben, bevor der Patch für dieses Commit ausgegeben wurde. Dadurch wird sichergestellt, dass immer die korrekte Reihenfolge der Patches erzeugt wird, allerdings ist die korrekte Reihenfolge nicht immer eindeutig: Es kann für einen bestimmten Commit-Graphen mehrere korrekte Anordnungen geben. Schauen wir uns an, was das bedeutet. Betrachten wir einige der möglichen Anordnungen für Patches, die für ein korrektes Repository sorgen, wenn der Empfänger sie in dieser Reihenfolge anwendet. Beispiel 13-1 zeigt einige der möglichen topologischen Sortierreihenfolgen für die Commits in unserem Beispielgraphen. Beispiel 13-1: Einige topologische Sortierreihenfolgen A B C D X Y Z E F A B X Y Z C D E F A B C X Y Z D E F A B X C Y Z D E F A B X C Y D Z E F
Denken Sie daran: Auch wenn die Patch-Erzeugung von einer topologischen Sortierung der ausgewählten Knoten im Commit-Graphen gesteuert wird, produzieren nur einige dieser Knoten tatsächlich Patches.
Patches generieren
| 263
Die erste Anordnung in Beispiel 13-1 wurde von Git für git format-patch master~5 gewählt. Da A das erste Commit in dem Bereich ist und keine --root-Option verwendet wurde, gibt es keinen Patch dafür. Commit E repräsentiert einen Merge, so dass auch dafür kein Patch generiert wurde. Das heißt, die Patches werden in der Reihenfolge B, C, D, X, Y, Z und F erzeugt. Ungeachtet der von Git gewählten Patch-Sequenz sollte man erkennen, dass Git eine Linearisierung aller ausgewählten Commits erzeugt hat, wie kompliziert oder verzweigt der Originalgraph auch war. Falls Sie immer wieder Header zu der Patch-E-Mail hinzufügen, könnten Sie Zeit sparen, indem Sie sich einmal die Konfigurationsoptionen format.headers anschauen.
Patches per E-Mail verschicken Nachdem Sie einen Patch oder eine Serie von Patches generiert haben, besteht der nächste logische Schritt darin, sie zur Bewertung an einen anderen Entwickler oder eine Entwicklungsliste zu schicken. Ziel ist es letztendlich, die Patches von einem Entwickler oder einem Upstream-Maintainer auf ein anderes Repository anwenden zu lassen. Die formatierten Patches sollen im Allgemeinen per E-Mail verschickt werden, indem man sie entweder direkt in den Mail User Agent (MUA) importiert oder den Git-Befehl git send-email benutzt. Sie sind nicht verpflichtet, git send-email zu benutzen; es ist allerdings bequem. Wie Sie im nächsten Abschnitt sehen werden, gibt es andere Werkzeuge, die den Patch direkt einsetzen. Angenommen, Sie wollen eine generierte Patch-Datei an einen anderen Entwickler schicken. Sie haben nun mehrere Möglichkeiten: Sie können git send-email ausführen, Sie können Ihren Mailer direkt auf die Patches verweisen oder Sie setzen die Patches in eine E-Mail ein. git send-email zu benutzen, ist einfach. In diesem Beispiel wird der Patch 0001-A.patch an eine Mailingliste namens [email protected] geschickt: $ git send-email -to [email protected] 0001-A.patch 0001-A.patch Who should the emails appear to be from? [Jon Loeliger <[email protected]>] Emails will be sent from: Jon Loeliger <[email protected]> Message-ID to be used as In-Reply-To for the first email? (mbox) Adding cc: Jon Loeliger <[email protected]> from line 'From: Jon Loeliger <[email protected]>' OK. Log says: Sendmail: /usr/sbin/sendmail -i [email protected] [email protected] From: Jon Loeliger <[email protected]> To: [email protected] Cc: Jon Loeliger <[email protected]> Subject: [PATCH] A Date: Mon, 29 Dec 2008 16:43:46 -0600
264 | Kapitel 13: Patches
Message-Id: <[email protected]> X-Mailer: git-send-email 1.6.0.90.g436ed Result: OK
Es gibt viele Optionen, mit denen man die Unzahl von SMTP-Problemen oder -Eigenschaften entweder ausnutzen oder umgehen kann. Sorgen Sie auf jeden Fall dafür, dass Sie Ihren SMTP-Server und -Port kennen. Wahrscheinlich handelt es sich um das traditionelle sendmail-Programm oder um einen gültigen ausgehenden SMTP-Host, etwa smtp. my-isp.com. Richten Sie keine offenen SMTP-Relay-Server ein, nur um Ihre Git-E-Mails zu verschicken. Damit holen Sie sich nur Probleme mit Spam-E-Mails ins Haus.
Der Befehl git send-email besitzt viele Konfigurationsoptionen, die auf seiner Manpage dokumentiert sind. Vielleicht finden Sie es ja bequem, Ihre besonderen SMTP-Information in Ihrer globalen Konfigurationsdatei aufzuzeichnen, indem Sie z.B. die Werte sendemail.smtpserver und sendemail.smtpserverport mit solchen Befehlen setzen: $ git config --global sendemail.smtpserver smtp.my-isp.com $ git config --global sendemail.smtpserverport 465
Je nach Ihrem verwendeten MUA können Sie eine ganze Datei oder ein Verzeichnis mit Patches direkt in einen Mailordner importieren. Das würde das Verschicken einer großen oder komplizierten Patch-Serie stark vereinfachen. Hier ist ein Beispiel, bei dem ein herkömmlicher Mailordner im mbox-Stil mithilfe von format-patch erzeugt und dann direkt in mutt importiert wird, wo die Nachricht adressiert und verschickt werden kann: $ git format-patch --stdout master~2..master > mbox $ mutt -f mbox q:Quit d:Del u:Undel s:Save 1 N Dec 29 Jon Loeliger 2 N Dec 29 Jon Loeliger 3 N Dec 29 Jon Loeliger 4 N Dec 29 Jon Loeliger
m:Mail ( 15) ( 16) ( 16) ( 15)
r:Reply g:Group [PATCH] X [PATCH] Y [PATCH] Z [PATCH] F
?:Help
Die letzten beiden Mechanismen, die send-email benutzen und einen Mailordner direkt importieren, sollten bevorzugt verwendet werden, da sie beide zuverlässig sind und keinen Unfug mit dem sorgfältig formatierten Patch-Inhalt anstellen. Sie werden z.B. seltener hören, dass sich ein Entwickler über seltsame Zeilenumbrüche beschwert, wenn Sie eine dieser Techniken einsetzen.
Patches per E-Mail verschicken
| 265
Andererseits werden Sie feststellen, dass Sie eine generierte Patch-Datei direkt in eine neu erstellte E-Mail einfügen müssen, wenn Sie z.B. thunderbird oder evolution benutzen. In diesen Fällen ist das Risiko viel größer, dass der Patch beeinträchtigt wird. Schalten Sie unbedingt jede Art von HTML-Formatierung aus und senden Sie die Nachricht als einfachen ASCII-Text ohne Umbrüche. Je nach den Fähigkeiten Ihres Empfängers beim Umgang mit Mail oder abhängig von den Richtlinien in Ihrer Entwicklungsliste könnten Sie den Patch als Anhang (Attachment) verschicken. Das erleichtert auch die Bewertung des Patch. Falls sich allerdings der Patch direkt im Mail-Body befindet, müssen einige der Header, die von git format-patch generiert werden, entfernt werden, sodass nur die From:- und Subject:-Header übrigbleiben. Falls Sie Ihre Patches häufig als Textdateien in neue E-Mails einsetzen und es Sie stört, dass Sie die überflüssigen Header löschen müssen, dann könnten Sie einmal den Befehl git format-patch --pretty=format:%s%n%n%b commit ausprobieren. Sie könnten das auch als globalen Git-Alias konfigurieren, wie in »Einen Alias konfigurieren« auf Seite 30 beschrieben.
Egal, wie die Patch-Mail verschickt wird: Sie sollte im Prinzip genau aussehen wie die Original-Patch-Datei beim Empfang – wenn auch mit mehr und anderen Mail-Headern. Die Ähnlichkeit des Patch-Dateiformats vor und nach dem Transport durch das Mailsystem ist kein Versehen. Der Schlüssel zum Erfolg heißt »Einfacher Text«. Außerdem muss verhindert werden, dass ein MUA das Patch-Format durch Operationen wie Zeilenumbrüche verändert. Wenn Sie solchen Störungen vorbeugen können, bleibt der Patch auch bei der Weitergabe über viele MTAs (Mail Transfer Agents) benutzbar. Benutzen Sie git send-email, wenn Ihr MUA dazu neigt, die Zeilen bei ausgehenden Mails umzubrechen.
Es gibt eine Menge Optionen und Konfigurationseinstellungen, mit denen sich die Generierung von Headern für Patches steuern lässt. Vermutlich müssen Sie in Ihrem Projekt entsprechende Vereinbarungen befolgen. Falls Sie eine ganze Reihe von Patches haben, könnten Sie sie mit der Option -o Verzeichnis für git format-patch in ein gemeinsames Verzeichnis legen und anschließend mit git send-email Verzeichnis alle auf einmal verschicken. Benutzen Sie in diesem Fall entweder git format-patch --cover-letter oder git send-email --compose, um eine Art Anschreiben für die ganze Serie zu verfassen. Es gibt darüber hinaus Optionen, um den verschiedenen sozialen Aspekten der meisten Entwicklungslisten zu genügen. Benutzen Sie z.B. --cc, um zusätzliche Empfänger anzugeben, wobei die einzelnen Signed-off-by:-Adressen als Cc:-Empfänger angegeben oder weggelassen werden, oder um festzulegen, ob und wie eine Patch-Serie als Thread auf einer Liste behandelt wird.
266 | Kapitel 13: Patches
Patches anwenden Git besitzt zwei grundlegende Befehle zum Anwenden von Patches. Der auf einer höheren Ebene agierende Befehl git am ist teilweise in Form des Plumbing-Befehls git apply implementiert. Der Befehl git apply ist das Arbeitspferd der Patch-Anwendungsprozedur. Er akzeptiert eine Ausgabe im git diff- oder diff-Stil und wendet sie auf die Dateien in Ihrem aktuellen Arbeitsverzeichnis an. Obwohl er sich in einigen wesentlichen Beziehungen unterscheidet, spielt er im Prinzip die gleiche Rolle wie Larry Walls patch-Befehl. Da ein Diff nur zeilenweise Änderungen und keine weiteren Informationen (wie Autor, Datum oder Logmeldung) enthält, kann es nicht ein Commit ausführen und die Änderung in Ihrem Repository protokollieren. Das heißt: Wenn git apply fertig ist, bleiben die Dateien in Ihrem Arbeitsverzeichnis modifiziert zurück. (In besonderen Fällen kann es den Index ebenfalls benutzen oder ändern.) Im Gegensatz dazu enthalten die Patches, die mit git format-patch entweder vor oder nach dem Verschicken per E-Mail formatiert wurden, alle zusätzlichen Informationen, die nötig sind, um ein richtiges Commit in Ihrem Repository auszuführen und aufzuzeichnen. git am ist zwar so konfiguriert, dass es Patches akzeptiert, die mit git formatpatch generiert wurden, es kann aber auch andere Patches verarbeiten, solange sie einigen grundlegenden Formatierungsrichtlinien folgen.1 Beachten Sie, dass der Befehl git am Commits im aktuellen Zweig generiert. Wir wollen unser Beispiel mithilfe des Repository aus dem Abschnitt »Patches generieren« auf Seite 255 abschließen. Ein Entwickler hat einen vollständigen Satz Patches konstruiert, 0001-B.patch bis 0007-F.patch, und diesen an einen anderen Entwickler verschickt oder auf andere Weise zur Verfügung gestellt. Der andere Entwickler besitzt eine frühere Version des Repository und möchte nun den Patch-Satz anwenden. Schauen wir uns zuerst einen naiven Ansatz an, der einige verbreitete Probleme an den Tag bringt, die letztendlich nicht zu lösen sind. Anschließend untersuchen wir einen zweiten Ansatz, der erfolgreich verläuft. Hier sind die Patches aus dem Original-Repository: $ git format-patch -o /tmp/patches master~5 /tmp/patches/0001-B.patch /tmp/patches/0002-C.patch /tmp/patches/0003-D.patch /tmp/patches/0004-X.patch
1
Wenn Sie die Richtlinien befolgen, die in der Manpage für git am niedergelegt sind (ein »From:«, ein »Subject:«, ein »Date:« und davon abgetrennt der Patch-Inhalt), können Sie es genauso gut als E-Mail-Nachricht bezeichnen.
Patches anwenden
| 267
/tmp/patches/0005-Y.patch /tmp/patches/0006-Z.patch /tmp/patches/0007-F.patch
Diese Patches hätten vom zweiten Empfänger per E-Mail empfangen und auf der Festplatte gespeichert oder direkt in ein gemeinsames Dateisystem gelegt werden können. Konstruieren wir ein erstes Repository als Ziel für diese Patch-Serie. (Wie dieses Ausgangs-Repository aufgebaut ist, ist eigentlich nicht wichtig – es hätte auch vom ersten Repository geklont sein können, muss es aber nicht.) Der Schlüssel zu langfristigem Erfolg ist ein bestimmter Zeitpunkt, zu dem beide Repositories bekanntermaßen exakt den gleichen Dateiinhalt aufweisen. Wir wollen diesen Augenblick rekonstruieren, indem wir ein neues Repository anlegen, das die Datei Datei mit dem anfänglichen Inhalt A enthält. Das ist exakt der gleiche Repository-Inhalt wie ganz am Anfang des Original-Repository: $ mkdir /tmp/am $ cd /tmp/am $ git init Initialized empty Git repository in am/.git/ $ echo A >> Datei $ git add Datei $ git commit -mA Created initial commit 5108c99: A 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 Datei
Eine direkte Ausführung von git am zeigt einige Probleme: $ git am /tmp/patches/* Applying B Applying C Applying D Applying X error: patch failed: Datei:1 error: Datei: patch does not apply Patch failed at 0004. When you have resolved this problem run "git am --resolved". If you would prefer to skip this patch, instead run "git am --skip". To restore the original branch and stop patching run "git am --abort".
Das ist ein harter Fehlerfall, der sie möglicherweise im Zweifel darüber lässt, wie Sie fortfahren sollten. Ein guter Ansatz in diesem Augenblick ist es, ein wenig herumzuschauen. $ git diff $ git show-branch --more=10 [master] D [master^] C [master~2] B [master~3] A
268 | Kapitel 13: Patches
Das ist schon genau wie erwartet. Keine Datei wurde in Ihrem Arbeitsverzeichnis unsauber hinterlassen, und Git hat die Patches bis einschließlich D erfolgreich angewandt. Oft hilft es schon, sich den Patch selbst und die Dateien anzuschauen, die von dem Patch beeinflusst werden. Je nach der bei Ihnen installierten Version von Git ist entweder das Verzeichnis .dotest oder das Verzeichnis .git/rebase-apply vorhanden, wenn git am läuft. Es enthält verschiedene Kontextinformationen für die gesamte Patch-Serie und die einzelnen Patches (Autor, Lognachricht usw.). # Oder .dotest/patch in früheren Git-Versionen $ cat .git/rebase-apply/patch --Datei | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diff --git a/Datei b/Datei index 35d242b..7f9826a 100644 --- a/Datei +++ b/Datei @@ -1,2 +1,3 @@ A B +X -1.6.0.90.g436ed $ cat Datei A B C D
Das ist eine schwierige Stelle. Die Datei enthält vier Zeilen, der Patch gilt aber für eine Version dieser Datei mit nur zwei Zeilen. Wie die Ausgabe des git am-Befehls andeutete, gilt dieser Patch tatsächlich nicht: error: patch failed: Datei:1 error: Datei: patch does not apply Patch failed at 0004.
Sie wissen wahrscheinlich, dass das ultimative Ziel darin besteht, eine Datei zu erzeugen, in der alle Buchstaben sortiert sind, was Git allerdings nicht automatisch erledigen kann. Der Kontext reicht nicht aus, um die richtige Konfliktlösung zu ermitteln. Wie bei anderen echten Dateikonflikten unterbreitet git am einige Vorschläge: When you have resolved this problem run "git am --resolved". If you would prefer to skip this patch, instead run "git am --skip". To restore the original branch and stop patching run "git am --abort".
Leider gibt es in diesem Fall keinen Inhaltskonflikt, der aufgelöst werden könnte.
Patches anwenden
| 269
Vielleicht glauben Sie, dass Sie den X-Patch einfach, wie vorgeschlagen, »überspringen« können: $ git am --skip Applying Y error: patch failed: Datei:1 error: Datei: patch does not apply Patch failed at 0005. When you have resolved this problem run "git am --resolved". If you would prefer to skip this patch, instead run "git am --skip". To restore the original branch and stop patching run "git am --abort".
Aber genau wie dieser Y-Patch schlagen alle nachfolgenden Patches fehl. Mit diesem Ansatz lässt sich die Patch-Serie nicht sauber anwenden. Sie können versuchen, das System von hier aus wiederherzustellen, allerdings ist das schwierig, wenn man die ursprünglichen Verzweigungseigenschaften nicht kennt, die zu den Patch-Serien geführt haben, die git am präsentiert wurden. Erinnern Sie sich daran, dass das X-Commit auf einen neuen Zweig angewandt wurde, der bei Commit B entstand. Das bedeutet, dass das X richtig wäre, wenn es noch einmal auf diesen Commit-Zustand angewandt würde. Sie können es überprüfen: Setzen Sie das Repository zurück auf das A-Commit, leeren Sie das rebase-apply-Verzeichnis, wenden Sie das B-Commit mit git am /tmp/patches/0002-B.patch noch einmal an, und stellen Sie fest, dass das X-Commit ebenfalls angewandt wird! # Zurücksetzen auf Commit A $ git reset --hard master~3 HEAD is now at 5108c99 A # Oder .dotest, je nachdem $ rm -rf .git/rebase-apply/ $ git am /tmp/patches/0001-B.patch Applying B $ git am /tmp/patches/0004-X.patch Applying X
Das Aufräumen eines fehlgeschlagenen, verpfuschten oder hoffnungslosen git am und das Wiederherstellen des Originalzweigs kann auf git am --abort vereinfacht werden.
Das erfolgreiche Anwenden von 0004-X.patch auf Commit B liefert einen Hinweis darauf, wie weitergemacht werden soll. Sie können die Patches X, Y und Z allerdings nicht anwenden, weil ansonsten später die Patches C, D und F nicht gelten würden. Und Sie wollen sich sicher nicht damit herumschlagen, die exakte Originalzweigstruktur wieder-
270 | Kapitel 13: Patches
herzustellen – nicht einmal vorübergehend. Und selbst wenn Sie willens wären, sie wiederherzustellen, woher sollten Sie die Originalzweigstruktur kennen? Die Basisdatei herauszufinden, auf die ein Diff angewandt werden kann, ist ein schwieriges Problem, für das Git eine einfache technische Lösung bietet. Wenn Sie sich eine Patch- oder Diff-Datei, die von Git generiert wurde, genauer anschauen, werden Sie neue, zusätzliche Informationen sehen, die nicht Teil einer traditionellen Unix-diffZusammenfassung sind. Die Zusatzinformationen, die Git für die Patch-Datei 0004-X. patch bietet, sind in Beispiel 13-2 zu sehen. Beispiel 13-2: Neuer Patch-Kontext in 0004-X.patch diff --git a/Datei b/Datei index 35d242b..7f9826a 100644 --- a/Datei +++ b/Datei
Direkt hinter der Zeile diff --git a/Datei b/Datei fügt Git die neue Zeile index 35d242b.. 7f9826a 100644 ein. Diese Information ist dazu gedacht, mit Sicherheit diese Frage zu beantworten: »Welches ist der Originalzustand, für den dieser Patch gilt?« Die erste Zahl auf der index-Zeile, 35d242b, ist der SHA1-Hash des Blobs innerhalb des Git-Objektspeichers, für den dieser Teil des Patch gilt. Das heißt, 35d242b ist die Datei mit nur zwei Zeilen: $ git show 35d242b A B
Und das ist genau die Version von Datei, für die dieser Teil des X-Patch gilt. Wenn diese Version der Datei sich im Repository befindet, kann Git den Patch darauf anwenden. Dieser Mechanismus – eine aktuelle Version einer Datei nehmen, eine alternative Version nehmen und die ursprüngliche Grundform einer Datei suchen, auf die der Patch angewandt werden soll – wird als Dreifach-Merge (three-way merge) bezeichnet. Git ist in der Lage, dieses Szenario mit der Option -3 oder --3way für git am zu rekonstruieren. Wir wollen den fehlgeschlagenen Versuch aufräumen; setzen Sie alles zurück auf den ersten Commit-Zustand A und versuchen Sie, die Patch-Serien erneut anzuwenden: # Entfernen Sie den temporären "git am"-Kontext, falls nötig $ rm -rf .git/rebase-apply/ # Benutzen Sie "git log", um Commit A zu lokalisieren -- es war SHA1 5108c99 # Für Sie ist es etwas Anderes. $ git reset --hard 5108c99 HEAD is now at 5108c99 A $ git show-branch --more=10 [master] A
Patches anwenden
| 271
Wenden Sie nun mithilfe der Option -3 die Patch-Folgen an: $ git am -3 /tmp/patches/* Applying B Applying C Applying D Applying X error: patch failed: Datei:1 error: Datei: patch does not apply Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... Auto-merged Datei CONFLICT (content): Merge conflict in Datei Failed to merge in the changes. Patch failed at 0004. When you have resolved this problem run "git am -3 --resolved". If you would prefer to skip this patch, instead run "git am -3 --skip". To restore the original branch and stop patching run "git am -3 --abort".
Viel besser! Wie zuvor ist der einfache Versuch, die Datei mit einem Patch zu versehen, fehlgeschlagen; anstatt den Versuch aber zu beenden, ist Git zu dem Dreifach-Merge übergegangen. Dieses Mal erkennt Git, dass es in der Lage ist, den Merge durchzuführen, allerdings bleibt ein Konflikt, weil überschneidende Zeilen auf zwei unterschiedliche Arten geändert wurden. Da Git nicht in der Lage ist, diesen Konflikt korrekt aufzulösen, wird der Befehl git am -3 zeitweise suspendiert. Jetzt müssen Sie den Konflikt auflösen, bevor Sie den Befehl fortsetzen können. Wieder kann scharfes Hinschauen dabei helfen, das weitere Vorgehen zu bestimmen: $ git status Datei: needs merge # On branch master # Changed but not updated: # (use "git add ..." to update what will be committed) # # unmerged: Datei
Wie bereits angedeutet, muss für die Datei Datei noch ein Merge-Konflikt aufgelöst werden. Der Inhalt von Datei zeigt die herkömmlichen Konflikt-Merge-Marker und muss über einen Editor aufgelöst werden: $ cat Datei A B <<<<<<< HEAD:Datei C D ======= X >>>>>>> X:Datei
272 | Kapitel 13: Patches
# Konflikte in "Datei" lösen $ emacs Datei $ cat Datei A B C D X
Setzen Sie git am -3 nach dem Auflösen des Konflikts und dem Aufräumen fort: $ git am -3 --resolved Applying X No changes - did you forget to use 'git When you have resolved this problem run If you would prefer to skip this patch, To restore the original branch and stop
add'? "git am -3 --resolved". instead run "git am -3 --skip". patching run "git am -3 --abort".
Haben Sie vergessen, git add zu benutzen? Genau! $ git add Datei $ git am -3 --resolved Applying X Applying Y error: patch failed: Datei:1 error: Datei: patch does not apply Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... Auto-merged Datei Applying Z error: patch failed: Datei:2 error: Datei: patch does not apply Using index info to reconstruct a base tree... Falling back to patching base and 3-way merge... Auto-merged Datei Applying F
Endlich erfolgreich! $ cat Datei A B C D X Y Z F $ git show-branch --more=10 [master] F [master^] Z [master~2] Y [master~3] X
Patches anwenden
| 273
[master~4] [master~5] [master~6] [master~7]
D C B A
Durch das Anwenden dieser Patches wurde keine Replik der Zweigstruktur aus dem Original-Repository konstruiert. Alle Patches wurden in linearer Abfolge angewandt, und das spiegelt sich im Commit-Verlauf des master-Zweigs wider. # Das C-Commit $ git log --pretty=fuller -1 1666a7 commit 848f55821c9d725cb7873ab3dc3b52d1bcbf0e93 Author: Jon Loeliger <[email protected]> AuthorDate: Sun Dec 28 12:10:42 2008 -0600 Commit: Jon Loeliger <[email protected]> CommitDate: Mon Dec 29 18:46:35 2008 -0600 C
Die Patches Author und AuthorDate sind für das Original-Commit und den Patch, während die Daten für den Committer das Anwenden des Patch sowie das Bestätigen für diesen Zweig und dieses Repository wiedergeben.
Schlechte Patches Es ist nicht ganz einfach, wenn man ungeachtet der Schwierigkeiten heutiger E-Mail-Systeme robusten, identischen Inhalt in mehreren verteilten Repositories auf der ganzen Welt erzeugen muss. Daher nimmt es nicht Wunder, dass ein absolut guter Patch durch eine Vielzahl von mailbezogenen Fehlern zunichte gemacht werden kann. Letztendlich obliegt es Git, dafür zu sorgen, dass der vollständige Patchen-E-Mail-Anwenden-Zyklus in der Lage ist, zuverlässig identischen Inhalt über einen unzuverlässigen Transportmechanismus zu rekonstruieren. Patch-Fehler können ihre Ursache in vielen Bereichen, vielen unpassenden Werkzeugen und vielen unterschiedlichen Philosophien haben. Der am häufigsten auftretende Fehler ist aber oft schlicht und ergreifend eine falsche Behandlung der Zeilen des Originalinhalts. Normalerweise zeigt sich das in Zeilenumbrüchen, weil der Text entweder von den Absender- oder den Empfänger-MUAs oder von irgendwelchen Zwischen-MTAs falsch umgebrochen wird. Glücklicherweise verfügt das Patch-Format über interne Konsistenztests, die verhindern, dass diese Art von Fehler ein Repository beschädigt.
Patching und Merging im Vergleich Git kommt mit Fällen zurecht, in denen Patches angewandt und gleichzeitig dieselben Änderungen in ein Repository gezogen wurden. Auch wenn sich das Commit im empfangenden Repository letztendlich von dem Commit im Original-Repository, aus dem der Patch erzeugt wurde, unterscheidet, kann Git seine Fähigkeit zum Vergleichen und Zuordnen des Inhalts einsetzen, um die Lage zu klären. 274 | Kapitel 13: Patches
Später zeigen z.B. nachfolgende Diffs keine Inhaltsänderungen. Logmeldung und Autoreninformationen sind identisch mit den Daten, die in der Patch-Mail übertragen wurden, Informationen wie das Datum und der SHA1-Hash unterscheiden sich dagegen. Das direkte Abrufen und Zusammenführen eines Zweigs mit einem komplexen Verlauf ergibt im empfangenden Repository allerdings einen anderen Verlauf, als wenn mit einer Folge von Patches gearbeitet wird. Denken Sie daran: Eine der Auswirkungen der Erzeugung einer Patch-Folge auf einen komplexen Zweig ist die topologische Sortierung des Graphen zu einem linearisierten Verlauf. Wird das dann auf ein anderes Repository angewandt, ergibt sich ein linearisierter Verlauf, der so nicht im Original zu finden war. Je nach Ihrem Entwicklungsstil und Ihrer eigentlichen Absicht kann es ein Problem für Sie und Ihr Projekt darstellen, wenn der ursprüngliche Entwicklungsverlauf im empfangenden Repository linearisiert wurde. Zumindest haben Sie den kompletten Zweigverlauf verloren, der zu dieser Patch-Folge geführt hat. Im besten Fall ist es Ihnen einfach egal, wie Sie zu der Patch-Folge gekommen sind.
Patching und Merging im Vergleich | 275
Kapitel 14
KAPITEL 14
Hooks – Einstiegspunkte für die Automatisierung
Sie können einen Git-Hook verwenden, um immer dann, wenn in Ihrem Repository ein bestimmtes Ereignis eintritt (z.B. ein Commit oder ein Patch), ein oder mehrere beliebige Skripten auszuführen. Ein Ereignis (Event) ist typischerweise in mehrere vorgeschriebene Schritte unterteilt. An jeden Schritt können Sie ein eigenes Skript binden. Wenn das GitEreignis eintritt, wird am Anfang des jeweiligen Schritts das passende Skript aufgerufen. Hooks gehören zu einem bestimmten Repository und beeinflussen es. Sie werden während eines git clone nicht kopiert. Mit anderen Worten werden Hooks, die Sie in Ihrem privaten Repository einrichten, nicht an den neuen Klon weitergegeben und haben deshalb auch keinen Einfluss auf dessen Verhalten. Falls Ihr Entwicklungsprozess aus irgendeinem Grund Hooks in den persönlichen Entwicklungs-Repositories der einzelnen Programmierer verlangt, dann müssen Sie das Verzeichnis .git/hooks mithilfe einer anderen (Nicht-Klon-) Methode kopieren. Ein Hook läuft entweder im Kontext Ihres aktuellen lokalen Repository oder im Kontext des entfernten Repository. So können z.B. das Abrufen von Daten aus einem entfernten Repository in Ihr lokales Repository und das Ausführen eines lokalen Commit dafür sorgen, dass lokale Hooks ausgeführt werden; das Verschieben von Änderungen in ein entferntes Repository könnte Hooks im entfernten Repository starten. Die meisten Git-Hooks gehören zu einer der beiden Kategorien: • Ein »Prä«-Hook wird ausgeführt, bevor eine Aktion abgeschlossen ist. Sie können mit dieser Art von Hook eine Änderung freigeben, ablehnen oder anpassen, bevor sie angewandt wird. • Ein »Post«-Hook wird ausgeführt, nachdem eine Aktion abgeschlossen ist, und kann dazu dienen, Benachrichtigungen (etwa E-Mails) auszulösen oder zusätzliche Verarbeitungsschritte zu starten, etwa das Ausführen eines Build oder das Schließen eines Bugs. Als allgemeine Regel gilt: Wenn ein Prä-Aktions-Hook mit einem Nonzero-Status beendet wird (damit wird laut Konvention ein Fehlschlagen angezeigt), wird die Git-Aktion
| 277
abgebrochen. Im Gegensatz dazu wird der Exit-Status eines Post-Aktions-Hook im Allgemeinen ignoriert, weil der Hook das Ergebnis oder den Abschluss der Aktion nicht mehr beeinflussen kann. Im Allgemeinen empfehlen die Git-Entwickler, Hooks mit Bedacht einzusetzen. Ein Hook, so sagen sie, sollte als letzter Ausweg betrachtet werden, wenn es Ihnen nicht gelingt, das gleiche Ergebnis auf eine andere Weise zu erzielen. Falls Sie z.B. jedesmal, wenn Sie ein Commit ausführen, eine Datei auschecken oder einen Zweig erzeugen, eine bestimmte Option angeben wollen, ist ein Hook unnötig. Sie können die gleiche Aufgabe mit einem Git-Alias (siehe »Einen Alias konfigurieren« auf Seite 30) oder mit Shell-Skripten erledigen, mit denen Sie git commit, git checkout bzw. git branch erweitern.1 Auf den ersten Blick scheint ein Hook eine verlockende und einfache Lösung dazustellen. Allerdings zieht sein Einsatz einige Implikationen nach sich: • Ein Hook ändert das Verhalten von Git. Wenn ein Hook eine ungewöhnliche Operation ausführt, könnten andere Entwickler, die mit Git vertraut sind, überrascht sein, wenn sie Ihr Repository benutzen. • Ein Hook kann Operationen verlangsamen, die normalerweise schnell sind. So sind Entwickler oft versucht, Git mit einem Hook zu veranlassen, Tests auszuführen, bevor Sie etwas mit einem Commit bestätigen, allerdings wird dadurch das Commit ausgebremst. In Git soll ein Commit eine schnelle Operation sein, sodass zu häufigen Commits ermuntert wird, um Datenverluste zu vermeiden. Wenn man ein Commit langsamer ausführen lässt, geht der ganze Spaß an Git verloren. • Ein Hook-Skript, das fehlerhaft ist, kann Ihre Arbeit und Produktivität behindern. Ein Hook lässt sich nur umgehen, indem man es deaktiviert. Falls Sie dagegen einen Alias oder ein Shell-Skript anstelle eines Hook benutzen, können Sie jederzeit auf den normalen Git-Befehl zurückgreifen, wenn das sinnvoll ist. • Die Hook-Sammlung eines Repository wird nicht automatisch repliziert. Falls Sie also ein Commit-Hook in Ihrem Repository installieren, hat es nicht unbedingt Einfluss auf die Commits eines anderen Entwicklers. Teilweise hat das Sicherheitsgründe – in ein ansonsten harmlos aussehendes Repository könnte sonst leicht ein bösartiges Skript eingeschmuggelt werden – und teilweise liegt es daran, dass Git einfach keinen Mechanismus besitzt, um etwas anderes als Blobs, Bäume und Commits zu replizieren. Abgesehen von diesen Warnungen haben Hooks ihre Daseinsberechtigung und können ausgesprochen vorteilhaft sein.
1
Wie das Leben so spielt, ist das Ausführen eines Hook zum Commit-Zeitpunkt eine so verbreitete Anforderung, dass dafür sogar ein Prä-Commit-Hook existiert, auch wenn es nicht unbedingt nötig ist.
278 | Kapitel 14: Hooks – Einstiegspunkte für die Automatisierung
Junios Überblick über Hooks Junio Hamano schrieb auf der Git-Mailingliste Folgendes über Git-Hooks (nach dem Original zusammengefasst). Es gibt fünf sinnvolle Gründe, um einen Git-Befehl/eine Git-Operation mit einem Hook anzubinden: 1. Um die Entscheidung zu widerrufen, die von dem zugrunde liegenden Befehl getroffen wurde. Der update-Hook und das pre-commit-Hook sind zwei Hooks für diesen Zweck. 2. Um Daten zu manipulieren, die generiert wurden, nachdem ein Befehl gestartet ist. Ein Beispiel ist das Verändern der Commit-Log-Meldung im commit-msg-Hook. 3. Um auf dem entfernten Ende einer Verbindung zu operieren, auf die Sie nur über das Git-Protokoll zugreifen können. Ein post-update-Hook, das git update-server-info ausführt, erledigt diese Aufgabe. 4. Um eine Sperre für einen wechselseitigen Ausschluss zu beschaffen. Das ist eine seltene Anforderung, die aber dennoch von einigen Hooks erfüllt werden kann. 5. Um je nach dem Ergebnis des Befehls eine von mehreren möglichen Operationen auszuführen. Der post-checkout-Hook stellt ein nennenswertes Beispiel dar. Jede dieser fünf Anforderungen verlangt wenigstens ein Hook. Außerhalb des Git-Befehls lässt sich ein vergleichbares Ergebnis nicht erreichen. Falls Sie andererseits immer wollen, dass vor oder nach dem Ausführen einer Git-Operation lokal irgendeine Aktion stattfindet, brauchen Sie kein Hook. Wenn z.B. Ihre nachfolgende Verarbeitung von den Wirkungen eines Befehls abhängt (Eintrag 5 in der Liste), die Ergebnisse des Befehls aber einfach erkennbar sind, benötigen Sie kein Hook.
Hooks installieren Jeder Hook ist ein Skript. Die Sammlung der Hooks für ein bestimmtes Repository finden Sie im Verzeichnis .git/hooks. Wie bereits erwähnt, repliziert Git die Hooks zwischen den Repositories nicht; wenn Sie Daten mit git clone oder git fetch aus einem anderen Repository holen, dann erben Sie die Hooks dieses Repository nicht. Sie müssen die Hook-Skripten von Hand kopieren. Jedes Hook-Skript ist nach dem Ereignis benannt, mit dem es verknüpft ist. So heißt der Hook, der direkt vor einer git commit-Operation ausgeführt wird, .git/hooks/pre-commit. Ein Hook-Skript muss die normalen Regeln für Unix-Skripten befolgen: Es muss ausführbar sein (chmod a+x .git/hooks/pre-commit) und mit einer Zeile beginnen, die die Sprache angibt, in der das Skript geschrieben ist (z.B. #!/bin/bash oder #!/usr/bin/perl). Wenn ein bestimmtes Hook-Skript existiert und den korrekten Dateinamen sowie die passenden Dateiberechtigungen besitzt, benutzt Git es automatisch.
Hooks installieren | 279
Beispiel-Hooks Je nach Ihrer speziellen Git-Version können Sie bereits nach der Erzeugung Ihres Repository einige Hooks vorfinden. Hooks werden automatisch aus Ihrem Git-TemplateVerzeichnis kopiert, wenn Sie ein neues Repository erzeugen. Auf Debian und Ubuntu z. B. werden die Hooks aus /usr/share/git-core/templates/hooks kopiert. Die meisten GitVersionen enthalten einige Beispiel-Hooks, die Sie benutzen können. Diese sind im Templates-Verzeichnis vorinstalliert. Folgendes müssen Sie über die Beispiel-Hooks wissen: • Die Template-Hooks machen wahrscheinlich nicht genau das, was Sie wollen. Sie können sie lesen, bearbeiten und von ihnen lernen, werden sie aber selten unverändert einsetzen. • Die Hooks werden zwar standardmäßig erzeugt, sind aber zunächst deaktiviert. Je nach Ihrer Git-Version und Ihrem Betriebssystem werden die Hooks deaktiviert, indem man entweder das Ausführungsbit entfernt oder .sample an den Hook-Dateinamen anhängt. Moderne Versionen von Git haben ausführbare Hooks mit dem Suffix .sample. • Um ein Beispiel-Hook zu aktivieren, müssen Sie das Suffix .sample von seinem Dateinamen entfernen (mv .git/hooks/pre-commit.sample .git/hooks/pre-commit) und sein Ausführungsbit setzen (chmod a+x .git/hooks/pre-commit). Ursprünglich wurden die Beispiel-Hooks einfach aus dem Template-Verzeichnis in das Verzeichnis .git/hooks/ kopiert, wobei die Ausführungsberechtigung entfernt wurde. Sie konnten den Hook dann aktivieren, indem Sie das Ausführungsbit setzten. Das funktionierte auf Systemen wie Unix und Linux ganz gut, nicht jedoch unter Windows. Unter Windows funktionieren die Dateiberechtigungen anders, und Dateien sind leider standardmäßig ausführbar. Das bedeutet, dass die Beispiel-Hooks von vornherein ausführbar waren, was unter Git-Neulingen Verwirrung stiftete, weil alle Hooks ausgeführt wurden, obwohl keiner ausgeführt werden sollte. Wegen dieses Problems mit Windows hängen neuere Versionen von Git an jeden HookDateinamen das Suffix .sample an, damit es auch dann nicht ausgeführt wird, wenn es ausführbar ist. Um die Beispiel-Hooks zu aktivieren, müssen Sie die entsprechenden Skripten selbst umbenennen. Falls die Beispiel-Hooks Sie nicht interessieren, können Sie sie einfach aus Ihrem Repository entfernen: rm .git/hooks/*. Sie bekommen sie jederzeit zurück, indem Sie sie wieder aus dem templates-Verzeichnis kopieren. Neben den Template-Beispielen gibt es weitere Beispiel-Hooks im Git-Verzeichnis contrib, einem Teil des Git-Quellcodes. Die ergänzenden Dateien können zusammen mit Git auf Ihrem System installiert werden. Auf Debian und Ubuntu werden die mitgelieferten Hooks z.B. in /usr/share/ doc/git-core/contrib/hooks installiert.
280 | Kapitel 14: Hooks – Einstiegspunkte für die Automatisierung
Ihr erstes Hook erzeugen Um zu lernen, wie ein Hook funktioniert, wollen wir ein neues Repository anlegen und einen einfachen Hook installieren. Zuerst erzeugen wir das Repository und füllen es mit einigen Dateien: $ mkdir hooktest $ cd hooktest $ git init Initialized empty Git repository in .git/ $ touch a b c $ git add a b c $ git commit -m 'a, b und c hinzugefügt' Created initial commit 97e9cf8: a, b und c hinzugefügt 0 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 a create mode 100644 b create mode 100644 c
Legen wir nun ein pre-commit-Hook an, um zu verhindern, dass Änderungen eingecheckt werden, die das Wort »kaputt« enthalten. Setzen Sie mit Ihrem Lieblingstexteditor Folgendes in eine Datei namens .git/hooks/pre-commit: #!/bin/bash echo "Hallo, ich bin ein Prä-Commit-Skript!" >&2 if git diff --cached | grep '^\+' | grep -q 'kaputt'; then echo "FEHLER: Kann das Wort 'kaputt' nicht bestätigen" >&2 exit 1 # ablehnen fi exit 0 # akzeptieren
Das Skript generiert eine Liste aller Unterschiede, die eingecheckt werden sollen, extrahiert die Zeilen, die hinzugefügt werden sollen (d.h. Zeilen, die mit dem Zeichen + beginnen), und sucht auf diesen Zeilen nach dem Wort »kaputt«. Es gibt viele Möglichkeiten, auf das Wort »kaputt« zu testen, allerdings führen die meisten der offensichtlichen Methoden zu kleinen Problemen. Ich rede jetzt nicht davon, wie man »auf das Wort ‘kaputt’ testet«, sondern wie man den Text findet, der nach dem Wort »kaputt« durchsucht werden soll. Sie hätten z.B. diesen Test probieren if git ls-files | xargs grep -q 'kaputt'; then
oder mit anderen Worten in allen Dateien im Repository nach dem Wort »kaputt« suchen können. Allerdings weist dieser Ansatz zwei Probleme auf. Hätte jemand bereits eine Datei bestätigt, die das Wort »kaputt« enthält, dann würde dieses Skript alle künftigen Commits verhindern (zumindest, bis Sie es repariert haben), selbst wenn diese Com-
Hooks installieren | 281
mits überhaupt nichts damit zu tun haben. Darüber hinaus kann der Unix-Befehl grep gar nicht wissen, welche Dateien tatsächlich bestätigt werden; wenn Sie »kaputt« zu Datei b hinzufügen, eine Änderung an a vornehmen und dann git commit a ausführen, ist nichts an Ihrem Commit verkehrt, weil Sie ja nicht versuchen, b mit dem Commit zu bestätigen. Ein Skript mit diesem Test würde das Commit dennoch ablehnen. Wenn Sie ein pre-commit-Skript schreiben, das einschränkt, was Sie einchecken dürfen, müssen Sie es eines Tages mit ziemlicher Sicherheit umgehen. Sie können das pre-commit-Hook umgehen, indem Sie entweder die Option --no-verify für git commit benutzen oder Ihr Hook zeitweise deaktivieren.
Wenn Sie den pre-commit-Hook erzeugt haben, machen Sie ihn ausführbar: $ chmod a+x .git/hooks/pre-commit
Nun können Sie testen, ob er erwartungsgemäß funktioniert: $ echo "absolut gut" >a $ echo "kaputt" >b $ git commit -m "Test Commit -a" -a Hallo, ich bin ein Prä-Commit-Skript! FEHLER: Kann das Wort 'kaputt' nicht bestätigen $ git commit -m "Nur Datei a testen" a Hallo, ich bin ein Prä-Commit-Skript! Created commit 4542056: test 1 files changed, 1 insertions(+), 0 deletions(-) $ git commit -m "Nur Datei b testen" b Hallo, ich bin ein Prä-Commit-Skript! FEHLER: Kann das Wort 'kaputt' nicht bestätigen
Selbst wenn das Commit funktioniert, gibt das pre-commit-Skript noch sein »Hallo« aus. Bei einem echten Skript würde das nerven; Sie sollten solche Meldungen deshalb nur verwenden, wenn Sie beim Skript das Debugging durchführen. Beachten Sie außerdem, dass git commit keine Fehlermeldung ausgibt, wenn das Commit abgewiesen wird; es wird nur vom Skript eine Meldung erzeugt. Damit Sie den Benutzer nicht verwirren, sollten Sie darauf achten, dass ein »Prä«-Skript immer eine Fehlermeldung ausgibt, wenn es einen Nonzero-Exit-Code (also einen »ablehnenden«) ausgibt. Jetzt, da wir die Grundlagen kennen, wollen wir über die verschiedenen Hooks reden, die Sie erzeugen können.
Verfügbare Hooks Mit der Weiterentwicklung von Git werden auch neue Hooks verfügbar. Mit git help hooks stellen Sie fest, welche Hooks es in Ihrer Git-Version gibt. Schauen Sie außerdem in
282 | Kapitel 14: Hooks – Einstiegspunkte für die Automatisierung
die Git-Dokumentation, um alle Kommandozeilenparameter sowie die Ein- und Ausgaben der einzelnen Hooks zu ermitteln.
Commit-bezogene Hooks Wenn Sie git commit aufrufen, führt Git einen Vorgang aus, der ungefähr so aussieht wie in Abbildung 14-1. pre-commit hook (unless --no-verify) (prepare default commit message) prepare-commit-msg hook (let the user edit the commit message) commit-msg hook (unless --no-verify) (actually do the commit) post-commit hook Abbildung 14-1: Commit-Hook-Verarbeitung Keines der Commit-Hooks funktioniert für etwas Anderes als git commit. Z.B. führen git rebase, git merge und git am standardmäßig keines Ihrer Commit-Hooks aus. (Diese Befehle können aber andere Hooks ausführen.) git commit --amend führt allerdings Ihre Commit-Hooks aus.
Jeder Hook hat seinen eigenen Zweck: • Das pre-commit-Hook bietet Ihnen die Möglichkeit, ein Commit sofort abzubrechen, wenn etwas mit dem Inhalt nicht stimmt, der bestätigt werden soll. Der pre-commitHook läuft, bevor dem Benutzer erlaubt wird, die Commit-Meldung zu bearbeiten, so dass der Benutzer nicht Gefahr läuft, eine Commit-Meldung einzugeben, nur um anschließend zu entdecken, dass die Änderungen abgewiesen wurden. Sie können diesen Hook auch dazu verwenden, den Inhalt des Commit automatisch zu verändern. • prepare-commit-msg erlaubt Ihnen, Gits Standardmeldung zu verändern, bevor sie dem Benutzer gezeigt wird. Sie können das z.B. nutzen, um das vorgegebene Template für die Commit-Meldung zu ändern.
Verfügbare Hooks
| 283
• Der commit-msg-Hook kann die Commit-Meldung validieren oder modifizieren, nachdem der Benutzer sie bearbeitet hat. Sie können diesen Hook z.B. einsetzen, um nach Schreibfehlern zu suchen oder Meldungen abzuweisen, die eine bestimmte maximale Länge überschreiten. • post-commit läuft, nachdem die Commit-Operation abgeschlossen wurde. An dieser Stelle können Sie z.B. eine Logdatei aktualisieren, eine E-Mail versenden oder einen Auto-Builder auslösen. Manche Leute benutzen diesen Hook, um Bugs automatisch als behoben zu markieren, falls z.B. die Bug-Nummer in der Commit-Meldung genannt wird. In der Realität ist der post-commit-Hook jedoch nur selten sinnvoll, weil das Repository, in dem Sie git commit ausführen, selten dasjenige ist, das Sie mit anderen Leuten gemeinsam benutzen. (Der update-Hook ist wahrscheinlich passender.)
Patch-bezogene Hooks Wenn Sie git am aufrufen, führt Git einen Vorgang aus, der dem in Abbildung 14-2 ähnelt. applypatch-msg hook (apply the patch) pre-applypatch hook (actually do the commit) post-applypatch hook Abbildung 14-2: Patch-Hook-Verarbeitung Unabhängig davon, was Sie von den Hooks erwarten, die in Abbildung 14-2 gezeigt werden, führt git apply nicht die applypatch-Hooks aus; das macht nur git am. Das liegt daran, dass git apply eigentlich überhaupt nichts mit Commits bestätigt, sodass es auch keinen Grund gibt, Hooks auszuführen.
Jeder Hook hat seine eigene Aufgabe: • applypatch-msg untersucht die Commit-Meldung, die an den Patch angehängt ist, und stellt fest, ob sie akzeptabel ist oder nicht. Sie können z.B. beschließen, einen Patch abzulehnen, wenn er keinen Signed-off-by:-Header besitzt. An dieser Stelle könnten Sie außerdem auch die Commit-Meldung verändern.
284 | Kapitel 14: Hooks – Einstiegspunkte für die Automatisierung
• Der pre-applypatch-Hook trägt in gewisser Weise den falschen Namen, da dieses Skript tatsächlich ausgeführt wird, nachdem der Patch angewandt wurde, allerdings bevor das Ergebnis bestätigt wird. Dadurch ist es analog zum pre-commit-Skript, wenn git commit ausgeführt wird, auch wenn sein Name etwas anderes nahelegt. Viele Leute entscheiden sich sogar dafür, ein pre-applypatch-Skript zu erzeugen, das einfach pre-commit ausführt. • post-applypatch entspricht dem post-commit-Skript.
Push-bezogene Hooks Wenn Sie git push ausführen, führt die empfangende Seite von Git einen Prozess aus, der dem in Abbildung 14-3 ähnelt. (receive all new objects) pre-receive hook for each updated ref: update hook update ref post-receive hook post-update hook Abbildung 14-3: Hook-Verarbeitung auf der Empfängerseite
Alle push-bezogenen Hooks laufen auf dem Empfänger, nicht auf dem Absender. Das heißt: Die Hook-Skripten, die ausgeführt werden, befinden sich im .git/hooks-Verzeichnis des empfangenden Repository, nicht im sendenden. Ausgaben, die von entfernten Hooks erzeugt werden, werden trotzdem dem Benutzer gezeigt, der git push durchführt.
Wie Sie im Diagramm sehen können, besteht der erste Schritt von git push darin, alle fehlenden Objekte (Blobs, Bäume und Commits) aus Ihrem lokalen Repository zum entfernten Repository zu übertragen. Während dieses Vorgangs besteht kein Bedarf an einem Hook, da alle Git-Objekte anhand ihres eindeutigen SHA1-Hash-Wertes identifiziert werden – Ihr Hook kann ein Objekt nicht verändern, weil es damit den Hash-Wert
Verfügbare Hooks
| 285
verändern würde. Darüber hinaus gibt es keinen Grund, ein Objekt abzuweisen, weil git gc sowieso aufräumt, falls sich herausstellt, dass das Objekt unnütz war. Anstatt die Objekte selbst zu manipulieren, werden push-bezogene Hooks aufgerufen, wenn es Zeit wird, die Refs (Zweige und Tags) zu aktualisieren. • Der pre-receive-Hook empfängt eine Liste aller Refs, die aktualisiert werden müssen, einschließlich ihrer neuen und alten Objektzeiger. Der pre-receive-Hook kann lediglich alle Änderungen auf einmal akzeptieren oder abweisen, was natürlich insgesamt nicht so nützlich ist. Sie können ihn dennoch als Funktion betrachten, weil er die transaktionale Integrität über Zweiggrenzen hinweg erzwingt. Dennoch ist nicht klar, wieso Sie so etwas brauchen; wenn Ihnen dieses Verhalten nicht gefällt, dann benutzen Sie stattdessen den update-Hook. • Der update-Hook wird für jedes Ref, das aktualisiert wird, exakt einmal aufgerufen. Der update-Hook kann Updates an einzelne Zweige akzeptieren oder ablehnen, ohne die Aktualisierung anderer Zweige zu beeinflussen. Für jedes Update können Sie außerdem eine Aktion auslösen, etwa das Schließen eines Bugs oder das Versenden einer E-Mail-Bestätigung. Normalerweise ist es besser, solche Benachrichtigungen hier abzuhandeln als in einem post-commit-Hook, da ein Commit erst dann als richtig abgeschlossen betrachtet wird, wenn es in ein gemeinsam genutztes Repository geschoben wurde. • Genau wie der pre-receive-Hook empfängt post-receive eine Liste aller Refs, die gerade aktualisiert wurden. Alles, was post-receive tun kann, könnte auch vom update-Hook erledigt werden, aber manchmal ist post-receive bequemer. Falls Sie z.B. eine E-Mail mit einer Update-Benachrichtigung senden wollen, kann postreceive eine einzige Benachrichtigung über alle Updates schicken, anstatt für jedes Update einzeln eine Mail zu senden. • Benutzen Sie den post-update-Hook nicht. Er wurde vom neueren post-receiveHook abgelöst. (post-update weiß, welche Zweige sich geändert haben, kennt aber ihre alten Werte nicht; das schränkt seinen Nutzen ein.)
Andere lokale Repository-Hooks Schließlich gibt es noch einige weitere Hooks, und vermutlich sind seit der Drucklegung dieses Buches noch einige hinzugekommen. (Auch hier finden Sie die Liste der verfügbaren Hooks mit dem Befehl git help hooks.) • Der pre-rebase-Hook läuft, wenn Sie versuchen, die Basis eines Zweigs zu ändern. Das ist sinnvoll, da es verhindert, dass Sie versehentlich git rebase auf einem Zweig ausführen, dessen Basis nicht geändert werden sollte, weil er schon veröffentlicht wurde. • post-checkout läuft, wenn Sie einen Zweig oder eine einzelne Datei auschecken. Sie können es z.B. benutzen, um automatisch leere Verzeichnisse zu erzeugen (Git weiß
286 | Kapitel 14: Hooks – Einstiegspunkte für die Automatisierung
nicht, wie es leere Verzeichnisse überwachen soll) oder die Dateiberechtigungen oder ACLs auf ausgecheckten Dateien zu setzen (Git überwacht ACLs nicht). Man könnte das z.B. nutzen, um Dateien zu ändern, nachdem man sie ausgecheckt hat – um etwa eine RCS-artige Variablensubstitution vorzunehmen –, das ist allerdings keine besonders gute Idee, weil Git glaubt, dass die Dateien lokal geändert wurden. Nutzen Sie für solche Aufgaben besser smudge/clean filters. • post-merge läuft, nachdem Sie eine Merge-Operation durchgeführt haben. Dieser Hook wird selten benutzt. Falls Ihr pre-commit-Hook irgendeine Art von Änderung am Repository vorgenommen hat, müssen Sie möglicherweise ein post-merge-Skript einsetzen, um etwas Ähnliches zu tun. • pre-auto-gc: hilft git gc --auto bei der Entscheidung, ob es Zeit zum Aufräumen ist oder nicht. Sie können git gc --auto veranlassen, seine git gc-Aufgabe zu überspringen, indem Sie Nonzero von diesem Skript zurückgeben lassen. Das wird allerdings selten gebraucht.
Verfügbare Hooks
| 287
Kapitel 15
KAPITEL 15
Projekte kombinieren
Es gibt viele Gründe dafür, externe Projekte mit Ihren eigenen zu kombinieren. Ein Submodul ist einfach ein Projekt, das einen Teil Ihres eigenen Git-Repository bildet, aber außerdem unabhängig in seinem eigenen Source-Control-Repository existiert. In diesem Kapitel wird besprochen, weshalb Entwickler Submodule erzeugen und wie Git versucht, mit ihnen umzugehen. Weiter vorn in diesem Buch haben wir mit einem Repository namens public_html gearbeitet, das unsere Website enthalten sollte. Wenn Ihre Website auf einer AJAX-Bibliothek wie Prototype oder jQuery beruht, müssen Sie eine Kopie dieser Bibliothek irgendwo in public_html haben. Außerdem wollen Sie in der Lage sein, diese Bibliothek automatisch zu aktualisieren, festzustellen, was sich geändert hat, wenn Sie das tun, und vielleicht sogar Änderungen zurück an die Autoren zu liefern. Oder vielleicht wollen Sie Änderungen machen und diese nicht zurückliefern, aber trotzdem Ihr Repository auf die neueste Version der Autoren aktualisieren. Git macht all das möglich. Hier nun jedoch die schlechte Nachricht: Gits anfängliche Unterstützung für Submodule war einfach furchtbar – aus dem einfachen Grund, weil keiner der Git-Entwickler Bedarf dafür sah. Inzwischen beginnt die Lage sich etwas zu bessern. Anfangs gab es nur zwei große Projekte, die Git benutzten: Git selbst und den Linux-Kernel. Diese Projekte hatten zwei wichtige Dinge gemein: Beide waren ursprünglich von Linus Torvalds geschrieben worden und wiesen praktisch keine Abhängigkeiten zu irgendeinem externen Projekt auf. Wo sie Code von anderen Projekten borgten, importierten sie ihn direkt und machten ihn zu ihrem eigenen. Es bestand nie die Absicht, diesen Code wieder zurück in ein anderes Projekt zu übertragen. So etwas würde nur selten vorkommen, sodass es einfach wäre, von Hand einige Diffs zu erzeugen und wieder zurück in das andere Projekt zu übermitteln.
| 289
Wenn die Submodule Ihres Projekts so sind – Dinge, die Sie einmal importieren und die das alte Projekt für immer hinter sich lassen –, brauchen Sie dieses Kapitel nicht. Sie wissen bereits genug über Git, um einfach ein Verzeichnis voller Dateien hinzuzufügen. Manchmal allerdings ist die Lage etwas komplizierter. In vielen Unternehmen sieht es oft so aus, dass es viele Anwendungen gibt, die auf einer gemeinsamen Bibliothek oder mehreren Bibliotheken beruhen. Alle Anwendungen sollen jeweils in einem eigenen GitRepository entwickelt, freigegeben, verzweigt und zusammengeführt werden, entweder weil sich diese Trennung logisch ergibt oder aufgrund von entsprechenden Rechten am Code. Allerdings wirft das Aufteilen Ihrer Anwendungen auf diese Weise immer ein Problem auf: Was ist mit der gemeinsamen Bibliothek? Jede Anwendung beruht auf einer speziellen Version der gemeinsamen Bibliothek, und Sie müssen den Überblick behalten, um welche Version genau es sich handelt. Wenn jemand die Bibliothek versehentlich auf eine Version umstellt, die noch nicht getestet wurde, kann es passieren, dass Ihre Anwendung nicht mehr funktioniert. Allerdings entwickelt sich die Bibliothek mit den Dienstprogrammen nicht von selbst; normalerweise gibt es Leute, die neue Funktionen hinzufügen, die sie in ihren eigenen Anwendungen benötigen. Schließlich wollen sie diese neuen Funktionen mit jedem teilen, der selbst Anwendungen schreibt; das ist letztendlich die Aufgabe einer Bibliothek Was können Sie tun? Darum geht es in diesem Kapitel. Wir erläutern verschiedene Strategien – auch wenn manche Leute ihnen diese Bezeichnung nicht zugestehen würden, sondern sie eher »Hacks« nennen –, die gebräuchlich sind, und schließe dann mit der intelligentesten Lösung, nämlich Submodulen.
Die alte Lösung: partielle Checkouts Eine beliebte Funktion in vielen Versionskontrollsystemen, so auch in CVS und Subversion, wird als partielles Checkout bezeichnet. Bei einem partiellen Checkout entscheiden Sie sich dafür, nur ein oder mehrere bestimmte Unterverzeichnisse des Repository zu beziehen und darin zu arbeiten.1 Falls Sie ein zentrales Repository haben, das alle Ihre Projekte aufnimmt, sind partielle Checkouts eine praktikable Methode für den Umgang mit Submodulen. Packen Sie einfach Ihre Bibliothek in ein Unterverzeichnis und die jeweiligen Anwendungen, die diese Bibliothek benutzen, in ein anderes. Wenn Sie eine Anwendung bekommen wollen, checken Sie zwei Unterverzeichnisse (die Bibliothek und die Anwendung) aus, statt alle Verzeichnisse auszuchecken: Das ist ein partielles Checkout.
1
Um genau zu sein, benutzt Subversion schlauerweise partielle Checkouts, um all seine Verzweigungs- und Tagging-Funktionen zu implementieren. Sie kopieren einfach Ihre Dateien in ein Unterverzeichnis und checken dann nur dieses Unterverzeichnis aus.
290 | Kapitel 15: Projekte kombinieren
Ein Vorteil von partiellen Checkouts besteht darin, dass Sie nicht den gigantischen, vollständigen Verlauf jeder Datei herunterladen müssen. Sie laden nur die Dateien herunter, die Sie für eine bestimmte Revision eines speziellen Projekts brauchen. Sie benötigen nicht einmal den vollständigen Verlauf dieser Dateien; die aktuelle Version allein könnte ausreichend sein. Diese Technik war besonders in einem älteren Versionskontrollsystem, nämlich in CVS, beliebt. CVS besitzt kein konzeptuelles Verständnis des »ganzen« Repository; es versteht nur den Verlauf einzelner Dateien. Um genau zu sein, wird der Verlauf der Dateien in den Dateien selbst gespeichert. Das CVS-Repository-Format war so einfach, dass der Repository-Administrator Kopien erstellen und symbolische Links zwischen unterschiedlichen Anwendungs-Repositories benutzen konnte. Beim Auschecken einer Kopie einer Anwendung wurden automatisch auch Kopien der referenzierten Dateien ausgecheckt. Sie mussten nicht einmal wissen, dass die Dateien auch von anderen Projekten benutzt werden. Diese Technik ist ein bisschen eigenartig, hat aber in vielen Projekten jahrelang funktioniert. Das KDE-Projekt (K Desktop Environment) z.B. ermuntert zu partiellen Checkouts aus seinem mehrere Gigabyte großen Subversion-Repository. Leider ist diese Idee nicht kompatibel mit verteilten Versionskontrollsystemen wie Git. Bei Git laden Sie nicht nur die aktuelle Version einer ausgewählten Menge von Dateien herunter, sondern alle Versionen aller Dateien. Schließlich ist jedes Git-Repository eine vollständige Kopie des Repository. Gits aktuelle Architektur unterstützt partielle Checkouts nicht besonders gut.2 Derzeit zieht man beim KDE-Projekt einen Umstieg von Subversion zu Git in Betracht, und Submodule sind der wichtigste Streitpunkt. Ein Import des gesamten KDE-Repository nach Git ist immer noch mehrere Gigabyte groß. Jeder KDE-Mitarbeiter müsste all diese Daten kopieren, selbst wenn er nur an einer Anwendung arbeiten wollte. Sie können aber nicht nur ein Repository pro Anwendung herstellen: Jede Anwendung hängt wieder von einer oder mehreren der KDE-Kernbibliotheken ab. Damit KDE erfolgreich zu Git wechseln kann, braucht es eine Alternative zu den riesigen, monolithischen Repositories mit partiellen Checkouts. So wurde z.B. bei einem experimentellen Import von KDE nach Git die Codebasis in ungefähr 500 separate Repositories aufgeteilt.3
2
Es gibt sogar einige experimentelle Patches, die partielle Checkouts in Git implementieren. Sie befinden sich allerdings nicht in einer der veröffentlichten Git-Versionen und werden es möglicherweise auch nie dorthin schaffen. Außerdem sind sie nur partielle Checkouts, keine partiellen Klone. Sie müssen immer noch den gesamten Verlauf herunterladen, selbst wenn dieser nicht in Ihrem Arbeitsbaum landet, und das beschränkt den Nutzen. Manche Leute wollen dieses Problem ebenfalls lösen, aber es ist außerordentlich schwierig – vielleicht sogar unmöglich –, es richtig zu machen.
3
Siehe http://labs.trolltech.com/blogs/2008/08/29/workflow-and-switching-to-git-part-2-the-tools/.
Die alte Lösung: partielle Checkouts | 291
Die offensichtliche Lösung: den Code in Ihr Projekt importieren Schauen wir uns eine der Möglichkeiten an, die bereits erwähnt wurden: Wieso nicht einfach die Bibliothek in ein Unterverzeichnis in Ihrem eigenen Projekt importieren? Anschließend können Sie immer dann eine neue Gruppe von Dateien hineinkopieren, wenn Sie die Bibliothek aktualisieren wollen. Je nach Ihren Anforderungen kann diese Methode tatsächlich ganz gut funktionieren. Sie hat folgende Vorteile: • Sie erhalten niemals versehentlich die falsche Bibliotheksversion. • Sie ist außerordentlich einfach zu erklären und zu verstehen und verwendet nur alltägliche Git-Funktionen. • Sie funktioniert immer gleich, ob die externe Bibliothek nun in Git oder in einem anderen Versionskontrollsystem gepflegt wird oder überhaupt nicht der Versionskontrolle unterliegt. • Ihr Anwendungs-Repository ist immer eigenständig, sodass ein git clone Ihrer Anwendung immer alles enthält, was Ihre Anwendung braucht. • Es ist leicht, anwendungsspezifische Patches auf die Bibliothek in Ihrem eigenen Repository anzuwenden, selbst wenn Sie keinen Commit-Zugriff auf das Repository der Bibliothek haben. • Beim Verzweigen Ihrer Anwendung entsteht auch ein Zweig von der Bibliothek, ganz wie erwartet. • Wenn Sie die subtree-Merge-Strategie einsetzen, wie in »Besondere Merges« auf Seite 155 beschrieben, (mit dem git pull -s subtree-Befehl), dann ist das Aktualisieren auf neuere Versionen der Bibliothek genauso einfach wie das Aktualisieren eines anderen Teils Ihres Projekts. Leider gibt es auch einige Nachteile: • Jede Anwendung, die diese Bibliothek importiert, dupliziert die Dateien dieser Bibliothek. Es gibt keine einfache Methode, mit der sich diese Git-Objekte zwischen Repositories verteilen ließen. Würde z.B. KDE das tun und Sie wollten das gesamte Projekt auschecken – etwa, weil Sie die KDE-Distributionspakete für Debian oder Red Hat zusammenstellen wollten –, müssten Sie letztendlich die gleichen Bibliotheksdateien Dutzende von Malen herunterladen. • Falls Ihre Anwendung Änderungen an Ihrer Kopie der Bibliothek vornimmt, besteht die einzige Möglichkeit, diese Änderungen zu verteilen, darin, Diffs zu erzeugen und diese auf das Repository der Hauptbibliothek anzuwenden. Das ist OK, wenn Sie es nur selten tun, bedeutet aber eine Menge nervender Arbeit, wenn Sie es öfter machen müssen.
292 | Kapitel 15: Projekte kombinieren
Für viele Leute und viele Projekte sind diese Nachteile nicht so schlimm. Sie sollten diese Technik auf jeden Fall in Betracht ziehen, da ihre Einfachheit oft die Nachteile aufwiegt. Wenn Sie mit anderen Versionskontrollsystemen, speziell mit CVS, vertraut sind, haben Sie möglicherweise einige schlechte Erfahrungen gemacht, die Sie veranlassen, diese Methode zu vermeiden. Sie sollten sich bewusst sein, dass viele dieser Probleme in Git nicht mehr auftreten. Sehen Sie hier: • CVS hat das Umbenennen von Dateien oder Verzeichnissen nicht unterstützt, und seine Funktionen (z.B. »Herstellerzweige«) zum Importieren von neuen UpstreamPaketen bedeuteten, dass es einfach war, Fehler zu machen. Ein häufig gemachter Fehler war, zu vergessen, alte Dateien zu löschen, wenn man mit Merges neue Versionen erzeugte, was zu seltsamen Inkonsistenzen führte. Git hat dieses Problem nicht, da das Importieren eines Pakets einfach nur bedeutet, dass ein Verzeichnis gelöscht, neu erzeugt und git add --all benutzt wird. • Das Importieren eines neuen Moduls kann ein mehrstufiger Vorgang sein, der mehrere Commits erfordert. Dabei können Sie Fehler machen. In CVS oder Subversion bilden solche Fehler einen dauerhaften Teil des Repository-Verlaufs. Das ist normalerweise harmlos, allerdings können Fehler das Repository unnötig aufblähen, wenn man große Dateien importiert. Wenn Sie mit Git etwas vermasseln, werfen Sie die fehlerhaften Commits einfach weg, bevor Sie sie an jemanden verschieben. • CVS machte es schwer, den Verlauf der Zweige zu verfolgen. Falls Sie Upstream-Version 1.0 importierten, dann einige Ihrer eigenen Änderungen vornahmen und anschließend Version 2.0 importieren wollten, war es kompliziert, Ihre lokalen Änderungen zu extrahieren und noch einmal anzuwenden. Die verbesserte Verwaltung der Verläufe in Git erleichtert dieses Vorhaben beträchtlich. • Manche Versionskontrollsysteme sind sehr langsam, wenn sie eine große Anzahl von Dateien nach Änderungen durchsuchen. Falls Sie mit dieser Technik mehrere große Pakete importieren, könnten die Geschwindigkeitseinbußen die Produktivitätsgewinne wieder zunichte machen, die Sie aufgrund der Verwendung von Submodulen in Ihrem Repository zu verzeichnen haben. Git wurde im Gegensatz dazu für den Umgang mit Zehntausenden von Dateien in einem Projekt optimiert, sodass das eher kein Problem darstellen sollte. Falls Sie beschließen, Submodule direkt zu importieren, stehen Ihnen zwei Methoden zur Verfügung: Sie können die Dateien manuell kopieren oder den Verlauf importieren.
Teilprojekte durch Kopieren importieren Am einfachsten importieren Sie die Dateien eines anderen Projekts in Ihr Projekt, indem Sie sie kopieren. Falls das andere Projekt nicht in einem Git-Repository gespeichert ist, ist das sogar Ihre einzige Möglichkeit.
Die offensichtliche Lösung: den Code in Ihr Projekt importieren | 293
Man geht dabei erwartungsgemäß so vor: Löschen Sie alle Dateien, die sich bereits in diesem Verzeichnis befinden, legen Sie die gewünschten Dateien an (z.B. indem Sie eine TAR- oder ZIP-Datei auspacken, in der die Bibliothek enthalten ist, die Sie importieren wollen) und fügen Sie sie dann mit git add hinzu, zum Beispiel so: $ $ $ $ $ $ $
cd myproject.git rm -rf mylib git rm mylib tar -xzf /tmp/mylib-1.0.5.tar.gz mv mylib-1.0.5 mylib git add mylib git commit
Diese Methode funktioniert prima, Sie müssen allerdings Folgendes beachten: • Nur die exakten Versionen der Bibliothek, die Sie importieren, erscheinen in Ihrem Git-Verlauf. Verglichen mit der nächsten Möglichkeit – Einbinden des kompletten Verlaufs des Teilprojekts –, werden Sie das tatsächlich geeignet finden, da damit Ihre Logdateien sauber bleiben. • Wenn Sie anwendungsspezifische Änderungen an den Bibliotheksdateien vornehmen, müssen Sie diese Änderungen immer wieder anwenden, wenn Sie eine neue Version importieren. Sie müssen z.B. die Änderungen manuell mit git diff extrahieren und sie mit git apply wieder einbauen (mehr dazu in Kapitel 8 oder Kapitel 13). Git erledigt das nicht automatisch. • Das Importieren einer neuen Version verlangt von Ihnen, dass Sie jedesmal die komplette Befehlsfolge zum Entfernen und Hinzufügen von Dateien durchlaufen; git pull allein reicht nicht aus. Andererseits ist diese Methode leicht zu verstehen und den Mitarbeitern gut zu vermitteln.
Teilprojekte mit git pull -s subtree importieren Eine andere Methode zum Importieren eines Teilprojekts besteht darin, dass Sie den gesamten Verlauf aus diesem Teilprojekt mit einem Merge in Ihr Repository holen. Natürlich funktioniert das nur, wenn der Verlauf des Teilprojekts bereits in Git gespeichert ist. Beim ersten Mal ist das ein bisschen schwierig; wenn Sie es aber einmal eingerichtet haben, sind künftige Merges viel einfacher als die einfache Dateikopiermethode. Da Git den gesamten Verlauf des Teilprojekts kennt, weiß es genau, was jedesmal passieren muss, wenn Sie ein Update durchführen. Nehmen Sie einmal an, Sie wollen eine neue Anwendung namens myapp schreiben und möchten eine Kopie des Git-Quellcodes in ein Verzeichnis namens git einfügen. Zuerst erzeugen wir das neue Repository und führen das erste Commit aus. (Wenn Sie bereits ein myapp-Projekt haben, können Sie diesen Teil überspringen.)
294 | Kapitel 15: Projekte kombinieren
$ cd /tmp $ mkdir myapp $ cd myapp $ git init Initialized empty Git repository in /tmp/myapp/.git/ $ ls $ echo hello > hello.txt $ git add hello.txt $ git commit -m 'erstes Commit' Created initial commit 644e0ae: erstes Commit 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hello.txt
Nun importieren Sie das git-Projekt aus Ihrer lokalen Kopie, also vermutlich ~/git.git.4 Der erste Schritt ist der gleiche wie im vorangegangenen Abschnitt: Extrahieren einer Kopie in ein Verzeichnis namens git, anschließend Ausführen eines Commit. Das folgende Beispiel nimmt eine spezielle Version des git.git-Projekts, die mit dem Tag v1.6.0 gekennzeichnet ist. Der Befehl git archive v1.6.0 erzeugt eine TAR-Datei aller v1. 6.0-Dateien. Sie wird dann im neuen Unterverzeichnis git ausgepackt: $ ls hello.txt $ mkdir git $ cd git $ (cd ~/git.git && git archive v1.6.0) | tar -xf $ cd .. $ ls git/
hello.txt
$ git add git $ git commit -m 'git v1.6.0 importiert' Created commit 72138f0: git v1.6.0 importiert 1440 files changed, 299543 insertions(+), 0 deletions(-)
Sie haben also nun die (Anfangs-)Dateien von Hand importiert, allerdings weiß Ihr myapp-Projekt noch nichts über den Verlauf seines Submoduls. Sie müssen nun Git informieren, dass Sie v1.6.0 importiert haben, was bedeutet, dass Sie auch den gesamten Verlauf bis v1.6.0 haben sollten. Benutzen Sie dazu die -s ours-Merge-Strategie (aus Kapitel 9) mit dem Befehl git pull. Erinnern Sie sich, dass -s ours einfach Folgendes 4
Falls Sie ein solches Repository noch nicht haben, können Sie es von git://git.kernel.org/pub/scm/git/git.git klonen.
Die offensichtliche Lösung: den Code in Ihr Projekt importieren | 295
bedeutet: »Zeichne auf, dass wir einen Merge durchführen, aber meine Dateien sind die richtigen Dateien, ändere deshalb nichts.« Git führt keinen Abgleich der Verzeichnisse und Dateiinhalte zwischen Ihrem Projekt und dem importierten Projekt durch oder so etwas. Stattdessen importiert Git nur den Verlauf und die Baumpfadnamen, so wie sie im ursprünglichen Teilprojekt gefunden werden. Wir müssen diese »umgelagerte« Verzeichnisbasis allerdings später berücksichtigen. Aufgrund einer Eigenart von git pull funktioniert es nicht, einfach v1.6.0 zu ziehen: $ git pull -s ours ~/git.git v1.6.0 fatal: Couldn't find remote ref v1.6.0 fatal: The remote end hung up unexpectedly
Das könnte sich in einer künftigen Version von Git ändern, im Moment wird dem Problem dadurch begegnet, dass man refs/tags/v1.6.0 explizit ausschreibt, wie in »Refs und Symrefs« auf Seite 74 beschrieben wurde: $ git pull -s ours ~/git.git refs/tags/v1.6.0 warning: no common commits remote: Counting objects: 67034, done. remote: Compressing objects: 100% (19135/19135), done. remote: Total 67034 (delta 47938), reused 65706 (delta 46656) Receiving objects: 100% (67034/67034), 14.33 MiB | 12587 KiB/s, done. Resolving deltas: 100% (47938/47938), done. From ~/git.git * tag v1.6.0 -> FETCH_HEAD Merge made by ours.
Wenn alle v1.6.0-Dateien bereits bestätigt worden wären, könnten Sie den Eindruck gewinnen, dass nichts mehr zu tun wäre. Dabei hat Git im Gegenteil den vollständigen Verlauf von git.git bis v1.6.0 importiert, sodass unser Repository jetzt viel umfassender ist, selbst wenn die Dateien gleich geblieben sind. Um sicherzugehen, wollen wir einmal überprüfen, dass das Merge-Commit, das wir gerade erzeugt haben, wirklich keine Dateien geändert hat: $ git diff HEAD^ HEAD
Dieser Befehl dürfte keine Ausgabe liefern, was bedeutet, dass die Dateien vor und nach dem Merge exakt gleichgeblieben sind. Gut. Schauen wir uns nun an, was passiert, wenn wir an unserem Teilprojekt lokale Änderungen vornehmen und dann später versuchen, es aufzurüsten. Ändern Sie zuerst eine Kleinigkeit: $ cd git $ echo 'Ich bin ein Git-Mitarbeiter!' > contribution.txt $ git add contribution.txt $ git commit -m 'Mein erster Beitrag zu Git'
296 | Kapitel 15: Projekte kombinieren
Created commit 6c9fac5: Mein erster Beitrag zu Git 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 git/contribution.txt
Unsere Version des Git-Teilprojekts ist nun v1.6.0 mit einem zusätzlichen Patch. Schließlich wollen wir unser Git auf das Versions-Tag v1.6.0.1 umstellen, ohne allerdings unseren zusätzlichen Beitrag zu verlieren. Es ist ganz einfach: $ git pull -s subtree ~/git.git refs/tags/v1.6.0.1 remote: Counting objects: 179, done. remote: Compressing objects: 100% (72/72), done. remote: Total 136 (delta 97), reused 100 (delta 61) Receiving objects: 100% (136/136), 25.24 KiB, done. Resolving deltas: 100% (97/97), completed with 40 local objects. From ~/git.git * tag v1.6.0.1 -> FETCH_HEAD Merge made by subtree.
Vergessen Sie nicht, bei Ihrem Pull die -s subtree-Merge-Strategie anzugeben. Der Merge könnte auch ohne -s subtree funktioniert haben, weil Git weiß, wie es mit Dateiumbenennungen umzugehen hat. Und wir haben eine Menge Umbenennungen: Alle Dateien aus dem git.git-Projekt wurden aus dem Wurzelverzeichnis des Projekts in ein Unterverzeichnis namens git verschoben. Das Flag -s subtree teilt Git mit, dass es direkt nach dieser Situation suchen und sie verarbeiten soll. Um sicherzugehen, sollten Sie immer -s subtree benutzen, wenn Sie ein Teilprojekt in ein Unterverzeichnis überführen (außer während des ersten Imports, wo Sie -s ours verwenden sollten, wie wir gesehen haben).
War es wirklich so einfach? Überprüfen wir einmal, ob die Dateien korrekt aktualisiert wurden. Da alle Dateien in v1.6.0.1 sich im Wurzelverzeichnis befanden und nun im gitVerzeichnis liegen, müssen wir eine ungewöhnliche »Selektor«-Syntax mit git diff benutzen. In diesem Fall sagen wir: »Teile mir den Unterschied mit zwischen dem Commit, aus dem wir den Merge ausgeführt haben (d.h. Eltern-Commit #2, also v1.6.0.1), und dem Commit, in das wir den Merge ausgeführt haben, die HEAD-Version.« Da sich das letztere im git-Verzeichnis befindet, müssen wir nach dem Doppelpunkt dieses Verzeichnis angeben. Das erstere ist in seinem Wurzelverzeichnis, wir können also den Doppelpunkt weglassen, da standardmäßig dieses Verzeichnis gewählt wird. Der Befehl und seine Ausgabe sehen so aus: $ git diff HEAD^2 HEAD:git diff --git a/contribution.txt b/contribution.txt new file mode 100644 index 0000000..7d8fd26 --- /dev/null +++ b/contribution.txt @@ -0,0 +1 @@ +Ich bin ein Git-Mitarbeiter!
Die offensichtliche Lösung: den Code in Ihr Projekt importieren | 297
Es hat funktioniert! Der einzige Unterschied zu v1.6.0.1 ist die Änderung, die wir später eingebracht haben. Woher wussten wir, dass es HEAD^2 war? Nach dem Merge können Sie das Commit untersuchen und feststellen, welche Zweig-HEADs zusammengeführt wurden: Merge: 6c9fac5... 5760a6b...
Wie bei jedem Merge sind das HEAD^1 und HEAD^2. Sie sollten das zweite erkennen: commit 5760a6b094736e6f59eb32c7abb4cdbb7fca1627 Author: Junio C Hamano Date: Sun Aug 24 14:47:24 2008 -0700 GIT 1.6.0.1 Signed-off-by: Junio C Hamano
Wenn unsere Lage etwas komplexer ist, müssen Sie Ihr Teilprojekt möglicherweise tiefer in der Repository-Struktur platzieren und können nicht wie in diesem Beispiel auf der obersten Ebene bleiben, z.B. könnte es sein, dass Sie stattdessen other/projects/git brauchen. Git verfolgt nicht automatisch die Umlagerung des Verzeichnisses, wenn Sie es importiert haben. Daher müssten Sie wie gehabt den vollständigen Pfad zum importierten Teilprojekt ausschreiben: $ git diff HEAD^2 HEAD:other/projects/git
Sie können außerdem unsere Beiträge zum git-Verzeichnis Commit-weise einbringen: $ git log --no-merges HEAD^2..HEAD commit 6c9fac58bed056c5b06fd70b847f137918b5a895 Author: Jon Loeliger <[email protected]> Date: Sat Sep 27 22:32:49 2008 -0400 Mein erster Beitrag zu Git commit 72138f05ba3e6681c73d0585d3d6d5b0ad329b7c Author: Jon Loeliger <[email protected]> Date: Sat Sep 27 22:17:49 2008 -0400 git v1.6.0 importiert
Mit -s subtree können Sie beliebig oft Updates aus dem Hauptprojekt git.git in Ihr Teilprojekt einbringen. Das funktioniert genauso gut, als hätten Sie Ihre eigene Verzweigung des git.git-Projekts.
Ihre Änderungen upstream übermitteln Es ist zwar recht einfach, einen Verlauf in Ihr Teilprojekt hinein zu überführen, der umgekehrte Weg, nämlich etwas wieder herauszunehmen, ist viel schwieriger. Das liegt daran, dass diese Technik für das Teilprojekt keinen Verlauf pflegt. Es gibt nur den Verlauf des ganzen Anwendungsprojekts, einschließlich des Teilprojekts.
298 | Kapitel 15: Projekte kombinieren
Sie könnten den Verlauf Ihres Projekts mit der -s subtree-Merge-Strategie wieder zurück in git.git überführen, allerdings mit unerwarteten Ergebnissen: Sie importieren alle Commits aus dem ganzen Anwendungsprojekt und zeichnen dann die Löschung aller Dateien auf, mit Ausnahme derjenigen im git-Verzeichnis zum Zeitpunkt des letzten Merge. So ein zusammengeführter Bereich ist technisch gesehen korrekt, allerdings ist es schlicht und ergreifend falsch, den Verlauf Ihrer gesamten Anwendung in das Repository zu legen, in dem sich das Teilprojekt befindet. Es würde außerdem bedeuten, dass alle Versionen aller Dateien in Ihrer Anwendung permanenter Bestandteil des git-Projekts würden. Sie gehören nicht hierher, und es wäre Zeitverschwendung und würde eine Unmenge an irrelevanten Informationen erzeugen und Arbeit vergeuden. Es ist der falsche Ansatz. Stattdessen müssen Sie alternative Methoden einsetzen, etwa git format-patch (siehe Kapitel 13). Das erfordert mehr Schritte als ein einfaches git pull. Glücklicherweise müssen Sie es nur tun, wenn Sie Änderungen zurück in das Teilprojekt übermitteln, nicht jedoch in dem viel häufiger auftretenden Fall, in dem Sie Teilprojektänderungen in Ihre Anwendung ziehen.
Die automatisierte Lösung: Teilprojekte mit eigenen Skripten auschecken Nach dem Lesen des vorangegangenen Abschnitts haben Sie vielleicht Vorbehalte dagegen, den Verlauf Ihres Teilprojekts direkt in ein Unterverzeichnis Ihrer Anwendung zu kopieren. Schließlich kann jeder sehen, dass die beiden Projekte getrennt sind: Ihre Anwendung hängt von der Bibliothek ab, es sind aber offensichtlich zwei unterschiedliche Projekte. Das Zusammenführen der beiden Verläufe fühlt sich nicht wie eine saubere Lösung an. Es gibt andere Wege, um das Problem anzugehen, die Ihnen wahrscheinlich besser gefallen. Schauen wir uns eine offensichtliche Methode an: Klonen Sie das Teilprojekt einfach immer dann von Hand mit git clone in ein Unterverzeichnis, wenn Sie das Hauptprojekt klonen. $ $ $ $
git clone myapp myapp-test cd myapp-test git clone ~/git.git git echo git >.gitignore
Diese Methode erinnert an das partielle Checkout in Subversion oder CVS. Anstatt nur einige Unterverzeichnisse eines riesigen Projekts auszuchecken, checken Sie zwei kleine Projekte aus, der Grundgedanke ist aber der gleiche.
Die automatisierte Lösung: Teilprojekte mit eigenen Skripten auschecken | 299
Diese Methode des Umgangs mit Submodulen hat einige wesentliche Vorteile: • Das Submodul muss nicht in Git sein; es kann sich in einem beliebigen Versionskontrollsystem befinden oder auch als TAR- oder ZIP-Datei vorliegen. Da Sie die Dateien von Hand beziehen, können Sie sie von einer beliebigen Stelle holen. • Der Verlauf Ihres Hauptprojekts wird niemals mit dem Verlauf Ihrer Teilprojekte vermischt. Das Log wird nicht mit beziehungslosen Commits vollgemüllt und das Git-Repository selbst bleibt klein. • Falls Sie Änderungen am Teilprojekt vornehmen, können Sie sie genauso zurückliefern, als hätten Sie am Teilprojekt selbst gearbeitet – weil Sie das im Prinzip auch tun. Natürlich gibt es auch einige Probleme, die Sie lösen müssen: • Es kann anstrengend sein, anderen Benutzern erklären zu müssen, wie sie all die Teilprojekte auszuchecken haben. • Sie müssen irgendwie sicherstellen, dass Sie die richtige Revision der einzelnen Teilprojekte bekommen. • Wenn Sie in einen anderen Zweig Ihres Hauptprojekts wechseln oder mit git pull Änderungen von jemandem holen, wird das Teilprojekt nicht automatisch aktualisiert. • Wenn Sie Änderungen am Teilprojekt vornehmen, müssen Sie daran denken, sie mit git push separat zu verschieben. • Wenn Sie nicht das Recht besitzen, wieder etwas zurück in das Teilprojekt zu übertragen (d.h. wenn Sie keinen Commit-Zugriff auf dessen Repository haben), ist es nicht so einfach, anwendungsspezifische Änderungen vorzunehmen. (Befindet sich das Teilprojekt in Git, können Sie natürlich immer eine öffentliche Kopie Ihrer Änderungen irgendwohin legen.) Kurz gesagt, bietet Ihnen das manuelle Klonen der Teilprojekte unendliche Flexibilität, andererseits ist es leicht, das Ganze unnötig zu verkomplizieren oder Fehler zu machen. Wenn Sie sich für diese Methode entscheiden, besteht der beste Ansatz darin, ihn zu standardisieren, indem Sie einige einfache Skripten schreiben und diese in Ihr Repository aufnehmen. Sie könnten z.B. ein Skript namens ./update-submodules.sh nehmen, das alle Ihre Submodule automatisch klont oder aktualisiert. Je nachdem, wie viel Aufwand Sie investieren, könnte ein solches Skript Ihre Submodule auf bestimmte Zweige oder Tags oder sogar auf bestimmte Revisionen aktualisieren. Sie könnten z.B. Commit-IDs hart im Skript kodieren und dann eine neue Version des Skripts in Ihrem Hauptprojekt bestätigen, wenn Sie Ihre Anwendung auf eine neue Version der Bibliothek aktualisieren wollen. Wenn jemand dann eine bestimmte Revision Ihrer Anwendung auscheckt, kann er das Skript ausführen, um automatisch die entsprechende Version der Bibliothek abzuleiten.
300 | Kapitel 15: Projekte kombinieren
Man könnte auch in Betracht ziehen, mit den Techniken aus Kapitel 14 ein Commitoder Update-Hook zu erzeugen, das verhindert, dass Sie versehentlich ein Commit in Ihr Hauptprojekt ausführen, wenn die Änderungen an Ihrem Teilprojekt noch nicht richtig bestätigt und verschoben wurden. Sie können sich sicher vorstellen, dass nicht nur Sie, sondern auch andere Leute ihre Teilprojekte auf diese Weise verwalten wollen. Das heißt: Skripten zum Standardisieren und Automatisieren dieses Vorgangs gibt es bereits. Ein solches Skript von Miles Georgi heißt externals (oder ext). Sie finden es unter http://nopugs.com/ext-tutorial. Bequemerweise funktioniert ext für beliebige Kombinationen aus Subversion- und Git-Projekten und Teilprojekten.
Die native Lösung: Gitlinks und git submodule Git enthält einen Befehl für das Arbeiten mit Submodulen: git submodule. Ich habe ihn mir aus zwei Gründen bis zum Schluss aufgehoben: • Er ist viel komplizierter als das einfache Importieren des Verlaufs von Teilprojekten in das Repository Ihres Hauptprojekts und • im Prinzip ist er identisch mit der gerade besprochenen skriptbasierten Lösung, dabei aber viel restriktiver. Auch wenn es so klingt, als wären Git-Submodule eine natürliche Option, sollten Sie sorgfältig nachdenken, bevor Sie sie einsetzen. Gits Unterstützung für Submodule entwickelt sich schnell. Die erste Erwähnung von Submodulen im Verlauf der Git-Entwicklung erfolgte im April 2007 durch Linus Torvalds. Seitdem hat es zahlreiche Änderungen gegeben. Es ist ein bisschen wie das Schießen auf bewegliche Ziele. Führen Sie auf jeden Fall git help submodule in Ihrer GitVersion aus, um festzustellen, ob sich inzwischen wieder etwas geändert hat. Leider ist der Befehl git submodule nicht besonders transparent; Sie werden ihn kaum effektiv benutzen können, wenn Sie nicht genau wissen, wie er funktioniert. Es handelt sich um eine Kombination aus zwei getrennten Funktionen: sogenannten Gitlinks und dem eigentlichen git submodule-Befehl.
Gitlinks Ein Gitlink ist ein Link von einem Baumobjekt auf ein Commit-Objekt. Sie wissen aus Kapitel 4, dass jedes Commit-Objekt auf ein Baumobjekt verweist und dass jedes Baumobjekt auf eine Menge aus Blobs und Bäumen zeigt, die jeweils Dateien bzw. Verzeichnissen entsprechen. Das Baumobjekt eines Commit identifiziert eindeutig die exakte Menge von Dateien, Dateinamen und Berechtigungen, die mit diesem Commit verknüpft sind. Im Abschnitt »Commit-Graphen« auf Seite 81 wurde außerdem gesagt,
Die native Lösung: Gitlinks und git submodule | 301
dass die Commits selbst in einem gerichteten azyklischen Graphen miteinander verbunden sind. Jedes Commit-Objekt verweist auf null oder mehr Eltern-Commits, zusammen beschreiben sie den Verlauf Ihres Projekts. Uns ist aber noch kein Baumobjekt begegnet, das auf ein Commit-Objekt zeigt. Der Gitlink bildet Gits Mechanismus zum Anzeigen einer direkten Referenz auf ein anderes GitRepository. Probieren wir es aus. Wie im Abschnitt »Teilprojekte mit git pull -s subtree importieren« auf Seite 294 erzeugen wir ein myapp-Repository und importieren den Git-Quellcode: $ cd /tmp $ mkdir myapp $ cd myapp # Start des neuen Superprojekts $ git init Initialized empty Git repository in /tmp/myapp/.git/ $ echo hello >hello.txt $ git add hello.txt $ git commit -m 'erstes Commit' [master (root-commit)]: created c3d9856: "erstes Commit" 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hello.txt
Dieses Mal importieren wir allerdings das git-Projekt direkt, d.h. wir verwenden nicht wie beim letzten Mal git archive: $ ls hello.txt # In einen Repository-Klon kopieren $ git clone ~/git.git git Initialized empty Git repository in /tmp/myapp/git/.git/ $ cd git # Etablieren der gewünschten Submodul-Version $ git checkout v1.6.0 Note: moving to "v1.6.0" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b HEAD is now at ea02eef... GIT 1.6.0 # Zurück zum Superprojekt $ cd .. $ ls git/
hello.txt
302 | Kapitel 15: Projekte kombinieren
$ git add git $ git commit -m 'git v1.6.0 importiert' [master]: created b0814ac: "git v1.6.0 importiert" 1 files changed, 1 insertions(+), 0 deletions(-) create mode 160000 git
Da es bereits ein Verzeichnis namens git/.git gibt (erzeugt während des git clone), ist git add git in der Lage, einen Gitlink darauf zu setzen. Normalerweise wären git add git und git add git/ (mit dem POSIX-kompatiblen abschließenden Slash, der anzeigt, dass git ein Verzeichnis sein muss) äquivalent. Das gilt aber nicht, wenn Sie einen Gitlink erzeugen wollen! In der gerade gezeigten Sequenz würde ein Slash (also der Befehl git add git/) verhindern, dass ein Gitlink angelegt wird; der Befehl würde lediglich alle Dateien im git-Verzeichnis hinzufügen, was wahrscheinlich nicht direkt das ist, was Sie wollen.
Schauen Sie, wie sich das Ergebnis der vorangegangenen Sequenz von dem der vergleichbaren Schritte in »Teilprojekte mit git pull -s subtree importieren« auf Seite 294 unterscheidet. In diesem Abschnitt hat das Commit alle Dateien im Repository geändert. Dieses Mal zeigt die Commit-Meldung, dass sich nur eine Datei geändert hat. Der resultierende Baum sieht so aus: $ git ls-tree HEAD 160000 commit ea02eef096d4bfcbb83e76cfab0fcb42dbcad35e 100644 blob ce013625030ba8dba906f756967f9e9ca394464a
git hello.txt
Das Unterverzeichnis git ist vom Typ commit und hat den Modus 160000. Das macht es zu einem Gitlink. Normalerweise betrachtet Git Gitlinks als einfache Zeigerwerte oder Referenzen auf andere Repositories. Die meisten Git-Operationen, z.B. clone, dereferenzieren die Gitlinks nicht und arbeiten dann auch nicht auf dem Submodul-Repository. Falls Sie z.B. Ihr Projekt in ein anderes Repository schieben, werden die Commit-, Baumund Blob-Objekte nicht mitverschoben. Wenn Sie Ihr Superprojekt-Repository klonen, werden die Verzeichnisse des Teilprojekt-Repository leer sein. Im folgenden Beispiel bleibt das git-Verzeichnis des Teilprojekts nach dem Befehl clone leer: $ cd /tmp $ git clone myapp app2 Initialized empty Git repository in /tmp/app2/.git/ $ cd app2 $ ls git/
hello.txt
Die native Lösung: Gitlinks und git submodule | 303
$ ls git $ du git 4 git
Gitlinks verfügen über die wichtige Eigenschaft, dass sie Verbindungen zu Objekten herstellen, die in Ihrem Repository fehlen dürfen. Schließlich sollen sie ja zu einem anderen Repository gehören. Und weil es den Gitlinks erlaubt ist zu fehlen, erreicht diese Technik sogar eines der ursprünglichen Ziele: partielle Checkouts. Sie müssen nicht jedes Teilprojekt auschecken; es reicht, wenn Sie die auschecken, die Sie brauchen. Sie wissen nun, wie Sie einen Gitlink erzeugen und dass er fehlen darf. Allerdings sind fehlende Objekte an sich nicht besonders sinnvoll. Wie bekommen Sie sie zurück? Dafür gibt es den Befehl git submodule.
Der Befehl git submodule Momentan handelt es sich bei dem Befehl git submodule in Wirklichkeit um ein 700 Zeilen langes Unix-Shellscript namens git-submodule.sh. Und wenn Sie dieses Buch ganz durchgelesen haben, wissen Sie genug, um dieses Skript selbst zu schreiben. Seine Aufgabe ist ganz einfach: Es soll Gitlinks und die dazugehörigen Repositories für Sie auschecken. Erstens sollten Sie sich bewusst sein, dass das Auschecken der Dateien eines Submoduls keine Zauberei ist. Im Verzeichnis app2, das wir gerade geklont haben, könnten Sie das selbst erledigen: $ cd /tmp/app2 $ git ls-files --stage -- git 160000 ea02eef096d4bfcbb83e76cfab0fcb42dbcad35e 0
git
$ rmdir git $ git clone ~/git.git git Initialized empty Git repository in /tmp/app2/git/.git/ $ cd git $ git checkout ea02eef Note: moving to "ea02eef" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b HEAD is now at ea02eef... GIT 1.6.0
Die gerade ausgeführten Befehle sind äquivalent zu git submodule update. Der einzige Unterschied besteht darin, dass git submodule die nervende Arbeit erledigt, etwa das
304 | Kapitel 15: Projekte kombinieren
Ermitteln der korrekten Commit-ID für das Auschecken. Leider weiß er nicht, wie er das ohne Hilfe tun soll: $ git submodule update No submodule mapping found in .gitmodules for path 'git'
Der Befehl git submodule muss eine wichtige Information kennen, bevor er irgendetwas tun kann: Wo kann er das Repository für Ihr Submodul finden? Er bezieht diese Information aus einer Datei namens .gitmodules, die so aussieht: [submodule "git"] path = git url = /home/bob/git.git
Die Benutzung der Datei erfolgt in zwei Schritten. Zuerst wird die Datei .gitmodules angelegt, entweder von Hand oder mit git submodule add. Da wir den Gitlink bereits mit git add erzeugt haben, ist es jetzt zu spät für git submodule add. Deshalb erzeugen wir die Datei von Hand: $ cat >.gitmodules <<EOF [submodule "git"] path = git url = /home/bob/git.gitEOF
Der Befehl git submodule add, der die gleichen Operationen ausführt, lautet $ git submodule add /home/bob/git.git git
Der Befehl git submodule add fügt einen Eintrag in .gitmodules ein und füllt ein neues Git-Repository mit einem Klon des hinzugefügten Repository.
Führen Sie dann git submodule init aus, um die Einstellungen aus der Datei .gitmodules in Ihre .git/config-Datei zu kopieren: $ git submodule init Submodule 'git' (/home/bob/git.git) registered for path 'git' $ cat .git/config [core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true [remote "origin"] url = /tmp/myapp fetch = +refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge = refs/heads/master [submodule "git"] url = /home/bob/git.git
Der Befehl git submodule init hat nur die letzten beiden Zeilen hinzugefügt.
Die native Lösung: Gitlinks und git submodule | 305
Sie führen diesen Schritt aus, damit Sie Ihre lokalen Submodule so rekonfigurieren können, dass sie auf ein anderes Repository zeigen als auf das im offiziellen .gitmodules. Wenn Sie einen Klon von einem anderen Projekt herstellen, das Submodule verwendet, müssen Sie Ihre eigene Kopie der Submodule behalten und Ihren lokalen Klon darauf verweisen. In diesem Fall dürfen Sie den offiziellen Ort des Moduls in .gitmodules nicht ändern, aber trotzdem soll git submodule an Ihrer bevorzugten Stelle nachschauen. git submodule init kopiert also alle fehlenden Submodul-Informationen von .gitmodules nach .git/config, wo Sie sie sicher bearbeiten können. Suchen Sie einfach den Abschnitt [submodule], der auf das Submodul verweist, das Sie ändern, und bearbeiten Sie die URL. Führen Sie schließlich git submodule update aus, um die Dateien tatsächlich zu aktualisieren, oder klonen Sie, falls nötig, das ursprüngliche Teilprojekt-Repository: # Erzwingen Sie einen völlig neuen Klon, indem Sie löschen, was sich hier befand $ rm -rf git $ git submodule update Initialized empty Git repository in /tmp/app2/git/.git/ Submodule path 'git': checked out 'ea02eef096d4bfcbb83e76cfab0fcb42dbcad35e'
Hier geht git submodule update in das Repository, auf das in Ihrer .git/config verwiesen wird, holt die Commit-ID, die in git ls-tree HEAD -- git zu finden ist, und checkt diese Revision in das Verzeichnis aus, das in .git/config angegeben wurde. Sie müssen noch einige andere Dinge wissen: • Wenn Sie die Zweige wechseln oder einen fremden Zweig mit git pull ziehen, müssen Sie immer git submodule update ausführen, um eine passende Menge von Submodulen zu erhalten. Das erfolgt nicht automatisch, da Sie bei einem Fehler Arbeit im Submodul verlieren könnten. • Wenn Sie in einen anderen Zweig wechseln und nicht git submodule update aufrufen, glaubt Git, dass Sie Ihr Submodul-Verzeichnis absichtlich so geändert haben, dass es auf ein »neues« Commit zeigt (während es in Wirklichkeit das alte Commit war, das Sie zuvor benutzt haben). Falls Sie dann git commit -a ausführen, ändern Sie versehentlich den Gitlink. Seien Sie vorsichtig! • Sie können einen vorhandenen Gitlink aktualisieren, indem Sie einfach die richtige Version eines Submoduls auschecken, git add im Submodul-Verzeichnis ausführen und dann git commit aufrufen. Dazu benutzen Sie nicht den Befehl git submodule. • Falls Sie einen Gitlink in Ihrem Zweig aktualisiert und bestätigt haben und einen weiteren Zweig mit git pull holen oder mit git merge zusammenführen, der denselben Gitlink anders aktualisiert, weiß Git nicht, wie es das als Konflikt darstellen soll, und sucht sich einfach eine Variante aus. Sie müssen daran denken, diese konfliktbehafteten Gitlinks selbst aufzulösen. Wie Sie sehen können, ist die Verwendung von Gitlinks und von git submodule ziemlich verzwickt. Grundsätzlich kann das Gitlink-Konzept perfekt darstellen, wie Ihre Submo-
306 | Kapitel 15: Projekte kombinieren
dule sich zu Ihrem Hauptprojekt verhalten. Diese Information dann aber tatsächlich auszunutzen, ist viel schwieriger, als es klingt. Wenn Sie darüber nachdenken, wie Sie die Submodule in Ihrem eigenen Projekt benutzen können, müssen Sie sich fragen: Sind sie die Komplexität wert? Beachten Sie, dass git submodule ein eigenständiger Befehl ist und das Pflegen der Submodule nicht einfacher macht als etwa das Schreiben Ihrer eigenen Submodul-Skripten oder das Benutzen des ext-Pakets, das am Ende des vorigen Abschnitts beschrieben wurde. Solange Sie keinen wirklichen Bedarf an der Flexibilität verspüren, die git submodule bietet, sollten Sie eine der einfacheren Methoden benutzen. Trotzdem erwarte ich, dass die Git-Entwicklergemeinde sich der Defizite und Probleme mit dem Befehl git submodule annimmt, was schließlich zu einer technisch korrekten und sehr sinnvollen Lösung führen wird.
Die native Lösung: Gitlinks und git submodule | 307
Kapitel 16
KAPITEL 16
Git mit Subversion-Repositories benutzen
Je mehr Sie sich mit Git vertraut machen, desto schwieriger werden Sie es finden, ohne dieses fähige Werkzeug auszukommen. Manchmal aber muss es ohne Git gehen – etwa, wenn Sie in einem Team arbeiten, dessen Quellcode von einem anderen Versionskontrollsystem verwaltet wird. (Subversion ist z.B. bei Open Source-Projekten sehr beliebt.) Glücklicherweise haben die Git-Entwickler zahlreiche Plugins geschaffen, mit denen sich Quellcode-Revisionen importieren und mit anderen Systemen synchronisieren lassen. In diesem Kapitel wird demonstriert, wie Sie Git benutzen, wenn der Rest Ihres Teams auf Subversion setzt. Außerdem erhalten Sie Hinweise, wie Sie vorgehen können, wenn weitere Teammitglieder zu Git wechseln wollen und wenn Ihr Team Subversion schließlich ganz hinter sich lassen will.
Beispiel: ein einfacher Klon eines einzigen Zweigs Zu Anfang wollen wir einen einfachen Klon eines einzigen Subversion-Zweigs erzeugen. Lassen Sie uns insbesondere mit dem Quellcode von Subversion selbst (der garantiert noch eine ganze Weile mit Subversion verwaltet wird) und einer speziellen Gruppe von Revisionen arbeiten, nämlich 33005 bis 33142, aus dem Zweig 1.5.x von Subversion. Der erste Schritt besteht darin, das Subversion-Repository zu klonen. $ git svn clone -r33005:33142 http://svn.collab.net/repos/svn/branches/1.5.x/ svn.git
In manchen Git-Paketen, wie denen, die von Debian- und Ubuntu-Distributionen mitgeliefert werden, ist der Befehl git svn ein optionaler Bestandteil von Git. Wenn Sie git svn eintippen, und gewarnt werden, dass »svn kein GitBefehl ist«, dann versuchen Sie, das git-svn-Paket zu installieren. (In Kapitel 2 finden Sie nähere Informationen über das Installieren der Git-Pakete.)
| 309
Der Befehl git svn clone ist geschwätziger als das typische git clone und normalerweise auch langsamer, als wenn man entweder Git oder Subversion separat ausführt.1 In diesem Beispiel jedoch ist der anfängliche Klon nicht zu langsam, weil die Menge, mit der gearbeitet wird, nur einen Bruchteil des Verlaufs eines einzelnen Zweigs darstellt. Schauen Sie in Ihr neues Git-Repository, wenn git svn clone fertig ist: $ cd svn.git $ ls ./ ../ aclocal.m4 autogen.sh* BUGS
build/ build.conf CHANGES COMMITTERS configure.ac
contrib/ COPYING doc/ gen-make.py* .git/
HACKING INSTALL Makefile.in notes/ packages/
README STATUS subversion/ tools/ TRANSLATING
win-tests.py www/
$ git branch -a * master git-svn $ git log -1 commit 05026566123844aa2d65a6896bf7c6e65fc53f7c Author: hwright Date: Wed Sep 17 17:45:15 2008 +0000 Merge r32790, r32796, r32798 from trunk: * r32790, r32796, r32798 Fix issue #2505: make switch continue after deleting locally modified directories, as it updates and merges. Notes: r32796 updates the docstring. r32798 is an obvious fix. Justification: Small fix (with test). User requests. Votes: +1: danielsh, zhakov, cmpilato git-svn-id: http://svn.collab.net/repos/svn/branches/ 1.5.x@33142 612f8ebc-c883-4be0-9ee0-a4e9ef946e3a $ git log --pretty=oneline --abbrev-commit 0502656... Merge r32790, r32796, r32798 from trunk: 77a44ab... Cast some votes, approving changes. de50536... Add r33136 to the r33137 group. 96d6de4... Recommend r33137 for backport to 1.5.x. e2d810c... * STATUS: Nominate r32771 and vote for r32968, r32975.
1
git svn ist träge, weil es nicht besonders optimiert ist. Die Subversion-Unterstützung in Git hat weniger Benutzer und Entwickler als das einfache Git oder das einfache Subversion. Außerdem muss git svn einfach mehr tun. Git lädt den Verlauf des Repository herunter, nicht nur die neueste Version, während das Subversion-Protokoll nur für das Herunterladen einer Version auf einmal optimiert ist.
310 | Kapitel 16: Git mit Subversion-Repositories benutzen
23e5373...
* subversion/po/ko.po: Korean translation updated (no fuzzy left; applied from trunk of r33034) 92902fa... * subversion/po/ko.po: Merged translation from trunk r32990 4e7f79a... Per the proposal in http://svn.haxx.se/dev/archive-2008-08/0148.shtml, Add release stream openness indications to the STATUS files on our various release branches. f9eae83... Merge r31546 from trunk:
Es gibt einiges zu sehen: • Sie können alle importierten Commits nun direkt mit Git manipulieren und den Subversion-Server ignorieren. Nur git svn-Befehle reden mit dem Server; andere GitBefehle wie git blame, git log und git diff sind so schnell wie immer und funktionieren sogar, wenn Sie nicht online sind. Diese Offline-Funktionalität ist einer der Hauptgründe dafür, dass Entwickler lieber git svn als Subversion benutzen. • Im Arbeitsverzeichnis fehlen .svn-Verzeichnisse, das vertraute .git-Verzeichnis dagegen ist vorhanden. Wenn Sie ein Subversion-Projekt auschecken, enthält normalerweise jedes Unterverzeichnis zu Verwaltungszwecken ein .svn-Verzeichnis. git svn erledigt seine Verwaltung jedoch im Gegensatz zu Git nicht im .git-Verzeichnis. Der Befehl git svn benutzt ein Extraverzeichnis namens .git/svn, das gleich beschrieben wird. • Obwohl Sie einen Zweig namens 1.5.x ausgecheckt haben, trägt der lokale Zweig den Standard-Git-Namen master. Nichtsdestotrotz entspricht er dem Zweig 1.5.x, Revision 33142. Das lokale Repository besitzt außerdem ein entferntes Ref namens git-svn, das dem lokalen master-Zweig übergeordnet ist. • Der Name und die E-Mail-Adresse des Autors in git log sind untypisch für Git. So wird der Autor z.B. als hwright anstatt mit seinem richtigen Namen, Hyrum Wright, aufgeführt. Seine E-Mail-Adresse ist ein String aus Hexzeichen. Leider speichert Subversion den vollständigen Namen eines Autors oder seine E-Mail-Adresse nicht. Stattdessen speichert es nur den Loginnamen des Autors, in diesem Fall also hwright. Da Git jedoch zusätzliche Informationen haben möchte, erzeugt git svn diese. Der String aus Hexzeichen ist die eindeutige ID des Subversion-Repository. Damit kann Git diesen speziellen Benutzer hwright auf diesem speziellen Server eindeutig identifizieren, indem es seine generierte »E-Mail-Adresse« verwendet. Wenn Sie die richtigen Namen und E-Mail-Adressen aller Entwickler in Ihrem Subversion-Projekt kennen, können Sie die Option --authors-file angeben, um eine Liste bekannter Identitäten anstelle der hergestellten Werte zu benutzen. Das ist jedoch optional und wird nur dann wichtig, wenn Ihnen die Ästhetik Ihrer Logs am Herzen liegt. Den meisten Entwicklern ist es egal. Mehr erfahren Sie, wenn Sie git help svn ausführen. Subversion und Git identifizieren ihre Benutzer auf unterschiedliche Weise. Jeder Subversion-Benutzer benötigt ein Login auf dem zentralen Repository-Server, um ein Commit ausführen zu können. Die Loginna-
Beispiel: ein einfacher Klon eines einzigen Zweigs
| 311
men müssen eindeutig sein und eignen sich deshalb für die Identifikation in Subversion. Git verlangt andererseits keinen Server. Im Fall von Git bildet die E-MailAdresse den einzigen zuverlässigen, leicht verständlichen und global eindeutigen String.
• Subversion-Benutzer schreiben üblicherweise keine einzeiligen Zusammenfassungen in den Commit-Meldungen wie Git-Benutzer, sodass das »Einzeilen«-Format von git log ziemlich hässliche Ergebnisse erzeugt. Dagegen kann man nicht viel tun. Sie können lediglich Ihre Subversion-Kollegen bitten, freiwillig die einzeiligen Zusammenfassungen zu übernehmen. Schließlich ist eine einzeilige Zusammenfassung in jedem Versionskontrollsystem hilfreich. • Es gibt in jeder Commit-Meldung eine zusätzliche Zeile mit dem Präfix git-svn-id. git svn nutzt diese Zeile, um den Überblick zu behalten, woher das Commit kam. In diesem Fall stammte das Commit von http://svn.collab.net/repos/svn/branches/1.5.x, ab Revision 33142, und die eindeutige Server-ID ist identisch mit derjenigen, mit der Hyrums falsche E-Mail-Adresse generiert wurde. • git svn erzeugte für jedes Commit eine neue Commit-ID-Nummer (0502656...). Wenn Sie exakt die gleichen Git-Software- und Kommandozeilenoptionen verwendet haben wie hier, sollten die Commit-Nummern auf Ihrem lokalen System genauso aussehen. Schließlich sind Ihre lokalen Commits die gleichen Commits aus dem gleichen entfernten Repository. Dieses Detail ist in bestimmten git svn-Workflows wichtig, wie Sie gleich sehen werden. Es ist außerdem nicht sehr stabil. Wenn Sie andere git svn clone-Optionen benutzen, selbst wenn Sie eine andere Revisionssequenz klonen, ändern sich alle Ihre Commit-IDs.
Änderungen in Git vornehmen Wenn Sie ein Git-Repository mit Subversion-Quellcode hergestellt haben, müssen Sie als Nächstes eine Änderung vornehmen: $ echo 'Ich bin jetzt ein Subversion-Entwickler!' >hello.txt $ git add hello.txt $ git commit -m 'Mein erstes Subversion-Commit'
Herzlichen Glückwunsch, Sie haben Ihre erste Änderung in den Subversion-Quellcode eingebracht! Nun ja, eigentlich nicht. Sie haben Ihre erste Änderung im Subversion-Quellcode mit einem Commit bestätigt. Beim normalen Subversion, bei dem jedes Commit im zentralen Repository gespeichert wird, ist das Bestätigen einer Änderung identisch mit dem Freigeben dieser Änderung für andere. Bei Git dagegen ist ein Commit nur ein Objekt in Ihrem
312 | Kapitel 16: Git mit Subversion-Repositories benutzen
lokalen Repository, bis Sie die Änderung an eine andere Person verschieben. Und git svn ändert dieses Verhalten nicht. Und, ach, wenn Sie Ihre Änderungen zurückeinbringen wollen, funktioniert die normale Git-Operation nicht: $ git push origin master fatal: 'origin': unable to chdir or not a git archive fatal: The remote end hung up unexpectedly
Mit anderen Worten: »Sie haben kein Git-Remote namens origin erzeugt, sodass der Befehl sinnlos ist.« (Mehr über das Definieren von Remotes erfahren Sie in Kapitel 11.) Um genau zu sein, wird dieses Problem von einem Git-Remote auch gar nicht gelöst. Falls Sie wieder zurück zu Subversion bestätigen wollen, müssen Sie git svn dcommit benutzen.2 $ git svn dcommit Committing to http://svn.collab.net/repos/svn/branches/1.5.x ... Authentication realm: Subversion Committers Password for 'bob':
Wenn Sie tatsächlich Zugriff auf das zentrale Subversion-Quellcode-Repository haben (nur wenige Leute auf der Welt genießen dieses Privileg), würden Sie am Prompt Ihr Passwort eingeben, und git svn würde loszaubern. Dann würde sich aber alles immer weiter verwirren, weil Sie versuchen, eine Revision zu bestätigen, die nicht die allerneueste ist. Untersuchen wir einmal, was daher als Nächstes zu tun ist.
Abrufen vor dem Bestätigen Erinnern Sie sich, dass Subversion eine lineare, sequenzielle Ansicht des Verlaufs vorhält. Falls Ihre lokale Kopie eine ältere Version aus dem Subversion-Repository hat (und die hat sie) und Sie ein Commit in dieser älteren Version durchgeführt haben (und das haben Sie), gibt es keine Möglichkeit, das zurück an den Server zu schicken. Subversion kennt nun einmal keine Methode, um einen neuen Zweig an einem früheren Punkt im Verlauf eines Projekts zu erzeugen. Sie haben jedoch ein Fork im Verlauf erzeugt, wie bei einem Git-Commit immer üblich. Es gibt nun zwei Möglichkeiten: • Der Verlauf wurde absichtlich aufgespalten. Sie wollen beide Teile des Verlaufs bewahren, sie in einem Merge zusammenführen und den Merge in Subversion bestätigen. 2
Wieso »dcommit« anstatt »commit«? Der ursprüngliche git svn commit-Befehl war destruktiv und schlecht gestaltet und sollte vermieden werden. Anstatt jedoch die Abwärtskompatibilität an dieser Stelle aufzugeben, beschlossen die git svn-Entwickler, einen neuen Befehl hinzuzufügen, nämlich dcommit. Der alte commit-Befehl ist nun besser bekannt als set-tree – benutzen Sie ihn aber trotzdem nicht.
Beispiel: ein einfacher Klon eines einzigen Zweigs
| 313
• Der Fork geschah nicht absichtlich. In diesem Fall wäre es besser, ihn zu linearisieren und dann zu bestätigen. Klingt das vertraut? Es ist vergleichbar der Wahl zwischen einem Merge und einem Rebase, wie in »rebase und merge im Vergleich« auf Seite 187 besprochen wurde. Die erste Option entspricht git merge, während die zweite git rebase ähnelt. Das Gute ist hier wieder, dass Git beide Möglichkeiten bietet. Das Schlechte ist, dass Subversion einen Teil Ihres Verlaufs verliert, egal für welche Option Sie sich entscheiden. Um fortzufahren, holen Sie die neuesten Revisionen von Subversion:3 $ git svn fetch M STATUS M build.conf M COMMITTERS r33143 = 152840fb7ec59d642362b2de5d8f98ba87d58a87 (git-svn) M STATUS r33193 = 13fc53806d777e3035f26ff5d1eedd5d1b157317 (git-svn) M STATUS r33194 = d70041fd576337b1d0e605d7f4eb2feb8ce08f86 (git-svn)
Sie können die gezeigten Logmeldungen folgendermaßen interpretieren: • M bedeutet, dass eine Datei modifiziert wurde. • r33143 ist die Subversion-Revisionsnummer einer Änderung. • 152840f... ist die dazugehörende Git-Commit-ID, die von git svn generiert wurde. • git-svn ist der Name des entfernten Ref, das mit dem neuen Commit aktualisiert wurde. Schauen wir, was weiter passiert: $ git log --pretty=oneline --abbrev-commit --left-right master...git-svn <2e5f71c... Mein erstes Subversion-Commit >d70041f... * STATUS: Added note to r33173. >13fc538... * STATUS: Nominate r33173 for backport. >152840f... Merge r31203 from trunk:
Ganz einfach gesagt: Der »linke« Zweig (master) hat ein neues Commit, und der »rechte« Zweig (git-svn) drei. (Wahrscheinlich sehen Sie eine andere Ausgabe, wenn Sie den Befehl ausführen, weil diese Ausgabe während der Produktion dieses Buches aufgenommen wurde.) Die Option --left-right und der symmetrische Differenzoperator (...) werden in »git log mit Konflikten« auf Seite 143 bzw. »Commit-Bereiche« auf Seite 85 besprochen.
3
Ihrem lokalen Repository fehlen auf jeden Fall Revisionen, da nur eine Teilmenge aller Revisionen am Anfang geklont wurde. Wahrscheinlich sehen Sie mehr Revisionen, als hier gezeigt werden, weil die Subversion-Entwickler immer noch am Zweig 1.5.x arbeiten.
314 | Kapitel 16: Git mit Subversion-Repositories benutzen
Bevor Sie wieder zurück zu Subversion bestätigen können, brauchen Sie einen Zweig mit allen Commits an einer Stelle. Darüber hinaus müssen alle neuen Commits relativ zum aktuellen Zustand des git-svn-Zweigs sein, weil Subversion es nicht anders kennt.
Durch git svn rebase bestätigen Am einfachsten fügen Sie Ihre Änderungen hinzu, indem Sie sie mit einem Rebase auf den git-svn-Zweig verlagern: $ git checkout master # Rebase des aktuellen master-Zweigs auf dem Upstream-Zweig git-svn $ git rebase git-svn First, rewinding head to replay your work on top of it... Applying: Mein erstes Subversion-Commit $ git log --pretty=oneline --abbrev-commit --left-right master...git-svn <0c4c620... Mein erstes Subversion-Commit
Eine Kurzform für git svn fetch, gefolgt von git rebase git-svn ist git svn rebase. Dieser Befehl nimmt automatisch an, dass Ihr Zweig auf dem einen Zweig namens git-svn basiert, ruft diesen von Subversion ab und verlagert Ihren Zweig darauf. Wenn außerdem git svn dcommit bemerkt, dass Ihr Subversion-Zweig nicht mehr aktuell ist, gibt es nicht einfach auf, sondern ruft automatisch zuerst git svn rebase auf. Falls Sie immer ein Rebase anstelle eines Merge ausführen wollen, sparen Sie mit git svn rebase eine Menge Zeit. Falls Sie jedoch nicht standardmäßig den Verlauf umschreiben wollen, müssen Sie aufpassen, dass Sie nicht dcommit ausführen, bevor Sie manuell git svn fetch und git merge erledigt haben.
Wenn Sie Git lediglich als bequeme Methode ansehen, um auf Ihren Subversion-Verlauf zuzugreifen, ist das Verlagern mit einem Rebase in Ordnung – so wie git rebase absolut geeignet ist, um eine Menge von Patches neu anzuordnen, an denen Sie arbeiten –, solange Sie diese Patches nie an jemand anderen verschoben haben. Allerdings bringt das Rebasing mit git svn die gleichen Nachteile mit sich wie das Rebasing im Allgemeinen. Falls Sie Ihre Patches mit einem Rebase verlagern, bevor Sie sie in Subversion bestätigen, müssen Sie an Folgendes denken: • Erzeugen Sie keine lokalen Zweige und führen sie mit git merge zusammen. Wie in »rebase und merge im Vergleich« auf Seite 187 erwähnt, gerät git merge durch ein Rebase durcheinander. Beim einfachen Git können Sie beschließen, keinen Zweig umzulagern, auf dem ein anderer Zweig basiert. Mit git svn haben Sie diese Wahl nicht. Alle Ihre Zweige basieren auf dem Zweig git-svn, und das ist der Zweig, auf dem alle anderen Zweige basieren müssen.
Beispiel: ein einfacher Klon eines einzigen Zweigs
| 315
• Erlauben Sie niemandem, Ihr Repository zu klonen oder etwas daraus zu ziehen; stattdessen sollten andere Leute mit git svn ihr eigenes Git-Repository erzeugen. Weil das Ziehen eines Repository in ein anderes immer einen Merge verursacht, funktioniert es nicht, und zwar aus dem gleichen Grund, aus dem git merge nicht funktioniert, wenn Sie Ihr Repository umgelagert haben. • Führen Sie häufig Rebases und dcommit durch. Denken Sie daran: Ein SubversionBenutzer führt jedesmal, wenn er ein Commit durchführt, das Äquivalent eines git push durch. Das ist immer noch die beste Methode, um alles unter Kontrolle zu behalten, wenn Ihr Verlauf linear bleiben muss. • Vergessen Sie nicht, dass die Zwischenversionen, die von den Patches erzeugt wurden, niemals existiert haben und niemals wirklich getestet wurden, wenn Sie eine Reihe von Patches auf einen anderen Zweig verlagern. Im Prinzip schreiben Sie den Verlauf um, und das ist auch schon alles. Falls Sie später git bisect oder git blame (oder svn blame in Subversion) benutzen, um festzustellen, wann ein Problem entstanden ist, haben Sie keinen wahrheitsgetreuen Einblick in das, was geschehen ist. Lassen diese Warnungen git svn rebase gefährlich klingen? Gut. Jede Variation von git rebase ist heimtückisch. Falls Sie jedoch die Regeln befolgen und nichts Ausgefallenes probieren, geht schon alles in Ordnung. Jetzt wollen wir aber etwas Ausgefallenes versuchen.
Verschieben, Ziehen, Verzweigen und Zusammenführen mit git svn Ständiges Umlagern ist in Ordnung, wenn Sie Git einfach als einen besseren SubversionRepository-Spiegel benutzen wollen. Selbst das ist für sich genommen ein großer Schritt vorwärts: Sie können offline arbeiten, erhalten schnellere log-, blame- und diff-Operationen und ärgern diejenigen Mitarbeiter nicht, die mit Subversion absolut zufrieden sind. Es muss nicht einmal jemand wissen, dass Sie Git benutzen. Aber was ist, wenn Sie mehr wollen? Vielleicht möchte einer Ihrer Mitarbeiter mit Ihnen an einer neuen Funktion arbeiten und dabei Git benutzen. Oder Sie wollen an einigen Topic-Zweigen arbeiten und halten sich mit dem Bestätigen in Subversion zurück, bis Sie sicher sind, dass sie fertig sind. Oder vielleicht nerven Sie ja auch die Merging-Funktionen von Subversion, und Sie wollen lieber auf die ausgereifteren Fähigkeiten von Git zurückgreifen. Wenn Sie git svn rebase benutzen, können Sie nichts davon wirklich tun. Falls Sie allerdings das Umlagern mit einem Rebase vermeiden, erlaubt Ihnen git svn alle diese Aktionen. Es gibt nur einen Haken: Ihr ausgefallener, nichtlinearer Verlauf wird es nie nach Subversion schaffen. Ihre Subversion benutzenden Mitarbeiter sehen die Ergebnisse Ihrer
316 | Kapitel 16: Git mit Subversion-Repositories benutzen
schweren Arbeit lediglich in Form eines gelegentlichen zusammengequetschten MergeCommit (siehe »Squash-Merges« auf Seite 160), allerdings bleibt ihnen verborgen, wie Sie zu diesem Resultat gelangt sind. Falls das ein Problem darstellt, sollten Sie wahrscheinlich den Rest dieses Kapitels überspringen. Wenn es Ihren Mitarbeitern jedoch egal ist – die meisten Entwickler schauen sich die Verläufe anderer Entwickler sowieso nicht an – oder Sie es verwenden wollen, um Ihren Mitarbeitern Git schmackhaft zu machen, dann lernen Sie im Folgenden eine viel bessere Methode kennen, git svn einzusetzen.
Schützen Sie Ihre Commit-IDs Aus Kapitel 10 wissen Sie, dass ein Rebase störend ist, weil dabei völlig neue Commits generiert werden, die die gleichen Änderungen repräsentieren. Die neuen Commits tragen neue Commit-IDs. Wenn Sie dann einen Zweig mit einem der neuen Commits mit einem anderen Zweig zusammenführen, der eines der alten Commits enthält, kann Git nicht wissen, dass Sie dieselbe Änderung zweimal anwenden. Das Ergebnis sind doppelte Einträge in git log und manchmal ein Merge-Konflikt. Beim einfachen Git ist es leicht, solche Situationen zu vermeiden: Vermeiden Sie git cherry-pick und git rebase, und diese Probleme treten überhaupt nicht auf. Oder verwenden Sie die Befehle so sorgfältig, dass es nur in kontrollierten Situationen zu Problemen kommt. Mit git svn gibt es jedoch eine weitere potenzielle Quelle von Problemen, und die lässt sich nicht so einfach umgehen. Das Problem besteht darin, dass die Git-CommitObjekte, die von Ihrem git svn erzeugt wurden, nicht immer die gleichen sind wie diejenigen, die vom git svn anderer Leute erzeugt wurden. Und man kann nichts dagegen tun: • Wenn Sie eine andere Git-Version haben als jemand anders, könnte Ihr git svn andere Commits generieren als das Ihres Mitarbeiters. (Die Git-Entwickler versuchen, das zu vermeiden, es kann aber passieren.) • Falls Sie die Option --authors-file benutzen, um die Autorennamen zu wechseln, oder verschiedene andere git svn-Optionen anwenden, die sein Verhalten ändern, werden alle Commit-IDs unterschiedlich sein. • Falls Sie einen Subversion-URI benutzen, der sich von jemandem unterscheidet, der im Subversion-Repository arbeitet (z.B. wenn Sie anonym auf ein Subversion-Repository zugreifen, während jemand anders eine authentifizierte Methode benutzt, um auf dasselbe Repository zuzugreifen), werden Ihre git-svn-id-Zeilen anders sein. Das ändert wiederum die Commit-Nachricht, was den SHA1-Wert des Commit ändert, was letztendlich die Commit-ID ändert. • Falls Sie nur eine Teilmenge der Subversion-Revisionen abrufen, indem Sie die Option -r für git svn clone einsetzen (wie im ersten Beispiel in diesem Kapitel), und
Verschieben, Ziehen, Verzweigen und Zusammenführen mit git svn
| 317
jemand anders eine andere Teilmenge holt, ist der Verlauf unterschiedlich, und auch die Commit-IDs werden anders aussehen. • Falls Sie git merge benutzen und dann die Ergebnisse mit git svn dcommit bestätigen, sieht das neue Commit für Sie anders aus als das gleiche Commit, das andere Leute über git svn fetch beziehen, weil nur Ihre Kopie von git svn den wahren Verlauf dieses Commit kennt. (Denken Sie daran, dass die Informationen über den Verlauf auf ihrem Weg zu Subversion verloren gegangen sind, sodass selbst Git-Benutzer, die dieses Commit aus Subversion abrufen, diesen Verlauf nicht abrufen können.) Angesichts all dieser Warnungen scheint es fast ein Ding der Unmöglichkeit zu sein, git svn-Benutzer zu koordinieren. Es gibt allerdings einen einfachen Trick, den Sie einsetzen können, um all diese Probleme zu vermeiden: Sorgen Sie dafür, dass es nur ein Git-Repository, den »Pförtner« gibt, das jemals git svn fetch oder git svn dcommit benutzt. Es bringt mehrere Vorteile, diesen Trick zu verwenden: • Da nur ein Repository eine Verbindung mit Subversion besitzt, gibt es niemals ein Problem mit inkompatiblen Commit-IDs, weil jedes Commit nur einmal erzeugt wird. • Ihre Git benutzenden Mitarbeiter müssen niemals lernen, wie man git svn verwendet. • Da alle Git-Benutzer nur einfaches Git verwenden, können Sie bei ihrer Zusammenarbeit die normalen Git-Abläufe nutzen, ohne sich jemals Gedanken über Subversion machen zu müssen. • Es ist schneller, einen neuen Benutzer von Subversion zu Git zu konvertieren, weil eine git clone-Operation viel schneller verläuft als das sequenzielle Herunterladen jeder einzelnen Revision aus Subversion. • Falls Ihr Team schließlich zu Git wechselt, können Sie eines Tages einfach den Subversion-Server ausschalten. Niemand wird den Unterschied bemerken. Es gibt aber auch einen riesigen Nachteil: • Sie erhalten eine Engstelle zwischen der Git- und der Subversion-Welt. Alles muss durch ein einzelnes Git-Repository laufen, das wahrscheinlich nur von wenigen Leuten betreut wird. Vergleicht man ein vollständig verteiltes Git-System mit dem Erfordernis eines zentral verwalteten git svn-Repository, dann mag das auf den ersten Blick wie ein Schritt zurück wirken. Sie haben aber bereits ein zentrales Subversion-Repository, sodass dieses Vorgehen die Lage nicht verschlechtert. Schauen wir uns an, wie dieses zentrale Pförtner-Repository eingerichtet wird.
318 | Kapitel 16: Git mit Subversion-Repositories benutzen
Alle Zweige klonen Als Sie früher ein persönliches git svn-Repository einrichteten, klonte die Prozedur nur einige Revisionen aus einem einzigen Zweig. Das reicht für eine Person völlig aus, die einfach nur offline arbeiten möchte. Wenn jedoch ein ganzes Team auf ein Repository zugreifen soll, können Sie keine Annahmen darüber treffen, welche Teile benötigt werden und welche nicht. Sie brauchen alle Zweige, alle Tags und alle Revisionen aus allen Zweigen. Da diese Anforderung nicht unüblich ist, besitzt Git eine Option, mit der sich ein vollständiger Klon durchführen lässt. Wir wollen wieder den Subversion-Quellcode klonen, dieses Mal jedoch mit allen Zweigen: $ git svn clone --stdlayout --prefix=svn/ -r33005:33142 \ http://svn.collab.net/repos/svn svn-all.git
Am besten erzeugt man ein Pförtner-Repository, indem man die Option -r komplett weglässt. In diesem Fall würde es allerdings Stunden, vielleicht sogar Tage dauern, bis alles fertig ist. Momentan enthält der SubversionQuellcode Zehntausende von Revisionen, und git svn müsste sie alle einzeln aus dem Internet herunterladen. Wenn Sie das Beispiel nachvollziehen wollen, dann lassen Sie die Option -r stehen. Sollten Sie dagegen ein GitRepository für Ihr eigenes Subversion-Projekt einrichten, dann lassen Sie sie weg.
Beachten Sie die neuen Optionen: • --stdlayout teilt git svn mit, dass die Repository-Zweige auf normale SubversionWeise eingerichtet sind, bei der die Unterverzeichnisse /trunk, /branches bzw. /tags der Hauptentwicklungslinie, den Zweigen und Tags entsprechen. Wenn Ihr Repository anders aufgebaut ist, können Sie stattdessen die Optionen --trunk, --branches und --tags probieren oder .git/config bearbeiten, um die Option refspec von Hand einzurichten. Tippen Sie git help svn ein, um weitere Informationen zu erhalten. • --prefix=svn/ erzeugt alle entfernten Refs mit dem Präfix svn/, sodass Sie die einzelnen Zweige als svn/trunk und svn/1.5.x ansprechen können. Ohne diese Option hätten Ihre entfernten Subversion-Refs überhaupt kein Präfix, wodurch man sie leicht mit den lokalen Zweigen verwechseln könnte. git svn sollte eine Weile zu tun haben. Wenn alles vorbei ist, sehen die Ergebnisse so aus: $ cd svn-all.git $ git branch -a -v | cut -c1-60 * master 0502656 Merge r32790, r32796, r32798 svn/1.0.x 19e69aa Merge the 1.0.x-issue-2751 br svn/1.1.x e20a6ce Per the proposal in http://sv svn/1.2.x 70a5c8a Per the proposal in http://sv svn/1.3.x 32f8c36 * STATUS: Leave a breadcrumb svn/1.4.x 23ecb32 Per the proposal in http://sv svn/1.5.x 0502656 Merge r32790, r32796, r32798
Verschieben, Ziehen, Verzweigen und Zusammenführen mit git svn
| 319
svn/1.5.x-issue2489 svn/explore-wc svn/file-externals svn/ignore-mergeinfo svn/ignore-prop-mods svn/svnpatch-diff svn/tree-conflicts svn/trunk
2bbe257 798f467 4c6e642 e3d51f1 7790729 918b5ba 79f44eb ae47f26
On the On the On the On the On the On the On the Remove
1.5.x-issue2489 branch explore-wg branch: file externals branch. ignore-mergeinfo branc ignore-prop-mods branc 'svnpatch-diff' branch tree-conflicts branch, YADFC (yet another dep
Der lokale master-Zweig wurde automatisch erzeugt, sieht aber nicht so aus, wie Sie erwartet haben – er zeigt auf das gleiche Commit wie der svn/1.5.x-Zweig, nicht wie der svn/trunk-Zweig. Wieso? Das neueste Commit in dem Bereich, das mit -r angegeben wurde, gehörte zum svn/1.5.x-Zweig. (Rechnen Sie jedoch nicht mit diesem Verhalten; es wird sich wahrscheinlich in einer künftigen Version von git svn ändern.) Wir wollen den Zweig so ändern, dass er auf den Trunk verweist: $ git reset --hard svn/trunk HEAD is now at ae47f26 Remove YADFC (yet another deprecated function call). $ git branch -a -v | cut -c1-60 * master ae47f26 Remove YADFC (yet another dep svn/1.0.x 19e69aa Merge the 1.0.x-issue-2751 br svn/1.1.x e20a6ce Per the proposal in http://sv svn/1.2.x 70a5c8a Per the proposal in http://sv svn/1.3.x 32f8c36 * STATUS: Leave a breadcrumb svn/1.4.x 23ecb32 Per the proposal in http://sv svn/1.5.x 0502656 Merge r32790, r32796, r32798 svn/1.5.x-issue2489 2bbe257 On the 1.5.x-issue2489 branch svn/explore-wc 798f467 On the explore-wg branch: svn/file-externals 4c6e642 On the file externals branch. svn/ignore-mergeinfo e3d51f1 On the ignore-mergeinfo branc svn/ignore-prop-mods 7790729 On the ignore-prop-mods branc svn/svnpatch-diff 918b5ba On the 'svnpatch-diff' branch svn/tree-conflicts 79f44eb On the tree-conflicts branch, svn/trunk ae47f26 Remove YADFC (yet another dep
Ihr Repository freigeben Nachdem Sie Ihr komplettes git svn-Pförtner-Repository aus Subversion importiert haben, müssen Sie es veröffentlichen. Das tun Sie auf die gleiche Weise wie bei einem Bare-Repository (siehe Kapitel 11), allerdings mit einem Trick: Die Subversion-»Zweige«, die git svn erzeugt, sind eigentlich entfernte Refs, keine Zweige. Die normale Technik funktioniert nicht ganz: $ cd .. $ mkdir svn-bare.git $ cd svn-bare.git $ git init --bare
320 | Kapitel 16: Git mit Subversion-Repositories benutzen
Initialized empty Git repository in /tmp/svn-bare/ $ cd .. $ cd svn-all.git $ git push --all ../svn-bare.git Counting objects: 2331, done. Compressing objects: 100% (1684/1684), done. Writing objects: 100% (2331/2331), 7.05 MiB | 7536 KiB/s, done. Total 2331 (delta 827), reused 1656 (delta 616) To ../svn-bare * [new branch] master -> master
Sie haben es fast geschafft. Mit git push haben Sie den master-Zweig kopiert, aber keinen der svn/-Zweige. Damit alles richtig funktioniert, ändern Sie den git push-Befehl, indem Sie ihm explizit sagen, dass er diese Zweige kopieren soll: $ git push ../svn-bare.git 'refs/remotes/svn/*:refs/heads/svn/*' Counting objects: 6423, done. Compressing objects: 100% (1559/1559), done. Writing objects: 100% (5377/5377), 8.01 MiB, done. Total 5377 (delta 3856), reused 5167 (delta 3697) To ../svn-bare * [new branch] svn/1.0.x -> svn/1.0.x * [new branch] svn/1.1.x -> svn/1.1.x * [new branch] svn/1.2.x -> svn/1.2.x * [new branch] svn/1.3.x -> svn/1.3.x * [new branch] svn/1.4.x -> svn/1.4.x * [new branch] svn/1.5.x -> svn/1.5.x * [new branch] svn/1.5.x-issue2489 -> svn/1.5.x-issue2489 * [new branch] svn/explore-wc -> svn/explore-wc * [new branch] svn/file-externals -> svn/file-externals * [new branch] svn/ignore-mergeinfo -> svn/ignore-mergeinfo * [new branch] svn/ignore-prop-mods -> svn/ignore-prop-mods * [new branch] svn/svnpatch-diff -> svn/svnpatch-diff * [new branch] svn/tree-conflicts -> svn/tree-conflicts * [new branch] svn/trunk -> svn/trunk
Das nimmt die svn/-Refs, die als entfernte Zweige betrachtet werden, aus dem lokalen Repository und kopiert sie in das entfernte Repository, wo sie als Köpfe (d. h. lokale Zweige) angesehen werden.4 Sobald das verbesserte git push fertig ist, ist Ihr Repository bereit: Sagen Sie Ihren Mitarbeitern, dass sie loslegen und Ihr svn-bare.git-Repository klonen sollen. Sie können dann ohne Probleme Daten in das Repository schieben und aus ihm ziehen und Zweige anlegen und zusammenführen.
4
Sie haben recht, wenn Ihnen das verworren vorkommt. Schließlich könnte git svn eine Methode anbieten, um einfach lokale Zweige anstelle von entfernten Refs zu erzeugen, damit git push --all so funktioniert, wie man das erwartet.
Verschieben, Ziehen, Verzweigen und Zusammenführen mit git svn
| 321
Änderungen zurück in Subversion überführen Irgendwann werden Sie und Ihr Team Änderungen von Git zurück nach Subversion verschieben wollen. Wie gehabt benutzen Sie dazu den Befehl git svn dcommit, allerdings brauchen Sie davor kein Rebase. Stattdessen können Sie die Änderungen zuerst mit git merge oder git pull in einen Zweig der svn/-Hierarchie bringen und müssen dann nur das eine, neu zusammengeführte Commit bestätigen. Nehmen Sie z.B. an, dass Ihre Änderungen sich in einem Zweig namens new-feature befinden und Sie ihn mit dcommit in svn/trunk bestätigen wollen. So geht’s: $ git checkout svn/trunk Note: moving to "svn/trunk" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b HEAD is now at ae47f26... Remove YADFC (yet another deprecated function call). $ git merge --no-ff new-feature Merge made by recursive. hello.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hello.txt $ git svn dcommit
Dreierlei überrascht uns hier: • Anstatt Ihren lokalen Zweig, new-feature, auszuchecken und in svn/trunk zu überführen, müssen Sie andersherum vorgehen. Normalerweise funktioniert das Merging gut in beide Richtungen, allerdings funktioniert git svn nicht, wenn Sie es andersherum machen. • Sie führen den Merge mithilfe der Option --no-ff durch, was sicherstellt, dass es immer ein Merge-Commit gibt, auch wenn ein Merge-Commit manchmal unnötig erscheint. • Sie führen die ganze Operation auf einem nicht angeschlossenen HEAD durch, was gefährlich klingt. Sie müssen diese drei überraschenden Dinge unbedingt tun, da die Operation sonst nicht zuverlässig durchgeführt wird.
Wie dcommit Merges verarbeitet Um zu verstehen, weshalb man das dcommit auf so seltsame Weise durchführen muss, sollten Sie sich überlegen, wie dcommit funktioniert. Zuerst ermittelt dcommit den Subversion-Zweig, in den mit dem Commit bestätigt werden soll, indem es sich die git-svn-id der Commits im Verlauf anschaut.
322 | Kapitel 16: Git mit Subversion-Repositories benutzen
Falls Sie sich Sorgen machen, welchen Zweig dcommit auswählen wird, können Sie mit git svn dcommit -n einen harmlosen Testlauf durchführen.
Falls Ihr Team ausgefallene Sachen gemacht hat (worum es schließlich in diesem Abschnitt geht), könnte es Merges und ausgewählte Patches auf Ihrem new-featureZweig geben. Einige dieser Merges könnten git-svn-id-Zeilen aus anderen Zweigen haben als dem, in den Sie mit dem Commit bestätigen wollen. Um diese Unklarheit zu beseitigen, schaut git svn sich nur die linke Seite eines Merge an – wie es auch git log --first-parent tut. Deshalb funktioniert das Zusammenführen von svn/trunk in new-feature nicht: svn/trunk würde auf der rechten, nicht auf der linken Seite landen, und git svn würde es nicht sehen. Schlimmer noch: Es würde denken, dass Ihr Zweig auf einer älteren Version des Subversion-Zweigs beruht, und würde automatisch versuchen, ihn mit git svn rebase für Sie umzulagern, was ein absolutes Chaos anrichten würde. Das erklärt auch, weshalb --no-ff notwendig ist. Wenn Sie den Zweig new-feature auschecken und git merge svn/trunk ausführen, den Zweig svn/trunk auschecken und git merge new-feature ohne die Option --no-ff aufrufen, führt Git anstelle eines Merge ein Fast-forward (Vorspulen auf dem Zweig) durch. Das ist effizient, führt aber ebenfalls dazu, dass svn/trunk auf der rechten Seite landet und das genannte Problem auftritt. Nachdem git svn dcommit das alles schließlich festgestellt hat, muss es in Subversion ein neues Commit erzeugen, das Ihrem Merge-Commit entspricht. Wenn das erledigt ist, muss es zur Commit-Meldung eine git-svn-id-Zeile hinzufügen, was bedeutet, dass sich die Commit-ID ändert, sodass es nicht mehr dasselbe Commit ist. Das neue Merge-Commit landet im echten svn/trunk-Zweig, und das Merge-Commit, das Sie früher auf dem abgesonderten HEAD erzeugt hatten, ist jetzt redundant. Um genau zu sein, ist es schlimmer als redundant: Wenn man es für irgendetwas benutzen würde, bekäme man anschließend Konflikte. Vergessen Sie dieses Commit deshalb einfach. Wenn Sie es noch nicht in irgendeinen Zweig gelegt haben, geht das Vergessen natürlich auch viel leichter.
Verschiedene Hinweise zum Arbeiten mit Subversion Es gibt noch einiges, was Sie wissen sollten, wenn Sie git svn benutzen.
svn:ignore im Vergleich mit .gitignore In jedem Versionskontrollsystem muss es möglich sein, Dateien anzugeben, die das System ignorieren soll, etwa Backup-Dateien, kompilierte Programmdateien usw.
Verschiedene Hinweise zum Arbeiten mit Subversion
| 323
In Subversion setzt man dazu die Eigenschaft svn:ignore in einem Verzeichnis. In Git erzeugt man eine Datei namens .gitignore, wie in »Die Datei .gitignoreT« auf Seite 63 erläutert wurde. Bequemerweise bietet git svn eine einfache Möglichkeit, um svn:ignore auf .gitignore abzubilden. Es gibt dabei zwei Ansätze: • git svn create-ignore erzeugt automatisch .gitignore-Dateien, die den svn:ignoreEigenschaften entsprechen. Man kann sie dann bestätigen, wenn man möchte. • git svn show-ignore sucht im ganzen Projekt nach allen svn:ignore-Eigenschaften und gibt die gesamte Liste aus. Man kann die Ausgabe des Befehls aufzeichnen und in die .git/info/exclude-Datei setzen. Welche Technik Sie wählen, hängt davon ab, wie sehr Sie sich bei der Benutzung von git svn bedeckt halten. Falls Sie die .gitignore-Dateien nicht mit einem Commit in Ihr Repository aufnehmen wollen – wodurch sie für Ihre Subversion-benutzenden Mitarbeiter sichtbar werden –, benutzen Sie die exclude-Datei. Ansonsten ist .gitignore normalerweise die Methode der Wahl, weil diese Dateien automatisch von jedem benutzt werden können, der in diesem Projekt Git einsetzt.
Den git-svn-Cache rekonstruieren Der Befehl git svn speichert zusätzliche Verwaltungsinformationen im Verzeichnis .git/ svn. Diese Informationen werden z.B. benutzt, um schnell festzustellen, ob eine bestimmte Subversion-Revision bereits heruntergeladen wurde und deshalb nicht mehr heruntergeladen werden muss. Sie enthalten außerdem die gleichen git-svn-id-Informationen, die in importierten Commit-Meldungen zu finden sind. Wenn das der Fall ist, wieso existieren die git-svn-id-Zeilen dann überhaupt? Nun, da die Zeilen zum Commit-Objekt hinzugefügt werden und der Inhalt eines CommitObjekts seine ID bestimmt, folgt daraus, dass sich die Commit-ID ändert, wenn das Commit-Objekt durch git svn dcommit geschickt wird – und das Ändern der Commit-IDs kann künftige Git-Merges erschweren, wenn man nicht peinlich genau den beschriebenen Schritten folgt. Wenn aber Git einfach die git-svn-id-Zeilen wegließe, müssten sich die Commit-IDs nicht ändern und git svn würde prima funktionieren. Richtig? Ja, bis auf ein wichtiges Detail. Das Verzeichnis .git/svn wird nicht zusammen mit Ihrem Git-Repository geklont: Ein wichtiger Bestandteil der Git-Sicherheitspolitik ist, dass nur Blob-, Baum- und Commit-Objekte freigegeben werden. Daher müssen die git-svn-idZeilen Teil eines Commit-Objekts sein, und jeder mit einem Klon Ihres Repository erhält alle Informationen, die er benötigt, um das .git/svn-Verzeichnis zu rekonstruieren. Das hat zwei Vorteile: • Wenn Sie versehentlich Ihr Pförtner-Repository verlieren oder etwas zerstören, oder wenn Sie verschwinden und niemand mehr da ist, um Ihr Repository zu pflegen, kann jeder, der einen Klon Ihres Repository besitzt, ein neues einrichten.
324 | Kapitel 16: Git mit Subversion-Repositories benutzen
• Wenn git-svn einen Bug enthält und sein .git/svn-Verzeichnis zerstört, können Sie es jederzeit regenerieren. Sie können versuchen, die Cache-Informationen zu regenerieren, indem Sie das Verzeichnis .git/svn an eine andere Stelle verschieben. Probieren Sie das hier: $ cd svn-all.git $ mv .git/svn /tmp/git-svn-backup $ git svn fetch -r33005:33142
Hier regeneriert git svn seinen Cache und ruft die angeforderten Objekte ab. (Wie gehabt würden Sie normalerweise die Option -r ausgeschaltet lassen. Das könnte jedoch dazu führen, dass Sie Tausende von Commits herunterladen müssten, weshalb wir die Option in diesem Beispiel aktivieren.)
Verschiedene Hinweise zum Arbeiten mit Subversion
| 325
Index
Symbole # (Hash-Zeichen) Kommentar 63, 138, 185 $GIT_DIR-Variable 19, 75 * (Asterisk) Globbing 64 show-branch 103 + (Pluszeichen) Diff 115 Merges 141 Refspec 200 show-branch 104 - (Minuszeichen) 21 Diff 115 -- (doppelter Bindestrich) 21 . (Punkt) 22, 99 .. (doppelte Punkte) 85, 124, 260 … (dreifache Punkte) 89, 126 / (Schrägstrich) 64, 99 > (rechts) 144 ^ (Zirkumflex) 76 ~ (Tilde) 76
Numerisch -3- oder --3way-Optionen (git am-Befehl) 271
A Abbildung 69 Abbrechen Merges 148 --abbrev-commit-Option (git log-Befehl) 79
abgesonderter HEAD 323 bisect-Modus 91 Zweige 111, 197 Abkürzungen Commit-IDs 73 Zirkumflexe und Tilden 77 Abnehmer Upstream- und Downstream 244 --abort-Option (git rebase-Befehl) 183 --abort-Option (git am-Befehl) 270 Abrufen git fetch-Befehl 201, 218 vor dem Bestätigen 313 Abrufschritt entfernte Repositories 211 absolute Commit-Namen Commit-IDs 73 add git add-Befehl 22, 66, 146 aktuelles Repository Definition 196 Aliase 278 konfigurieren 30 --all-Option (git commit-Befehl) 56 Already up-to-date-Merges 151 am git am-Befehl 267, 284 Ambivalenz hinsichtlich des Verlaufs 239 --amend-Option (git commit-Befehl) 178 Änderungen in unterschiedlichen Zweigen zusammenführen 108 Projektänderungen nach oben übermitteln 298 unbestätigte 107
| 327
Änderungsmengen atomare Änderungsmengen 72 anonymer Lesezugriff 228 anonymer Schreibzugriff 231 Anschauen Commits 25, 78 Commit-Unterschiede 26 Konfigurationsdateien 30 Zweige 103 Anwenden git apply-Befehl 267 Patches 267 -a-Option (git commit-Befehl) 56 -a-Option (git diff-Befehl) 120 applypatch-msg-Hook 284 Arbeitsbereich mehrere Repositories 245 Arbeitsverzeichnis Definition 22 Asterisk (siehe *) atomare Änderungsmengen Commits 72 atomare Transaktionen von VCS 4 Auflisten Zweignamen 102 Aufspalten Projekte 250 Austauschen Commits 253 --author-Option (git commit-Befehl) 180 --authors-file-Option (git svn-Befehl) 311 Autorenname Fehlermeldung 24 autoritative Repositories erzeugen 202
B Backups Peer-to-Peer 240 Backup-Strategien 1 --bare-Option (git clone-Befehl) 195, 203 Bare-Repositories Definition 194 --base-Option (git diff-Befehl) 141 Basisdateien Diffs 271 Bäume 34 Baumobjekte und Diffs 116 benutzen 43
328 |
Index
Bilder von 38 Hierarchien 46 über 34 Bearbeiten Konfiguration manuell bearbeiten 224 Bekommen Repository-Updates 209 Benutzeridentifikation SVN verglichen mit Git 311 Bereiche Commits 85, 124, 260 Verlauf 79 Bereitstellen Änderungen an Index 35 Dateien 56 über 51 Bilder von entfernten Repository-Operationen 214 binärer Merge-Treiber 158 Bindestrich (siehe - (Minuszeichen)) bisect git bisect-Befehl 90, 160 bisect replay git bisect replay-Befehl 93 bisect visualize git bisect visualize-Befehl 94 BitKeeper 2, 6 blame git blame-Befehl 95, 160 Blobs benutzen 42 Bilder von 38 über 34 -b-Option (git checkout-Befehl) 110
C --cached-Option (git diff-Befehl) 52, 117 --cached-Option (git rm-Befehl) 59 Caches git-svn-Cache 324 cat-Befehl 30 Checkouts git checkout-Befehl 105, 145, 176 Teilprojekte mit Skripten 299 teilweise Checkouts 290 Zweige 105 check-ref-format git check-ref-format-Befehl 100 cherry-pick git cherry-pick-Befehl 174
--color-Option (git diff-Befehl) 120 Commit-Autor konfigurieren 24 Commit-IDs 73, 317 absolute 73 relative 76 symbolische 74 Commit-Log-Meldungen schreiben 58 commit-msg-Hooks 284 Commits 71 anschauen 25 atomare Änderungsmengen 72 ausführen 24 austauschen 253 benutzen 47 Bereiche 85, 124 Bereiche und Merges 260 git commit-Befehl 56, 178 git diff-Befehl 117 Graphen 81 Hooks 283 identifizieren 73 Index 52 Linearisierung 264 Reihenfolge in git diff-Befehl 126 Schritte in 68 Squash-Commits 160 suchen 90 svn rebase 315 über 35 verändern 163 Verlauf 78 verteilte Versionskontrolle 238 vor dem Bestätigen 313 zwischen Upstream- und Downstream-Repositories übertragen 243 Commit-Unterschiede anschauen 26 Concurrent Version System (CVS) 5, 49, 291, 293 config git config-Befehl 223 Content-Tracking 36 --continue-Option (git rebase-Befehl) 182 Criss-Cross-Merges 151, 154 CVS (Concurrent Version System) 5, 49, 291, 293 Cygwin-Git über 13 unter Windows installieren 14
D Daemonen git-daemon 228 HTTP-Daemon 230 daemon-run git-daemon-run 10 DAG (gerichteter azyklischer Graph, directed acyclic graph) Ambivalenz hinsichtlich des Verlaufs 239 Gitlinks 301 topologische Sortierung 263 über 81 Dateien Basisdateien und Diffs 271 benutzen 43 bereitstellen 56 Dateiinhalt (siehe Blobs) entfernen 27, 58 Funktionen verschieben zwischen 72 .gitignore-Dateien 63 Git-Objektmodell 65 Globbing 63 Indexdatei 51 Klassifikationen 52 Konfigurationsdateien 28 Lokalisieren konfliktbehafteter 139 Namen des Git-Verzeichnisses 75 Pfadnamen 37 umbenennen 27, 62 verfolgen 37, 54 zu Repositories hinzufügen 22 Datenstrukturen (siehe auch Index) Debian Linux-Distributionen Git installieren 9 Pakete auf Debian/Ubuntu-Systemen 9 degenerierte Merges 151 Depot-Verzeichnis 202 Diffs 115 Basisdateien 271 Commit-Bereiche 124 einfaches git diff-Beispiel 120 git diff-Befehl 26, 52, 81, 116, 137, 140, 141, 255, 297 Treiber 158 zwischen Dateien nachvollziehen 62 Digests SHA1-Hashes 45 doppelte Punkte (..) 85, 124, 260 doppelter Bindestrich (--) 21
Index
| 329
-d-Option (git branch-Befehl) 112 Downstream-Abnehmer 244 Downstream-Flüsse verteilte Repositories 241 Downstream-Produzenten 244 Downstream-Repositories zwischen Upstream- und Downstream-Repositories übertragen 243 dreifache Punkte (…) 89, 126 Dreifach-Merges 271
E Editoren Commit-Log-Meldungen 58 Umgebungsvariablen 23 Effizienz SHA1 71 von VCS 3 Eltern-Commits benutzen 48 Definition 76 E-Mail git-email 10 Patches 264, 274 SMTP, offene Relay-Server 265 Entfernen Dateien 58 Dateien in Repositories 27 entfernte Repositories 33, 193 Bare-Repositories und git push-Befehl 224 Definition 196 entfernte Zweige hinzufügen und löschen 221 mit Bildern visualisieren 214 Repositories referenzieren 198 über 194 veröffentlichen 226 entfernte Tracking-Zweige 74, 103, 197 entferntes Update 205 Entwickler hinzufügen 207 Entwicklerrolle verteilte Repositories 241 Entwicklungs-Repositories Definition 194 Erreichbarkeit Graphen 86 Erzeugen autoritative Repositories 202 Hooks 281 Zweige 101, 110 /etc/gitconfig-Datei 29
330 |
Index
F Fast-Forward-Merges 151, 217 Fedora Linux-Distributionen Git installieren 10 Fehlermeldungen 24 Festplattenplatz in SVN sparen 129 FETCH_HEAD-Symref 75 find git find-Befehl 41 --first-parent-Option (git log-Befehl 160 flüchtige Dateien 53 -f-Option (git checkout-Befehl) 108 -f-Option (git push-Befehl) 218 -f-Option (git remote update-Befehl) 205 -f-Option (git rm-Befehl) 60 format-patch git format-patch-Befehl 255, 266, 267 Freedesktop.org 236 freigegebene Repositories 233, 320 Funktionen von einer Datei in die andere verschieben 72
G Generieren Patches 255 Gentoo Linux-Distribution Git installieren 10 gerichteter azyklischer Graph (siehe DAG) Geschichte von Git 2 Git benennen 7 Diffs ableiten im Vergleich zu Subversion 128 grundlegende Konzepte 33, 41 kurze Einführung 21 Verlauf 2 git add-Befehl 22, 54, 66, 146 git am-Befehl 267, 284 git apply-Befehl 267 Git Bash-Befehl 16 git bisect replay-Befehl 93 git bisect visualize-Befehl 94 git bisect-Befehl 90, 160 git blame-Befehl 95, 160 git branch-Befehl 102, 112 git checkout-Befehl 105, 145, 176 git check-ref-format-Befehl 100 git cherry-pick-Befehl 174 git clone-Befehl 28, 34, 195, 203, 214, 277, 299, 303
git commit-Befehl 48, 52, 56, 68, 178, 283 git config-Befehl 24, 29, 223 git diff-Befehl 26, 52, 81, 115, 137, 140, 141, 255, 297 git fetch-Befehl 201, 218 git find-Befehl 41 git format-patch-Befehl 255, 266, 267 Git für eine Open-Source-Version kompilieren 12 git hash-object-Befehl 55 git init-Befehl 22, 41 Git installieren 9 mithilfe der Quellversionen 11 git log Konflikte 143 git log-Befehl 25, 78, 86, 96, 124, 134, 143, 147 git ls-files-Befehl 55, 139, 145 git merge-base-Befehl 85, 100, 150 git merge-Befehl 133, 314 git mv-Befehl 27, 60 git pull-Befehl 157, 211, 254, 294, 295, 322 git push-Befehl 201, 206, 217, 218, 220, 224, 285 git rebase-Befehl 180, 211, 238, 314 git reflog-Befehl 114 git remote-Befehl 196, 204, 222 git reset-Befehl 166, 176 git revert-Befehl 176 git rev-list-Befehl 89, 261 git rev-parse-Befehl 75, 78, 170, 207 git rm-Befehl 27, 58 git send-email-Befehl 264, 266 git show-Befehl 25, 48, 81, 147 git show-branch-Befehl 26, 78, 103 git show-ref-Befehl 201 git status-Befehl 53, 139 git svn clone-Befehl 310 git svn dcommit-Befehl 321 git svn rebase-Befehl 316 git svn-Befehl 316, 324 git symbolic-ref-Befehl 76 git tag-Befehl 49 git write-tree-Befehl 44, 47 .git/config-Datei 29 git-Befehl 19 git-daemon 228 git-daemon-run 10 .git-Dateien 41 Dateien .git-Dateien 41 .git-Dateierweiterung 75 Git-eigenes Protokoll 199
git-email 10 .gitignore verglichen mit svn ignore 323 .gitignore-Dateien 54, 63 gitk-Befehl 84, 160 Gitlinks Projekte kombinieren 301 Git-Objektmodell Bedeutung 4 Merges 158 über 65 git-svn 9 git-svn-Cache 324 Git-Verzeichnis Namen 75 .git-Verzeichnis 41 gitweb 9 global eindeutige Identifikatoren SHA1 36 --global-Option (git config-Befehl) 29 Globbing 63 Graphen Commit-Graphen 81 Erreichbarkeit 86 topologische Sortierung 263 --graph-Option (git log-Befehl) 134
H --hard-Option (git reset-Befehl) 166, 177 hash-object git hash-object-Befehl 55 Hash-Werte benutzen 42 SHA1 43, 44, 73 Hash-Zeichen (siehe #) HEAD (siehe abgesonderter HEAD) HEAD-Symref 75 HEAD-Zweige, abgesonderte 111 »Hello World«-Beispiel 42, 46, 146 Herausgeber Rollen und Datenflüsse 244 Hierarchien Bäume 46 Hinzufügen entfernte Zweige 221 Entwickler 207 git add-Befehl benutzen 54 Hooks 277 Arten von 282 installieren 279
Index
| 331
HTTP verglichen mit dem Git-eigenen Übertragungsprotokoll 253 HTTP- und HTTPS-URLs 199 HTTP-Daemon 230
I Identifizieren Commits 73 Identifizieren von Commits relative 76 IDs (siehe auch Commit-IDs) SHA1 36 --ignore-all-space-Option (git diff-Befehl) 120 ignorierte Dateien 53 Importieren Code in Projekte 292 Teilprojekte mit git pull-Befehl 294 Index 51 Dateiklassifikationen 52 Definition 34 git add-Befehl 54 git commit-Befehl 56 git rm-Befehl 58 schmutziger 132 über 35, 52 Überwachen von Konflikten 145 inetd 229 Inhalt im Vergleich mit Pfadnamen 37 inhaltsadressierbare Namen 36 init git init-Befehl 22, 41 Installieren von Git als Quellversion 11 unter Linux 9 unter Windows 13 --interactive-Option (git add-Befehl) 56 --interactive-Option (git commit-Befehl) 56 -i-Option (git rebase-Befehl) 183
K KDE-Projekt 291 Klassifikationen Dateien 52 Klone Definition 193 git clone-Befehl 28, 34, 195, 203, 214, 277, 299, 303
332 |
Index
Zweige 309, 318 Kollisionen SHA1 43 Kombinieren Projekte 289 Kommandozeile benutzen 19 kommentierte Tags Definition 48 Konfigurationsdateien 28 Konfigurieren Aliase 30 Commit-Autor 24 entfernte Repositories 222 Konfigurationswerte beim Klonen 34 konfliktbehaftete Dateien lokalisieren 139 Konflikte Merges 134, 219 kontrollierter Zugriff Repositories 227 Kopieren Teilprojekte importieren 293 Kreise 34 Bilder von 38 Kurzform für Optionen (git-Befehl) 20
L Langform für Optionen (git-Befehl) 20 Lebensdauer Zweige 102 left (<) 144 -left-right-Option(git log-Befehl) 144 leichte Tags Definition 48 Lesezugriff, anonymer 228 Linearisierung Commits 190, 239, 264, 274 Linux Git auf Binärdistributionen installieren 9 Linux Kernel-Projekt 2, 3, 7, 156, 235, 236, 240, 250, 252, 289 log git log-Befehl 25, 78, 86, 96, 124, 134, 147 Logmeldungen 23 Logs git log 78, 143 lokales Repository Definition 196 -l-Option (git config-Befehl) 29
Löschen remote Zweige 221 Zweige 112 ls-files git ls-files-Befehl 55, 139, 145
M Mail Transfer Agent (MTA) 266 Mail User Agent (MUA) 264 Maintainer-Rolle 234, 241 master-Zweig 99, 100 mehrere Repositories 245 Arbeitsbereich 245 benutzen 248 in ein anderes Upstream-Repository konvertieren 246 Projekte aufspalten 250 Repositories starten 245 Meldungen Commit-Logmeldungen 58 Fehlermeldungen 24 Logmeldungen 23 Mercurial 6 MERGE_HEAD-Symref 76 merge-base git merge-base-Befehl 150 Merge-Commits 134, 138, 147, 151, 176, 239, 259, 296, 316, 322, 323 Commit-Graphen 84 degenerierte Merges 151 git add 137 gitk 85 Graphen 84 in unterschiedliche Zweige zusammenführen 109 Konfliktauflösung 147 Merges abbrechen oder neustarten 148 Octopus-Merges 154 Patches 260 Push und Pull 243 relative Commit-Namen 76 Verläufe zusammenführen 219 Zweige anschauen 103 --merge-Option (git log-Befehl) 144 Merges 131 Änderungen in unterschiedliche Zweige zusammenführen 108 Beispiele 132 Dreifach-Merges 271 git svn-Befehl 316
Git-Objektmodell 158 Konflikte 134, 219 Strategien für 148 Subversion 321 svn dcommit 322 und Zweige 101 verglichen mit Patches 274 verglichen mit Rebase 187, 315 Verlauf 219 Merges neustarten 153 (siehe auch git bisect-Befehl) Merge-Schritt entfernte Repositories 212 Merge-Treiber 157 Metadaten in Objekttypen 34 Minuszeichen (siehe -) --mixed-Option (git reset-Befehl) 166 Monotone 6 --M-Option (git diff-Befehl) 120 -m-Option (git checkout-Befehl) 108 --more-Option (git show-branch-Befehl) 26, 104 msysGit über 13 unter Windows installieren 14 MTA (Mail Transfer Agent) 266 MUA (Mail User Agent) 264 Muster .gitignore-Muster und -Dateinamen 63 mutt mbox-Mailordner importieren 265 mv git mv-Befehl 27 git mv-Befehl benutzen 60
N Namen 7 absolute Commit-Namen 73 Commits 73 entfernte Tracking-Zweige 197 Fehlermeldung wegen Autorennamen 24 Git 7 Git-Verzeichnis 75 inhaltsadressierbare Namen 36 Pfadnamen verglichen mit Inhalt 37 relative Commit-Namen 76 Revisionen 27 Symrefs 74 Tags 74 Zweige 76, 99, 100, 102
Index
| 333
nicht verfolgte Dateien 53 Nicht-Fast-Forward-Push-Problem 218 --no-ff-Option (git svn dcommit-Befehl) 151, 322 normale Merges 153
O -o Verzeichnis-Option (git format-patch-Befehl) 266 oberste Commits ändern 178 Objekte benutzen 42 die zusammenarbeiten 38 Git-Objektmodell 65, 158 (siehe auch Blobs) Typen 34 vergleichen 46 Typen von Objekten 34 Objektspeicher 34 Definition 34 Inhaltsüberwachung 37 über 36 Octopus-Merges 154 --onto-Option (git rebase-Befehl) 181 Optionen git-Befehl 20 ORIG_HEAD-Symref 75 --origin-Option (git clone-Befehl) 196 Ours-Merges 155, 157 --ours-Option (git diff-Befehl) 141
P Packard, Keith über xorg und Git 237 Pakete andere Linux-Distributionen 10 Patches 253 anwenden 267 Aufgabe 254 Befehle zum Austauschen von Daten 253 generieren 255 Hooks 284 per E-Mail verschicken 264 Rebase 315 schlechte Patches 274 verglichen mit Merges 274 Peer-to-Peer-Backups 240 Peer-to-Peer-Modell 6, 224, 234, 247 mittels Patches 254
334 |
Index
Pfadbegrenzung git diff-Befehl 126 Pfadnamen git mv 61 verglichen mit Inhalt 37 Pfeile zwischen Commits 40 Pförtner-Repositories 319 Pickaxe Definition 96 git diff-Befehl 128 Pluszeichen (siehe +) -p-Option (git log-Befehl) 144 post-applypatch-Hook 285 post-checkout-Hook 286 post-commit-Hook 284 Post-Hooks Definition 277 post-merge-Hook 287 post-receive-Hook 286 post-update-Hook 286 PowerPC 247 Prä-Hooks Definition 277 pre-applypatch-Hook 285 pre-auto-gc-Hook 287 pre-commit-Hook 283 --prefix=svn/-Option (git svn-Befehl) 319 pre-rebase-Hook 286 pre-receive-Hook 286 --preserve-merges-Option (git rebase-Befehl) 191 --pretty-Option (git log-Befehl) 79 --pretty-Option (git show-Befehl) 48 Projekte aufspalten 250 Code importieren 292 kombinieren 289 Pull git pull-Befehl 211, 294 git svn-Befehl 316 Punkt (.) 22, 99 Push git push-Befehl 201, 206, 217, 218, 220, 224 Hooks 285
Q Quellversionen Git installieren 11
R Raute (siehe # (Hash-Zeichen)) RCS (Revision Control System) 5 Definition 1 Rebase durch git svn rebase bestätigen 315 git rebase-Befehl 180, 238 verglichen mit Merge 315 --rebase-Option (git pull-Befehl) 211 Rebase-Schritt entfernte Repositories 212 rechts (>) 144 Referenzieren Repositories 198 Reflog 195 git reflog-Befehl 114, 172 Refs und Symrefs (symbolische Referenzen) 74 Refspecs Syntax 222 über 200 Zweige zusammenführen 213 rekursive Merges 153, 154, 156 relative Commit-Namen 76 Remote Beispiel 202 git remote-Befehl 196, 204, 222 Konfiguration 222 Origin 204 Remote-Origin Depot-Verzeichnis 202 Repositories Bare-Repositories 194 Dateien entfernen und umbenennen 27 Dateien hinzufügen 22 Entwicklungs-Repositories 194 freigegebene Repositories 233, 320 Funktionsweise 21 Pförtner-Repositories 319 Sammlungen von Hooks replizieren 278 Struktur 233 über 33, 194 veröffentlichen 226 Vollständigkeit von 4 Repositories (siehe auch Klone) reset git reset-Befehl 166, 176 Resolve-Merges 153, 156 revert git revert-Befehl 176
Revision Control System (RCS) 5 Definition 1 Revisionen Namen 27 rev-list git rev-list-Befehl 261 rev-parse git rev-parse-Befehl 75, 78, 170, 207 rm git rm-Befehl 27, 58 --root-Option (git format-patch-Befehl) 262 -r-Option (git diff-Befehl) 116 rsync-Protokoll 200
S -s subtree-Option (git pull-Befehl) 297 SCCS (Source Code Control System) 5 schlechte Patches 274 schmutziges Arbeitsverzeichnis oder Index 132 Schnappschüsse 35, 71, 105, 129, 236 Schrägstrich (/) 64, 99 Schreiben Commit-Log-Meldungen 58 Schreibzugriff, anonymer 231 SCM (Source Code Manager) Definition 1 send-email git send-email-Befehl 264, 266 Konfigurationsoptionen 265 SHA1 (Secure Hash Function) 3 benutzen 44 Dateien ergänzen 56 Effizienz 71 Eindeutigkeit und Kollisionen 43 Werte 36 show git show-Befehl 25, 48, 81, 147 show-branch git show-branch-Befehl 26, 78, 103 Skalieren von VCS 2 Zweignamen und 99 --skip-Option (git rebase-Befehl) 183 Skripten Hook-Skripten 278, 279 Teilprojekte auschecken 299 SMTP Konfigurationsoptionen 265 offene Relayserver 265 --soft-Option (git reset-Befehl) 166
Index
| 335
-S-Option (git diff-Befehl) 128 -S-Option (git log-Befehl) 98, 128 (siehe auch Pickaxe) -s-Option (git ls-files-Befehl) 145 Source Code Control System (SCCS) 5 Source Code Manager (SCM) Definition 1 Sperren CVS 5 --squash-Option (git merge-Befehl, git pullBefehle) 160 Squash-Merges 159 Startpunkte Repositories 245 Zweige 85 --stat-Option (git log-Befehl, git diff-Befehl) 80 Status Dateien 54 git status-Befehl 53, 139 --stdlayout-Option (git svn-Befehl) 319 Suchen Commits 90 SVN (Subversion) 6 Ableiten von Diffs im Vergleich zu Git 128 git svn-Befehl 316, 324 mit Git benutzen 309 Umbenennungen überwachen 62 svn clone git svn clone-Befehl 310 svn dcommit git svn dcommit-Befehl 321 svn rebase git svn rebase-Befehl 316 svn:ignore verglichen mit .gitignore 323 symbolic-ref git symbolic-ref-Befehl 76 symmetrische Differenzen 89 Symrefs (symbolische Referenzen) und Refs 74
T Tags benutzen 48 Bilder von 38 Namen 74 über 35 verglichen mit Zweigen 98 Teilbäume Merges 155, 157
336 |
Index
Teilprojekte durch Kopieren importieren 293 mit git pull-Befehl importieren 294 mit Skripten auschecken 299 teilweise Checkouts 290 temporäre Dateien 53 Text-Merge-Treiber 158 --theirs-Option (git diff-Befehl) 141 Tilde (~) 76 Topic-Zweige Definition 98 topologische Sortierung git format-patch-Befehl 263 Patches 263 Torvalds, Linus Git herstellen 1 über Backups 240 über den Index 52 über Forks 252 Tracking-Zweige entfernte Repositories 194 Treiber Diff-Treiber 158 Merge-Treiber 157
U Übermitteln Projektänderungen upstream 298 Übertragungsprotokolle 253 Überwachen Dateien 52, 54 Dateiumbenennungen 62 Inhalt 36 Konflikte 144 Zweige 197 Ubuntu Linux Git installieren 9 Umbenennen Dateien 27, 62 überwachen 63 Umgebungsvariablen Editoren 23 unbestätigte Änderungen Zweige auschecken 107 Uniform Resource Locator (URL) entfernte Repositories 198 Union-Merge-Treiber 158 Unterbefehle git-Befehl 19
Untermodule 301 über 289 Untersuchen von Konflikten 140 Unveränderbarkeit 3 -u-Option (git diff-Befehl) 115 Update-Hooks 286 Updates Repositories 209 Upstream-Abnehmer 244 Upstream-Flüsse verteilte Repositories 241 Upstream-Herausgeber 244 Upstream-Repositories in andere Upstream-Repositories umwandeln 246 zwischen Upstream- und Downstream-Repositories übertragen 243 URL (Uniform Resource Locator) entfernte Repositories 198
V VCS (Version Control System) Beschränkungen 2 Definition 1 Verändern Commits 163 vereinheitlichte Diffs 115 Vergleichen Objekte 46 Verlauf abrufen 218 alternativer 165, 216 Bereich 79 Commits 78 öffentlichen Verlauf ändern 237 verteilte Versionskontrolle 239 zusammenführen 219 Zweige abrufen und zusammenführen 275 Veröffentlichen Repositories 226 Zweige 101 Verschieben (Push) git svn-Befehl 316 verteilte Entwicklung 2 verteilte Repositories 33, 237 leben mit 237 Maintainer- und Entwicklerrollen 241 Struktur 234 Upstream- und Downstream-Flüsse 241
Veröffentlichen und verteilte Versionskontrolle 238 Verzeichnisse Depot-Verzeichnis 202 .git-Verzeichnis 41 schmutzige 132 Struktur 38 Vorwärtsportierung 181, 212
W Werte SHA1 36 Wildcards Globbing 63 Zweignamen 99 Windows Git installieren 13 -w-Option (git diff-Befehl) 120 write-tree git write-tree-Befehl 44, 47
X X.org-Projekt 236 xinetd 229
Z Zirkumflex (^) 76 Zugriff anonymer Lesezugriff 228 anonymer Schreibzugriff 231 kontrollierter Zugriff 227 Zweige 97 anschauen 103 Aufgabe 97 auschecken 105 benutzen 100 Bilder von 39 entfernte Tracking-Zweige 197 entfernte Zweige hinzufügen und löschen 221 erzeugen 101 git branch-Befehl 102 git show-branch-Befehl 26 git svn-Befehl 316 in VCS 4 klonen 309, 318 Lebensdauer 102 löschen 112 mit komplexen Verläufen abrufen und zusammenführen 275
Index
| 337
Namen 76, 99, 102 Startpunkte 85 Tracking 194, 197, 205 und Merges 101
338 |
Index
veröffentlichen 101 wiederherstellen 114 zusammenführen 213
Über den Autor Jon Loeliger ist als freischaffender Software-Entwickler an Open Source-Projekten wie Linux, U-Boot und Git beteiligt. Er hat Git auf vielen Konferenzen vorgestellt, unter anderem auf der Linux World, und für das Linux Magazine eine Reihe von Artikeln über das Versionskontrollsystem geschrieben. Im früheren Leben entwickelte Jon hochoptimierende Compiler, Router-Protokolle, Linux-Portierungen und das ein oder andere Spiel. Jon hat an der Purdue University in Indiana ein Informatikstudium abgeschlossen. In seiner Freizeit betätigt er sich als Hobby-Winzer.
Kolophon Das Tier auf dem Cover von Versionskontrolle mit Git ist eine Langohrfledermaus, auch Langohr genannt. Diese mittelgroße Fledermaus ist in Europa, Asien und Nordafrika weit verbreitet. Sie lebt in Wäldern, Parks und Gärten, in Gebäuden und unter Kirchendächern und ist oft in Gruppen von 20 und mehr Tieren anzutreffen. Ihren Winterschlaf hält sie meist ganz zurückgezogen in Höhlen oder Kellern. Das Langohr hat eine Flügelspannweite von etwa 25 cm. Seine Ohren sind – wie sein Name schon sagt – sehr lang und haben eine deutlich erkennbare Falte. Die inneren Kanten der Ohren treffen sich in der Mitte des Kopfes und die äußeren Kanten enden gleich neben den Mundwinkeln. Zum Schlafen klappt diese Fledermaus ihre Ohren unter die Flügel; beim Fliegen zeigen die Ohren nach oben. Das lange, seidige Fell bedeckt den Körper und noch ein kleines Stück der Flügel. Es ist braun oder graubraun und an der Unterseite etwas heller. Jungtiere sind hellgrau, weisen also noch nicht die bräunliche Tönung der erwachsenen Tiere auf. Langohrfledermäuse ernähren sich von Fliegen, Motten und Käfern, die sie meist im freien Flug erbeuten. Zum Teil sammeln sie die Insekten auch von Zweigen und Blättern ab. Dabei gleiten sie flink von Baum zu Baum, wobei sie dicht über dem Boden fliegen. Die Paarungszeit der Langohren liegt im September und damit kurz vor Beginn des Winterschlafs. Im Sommer finden sich die schwangeren Weibchen in Gruppen von bis zu 100 Tieren in so genannten Wochenstuben zusammen, meistens suchen sie dafür Hausdächer auf. Die Jungen werden dann im Juni oder Juli geboren. Fledermäuse sind die einzigen Säugetiere, die fliegen können. Entgegen dem weit verbreiteten Aberglauben sind sie nicht blind – die meisten können sogar sehr gut sehen. Um sich nachts zu orientieren, nutzen sie ein Echoortungssystem: Sie stoßen Töne in extrem hohen Frequenzen (so genannte „Ultraschallwellen“) aus, die jenseits des menschlichen Hörvermögens liegen. Die Echos dieser Töne werden von Objekten als Reflexionen zurückgeworfen und von den Fledermäusen aufgenommen und ausgewertet. Auf diese Weise setzen sie ein exaktes „Ton-Bild“ ihrer Umgebung zusammen.
Wie alle Fledermäuse, sind auch die Langohren von zahlreichen Gefahren bedroht, so zum Beispiel vom Verlust ihrer Schlafplätze. Oft werden hohle Bäume gefällt, weil sie für unsicher gehalten werden. Auch der Einsatz von Pestiziden kann sich verheerend auswirken: Sie dezimieren dramatisch die Insektenbestände und verseuchen die Nahrung der fliegenden Säuger mit teilweise tödlichen Giftstoffen. Großen Schaden nehmen Fledermäuse auch, wenn das Holz in den Gebäuden, in denen sie leben, mit Insektiziden behandelt wird. Die erstmalige Behandlung kann eine gesamte Kolonie ausrotten, und noch nach 20 Jahren können diese Chemikalien für Fledermäuse tödlich sein. Es gibt eine Reihe von nationalen und internationalen Gesetzen, Abkommen und Konventionen, die den Schutz der Fledermäuse sicherstellen sollen. Laut Bundesnaturschutzgesetz ist es verboten, Fledermäusen nachzustellen, sie zu fangen, zu verletzen oder zu töten. Außerdem dürfen die Brut-, Wohn- und Zufluchtsstätten der Tiere nicht beschädigt oder zerstört werden. Der Umschlagsentwurf dieses Buchs basiert auf dem Reihenlayout von Edie Freedman und stammt von Karen Montgomery, die hierfür einen Stich des Naturforschers Richard Lydekker aus dem 19. Jahrhundert verwendet hat. Das Coverlayout der deutschen Ausgabe wurde von Michael Oreal mit Adobe InDesign CS3 unter Verwendung der Schriftart ITC Garamond von Adobe erstellt. Als Textschrift verwenden wir die Linotype Birka, die Überschriftenschrift ist die Adobe Myriad Condensed und die Nichtproportionalschrift für Codes ist LucasFont’s TheSans Mono Condensed. Die in diesem Buch enthaltenen Abbildungen stammen von Robert Romano und Michael Oreal.