-
1. Erste Schritte
- 1.1 Über Versionskontrolle
- 1.2 Eine kurze Geschichte von Git
- 1.3 Was ist Git?
- 1.4 Die Kommandozeile
- 1.5 Git installieren
- 1.6 Erstmalige Git-Einrichtung
- 1.7 Hilfe bekommen
- 1.8 Zusammenfassung
-
2. Git Grundlagen
-
3. Git Branching
- 3.1 Branches im Überblick
- 3.2 Grundlegendes Branching und Merging
- 3.3 Branch-Management
- 3.4 Branching-Workflows
- 3.5 Remote-Branches
- 3.6 Rebasing
- 3.7 Zusammenfassung
-
4. Git auf dem Server
- 4.1 Die Protokolle
- 4.2 Git auf einem Server einrichten
- 4.3 Generieren Ihres SSH-Public-Keys
- 4.4 Einrichten des Servers
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Drittanbieter-Hosting-Optionen
- 4.10 Zusammenfassung
-
5. Verteiltes Git
-
6. GitHub
-
7. Git-Werkzeuge
- 7.1 Revisionsauswahl
- 7.2 Interaktives Staging
- 7.3 Stashing und Bereinigen
- 7.4 Ihre Arbeit signieren
- 7.5 Suchen
- 7.6 Historie umschreiben
- 7.7 Reset entmystifiziert
- 7.8 Fortgeschrittene Zusammenführungen
- 7.9 Rerere
- 7.10 Debugging mit Git
- 7.11 Submodule
- 7.12 Bundling
- 7.13 Ersetzen
- 7.14 Credential-Speicher
- 7.15 Zusammenfassung
-
8. Git anpassen
-
9. Git und andere Systeme
- 9.1 Git als Client
- 9.2 Migration zu Git
- 9.3 Zusammenfassung
-
10. Git-Interna
- 10.1 Plumbing und Porcelain
- 10.2 Git-Objekte
- 10.3 Git-Referenzen
- 10.4 Packfiles
- 10.5 Die Refspec
- 10.6 Übertragungsprotokolle
- 10.7 Wartung und Datenwiederherstellung
- 10.8 Umgebungsvariablen
- 10.9 Zusammenfassung
-
Anhang A: Git in anderen Umgebungen
- A1.1 Grafische Oberflächen
- A1.2 Git in Visual Studio
- A1.3 Git in Visual Studio Code
- A1.4 Git in IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine
- A1.5 Git in Sublime Text
- A1.6 Git in Bash
- A1.7 Git in Zsh
- A1.8 Git in PowerShell
- A1.9 Zusammenfassung
-
Anhang B: Git in Ihre Anwendungen einbetten
- A2.1 Kommandozeilen-Git
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
Anhang C: Git-Befehle
- A3.1 Einrichtung und Konfiguration
- A3.2 Projekte abrufen und erstellen
- A3.3 Grundlegendes Snapshotting
- A3.4 Branching und Merging
- A3.5 Projekte teilen und aktualisieren
- A3.6 Inspektion und Vergleich
- A3.7 Debugging
- A3.8 Patching
- A3.9 E-Mail
- A3.10 Externe Systeme
- A3.11 Administration
- A3.12 Plumbing-Befehle
7.8 Git-Tools - Fortgeschrittene Zusammenführungen
Fortgeschrittene Zusammenführungen
Das Zusammenführen in Git ist normalerweise ziemlich einfach. Da Git es einfach macht, einen anderen Branch mehrmals zusammenzuführen, bedeutet dies, dass Sie einen sehr langlebigen Branch haben können, ihn aber auf dem neuesten Stand halten können, während Sie Fortschritte machen und kleine Konflikte oft lösen, anstatt von einem enormen Konflikt am Ende der Serie überrascht zu werden.
Allerdings treten manchmal knifflige Konflikte auf. Im Gegensatz zu einigen anderen Versionskontrollsystemen versucht Git nicht, bei der Lösung von Merge-Konflikten übermäßig clever zu sein. Die Philosophie von Git ist, intelligent darin zu sein, zu bestimmen, wann eine Merge-Lösung eindeutig ist. Wenn jedoch ein Konflikt auftritt, versucht es nicht, ihn automatisch zu lösen. Daher können Sie, wenn Sie zu lange warten, um zwei sich schnell verzweigende Branches zusammenzuführen, einige Probleme bekommen.
In diesem Abschnitt werden wir einige dieser Probleme behandeln und welche Werkzeuge Git Ihnen zur Verfügung stellt, um diese kniffligeren Situationen zu bewältigen. Wir werden auch einige der verschiedenen, nicht standardmäßigen Arten von Zusammenführungen abdecken, die Sie durchführen können, und sehen, wie Sie bereits durchgeführte Zusammenführungen rückgängig machen können.
Merge-Konflikte
Während wir einige Grundlagen zur Behebung von Merge-Konflikten in Grundlegende Merge-Konflikte behandelt haben, bietet Git für komplexere Konflikte einige Werkzeuge, die Ihnen helfen, zu verstehen, was vor sich geht und wie Sie besser mit dem Konflikt umgehen können.
Versuchen Sie zunächst, wenn möglich, sicherzustellen, dass Ihr Arbeitsverzeichnis sauber ist, bevor Sie eine Zusammenführung durchführen, bei der Konflikte auftreten könnten. Wenn Sie Arbeit in Bearbeitung haben, committen Sie sie entweder in einen temporären Branch oder stashen Sie sie. Dadurch können Sie alles rückgängig machen, was Sie hier versuchen. Wenn Sie ungespeicherte Änderungen in Ihrem Arbeitsverzeichnis haben, wenn Sie eine Zusammenführung versuchen, können einige dieser Tipps Ihnen helfen, diese Arbeit zu erhalten.
Gehen wir ein sehr einfaches Beispiel durch. Wir haben eine super einfache Ruby-Datei, die 'hello world' ausgibt.
#! /usr/bin/env ruby
def hello
puts 'hello world'
end
hello()
In unserem Repository erstellen wir einen neuen Branch namens whitespace und ändern alle Unix-Zeilenumbrüche in DOS-Zeilenumbrüche, im Wesentlichen jede Zeile der Datei, aber nur mit Leerzeichen. Dann ändern wir die Zeile „hello world“ in „hello mundo“.
$ git checkout -b whitespace
Switched to a new branch 'whitespace'
$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert hello.rb to DOS
1 file changed, 7 insertions(+), 7 deletions(-)
$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
#! /usr/bin/env ruby
def hello
- puts 'hello world'
+ puts 'hello mundo'^M
end
hello()
$ git commit -am 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
1 file changed, 1 insertion(+), 1 deletion(-)
Nun wechseln wir zurück zu unserem master-Branch und fügen einige Dokumentationen für die Funktion hinzu.
$ git checkout master
Switched to branch 'master'
$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
puts 'hello world'
end
$ git commit -am 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
1 file changed, 1 insertion(+)
Nun versuchen wir, unseren whitespace-Branch zusammenzuführen, und wir werden Konflikte wegen der Leerzeichenänderungen bekommen.
$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.
Abbruch einer Zusammenführung
Wir haben nun mehrere Optionen. Zuerst behandeln wir, wie wir aus dieser Situation herauskommen. Wenn Sie vielleicht keine Konflikte erwartet haben und die Situation noch nicht bearbeiten möchten, können Sie die Zusammenführung einfach mit git merge --abort abbrechen.
$ git status -sb
## master
UU hello.rb
$ git merge --abort
$ git status -sb
## master
Die Option git merge --abort versucht, zu Ihrem Zustand vor der Ausführung der Zusammenführung zurückzukehren. Die einzigen Fälle, in denen dies möglicherweise nicht perfekt gelingt, sind, wenn Sie nicht gestashte, nicht committete Änderungen in Ihrem Arbeitsverzeichnis hatten, als Sie sie ausgeführt haben. Andernfalls sollte sie gut funktionieren.
Wenn Sie aus irgendeinem Grund einfach neu beginnen möchten, können Sie auch git reset --hard HEAD ausführen, und Ihr Repository wird in den letzten Committestand zurückversetzt. Denken Sie daran, dass alle nicht committeten Arbeiten verloren gehen, also stellen Sie sicher, dass Sie keine Ihrer Änderungen wünschen.
Leerzeichen ignorieren
In diesem speziellen Fall sind die Konflikte auf Leerzeichen zurückzuführen. Wir wissen das, weil der Fall einfach ist, aber es ist auch ziemlich einfach, in echten Fällen beim Betrachten des Konflikts zu erkennen, dass jede Zeile auf der einen Seite entfernt und auf der anderen wieder hinzugefügt wird. Standardmäßig sieht Git all diese Zeilen als geändert an, sodass es die Dateien nicht zusammenführen kann.
Die Standard-Merge-Strategie kann jedoch Argumente entgegennehmen, und einige davon betreffen das ordnungsgemäße Ignorieren von Leerzeichenänderungen. Wenn Sie feststellen, dass Sie viele Leerzeichenprobleme bei einer Zusammenführung haben, können Sie diese einfach abbrechen und erneut ausführen, diesmal mit -Xignore-all-space oder -Xignore-space-change. Die erste Option ignoriert Leerzeichen vollständig beim Vergleichen von Zeilen, die zweite behandelt Folgen von einem oder mehreren Leerzeichen als gleichwertig.
$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
hello.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Da in diesem Fall die eigentlichen Dateiänderungen nicht kollidierten, nachdem wir die Leerzeichenänderungen ignoriert haben, funktioniert alles einwandfrei.
Dies ist ein Lebensretter, wenn Sie jemanden in Ihrem Team haben, der gerne alles von Leerzeichen zu Tabs oder umgekehrt neu formatiert.
Manuelle erneute Zusammenführung von Dateien
Obwohl Git die Vorverarbeitung von Leerzeichen ziemlich gut beherrscht, gibt es andere Arten von Änderungen, die Git möglicherweise nicht automatisch verarbeiten kann, aber skriptfähige Korrekturen sind. Als Beispiel nehmen wir an, dass Git die Leerzeichenänderung nicht verarbeiten konnte und wir sie von Hand machen mussten.
Was wir wirklich tun müssen, ist, die Datei, die wir zusammenführen wollen, durch ein dos2unix-Programm laufen zu lassen, bevor wir die eigentliche Dateizusammenführung versuchen. Wie würden wir das tun?
Zuerst geraten wir in den Merge-Konflikt-Zustand. Dann möchten wir Kopien unserer Version der Datei, deren Version (vom Branch, den wir zusammenführen) und die gemeinsame Version (von dort, wo sich beide Seiten verzweigt haben) erhalten. Dann wollen wir entweder ihre Seite oder unsere Seite reparieren und die Zusammenführung für nur diese einzelne Datei erneut versuchen.
Das Abrufen der drei Dateiversionen ist tatsächlich ziemlich einfach. Git speichert all diese Versionen im Index unter „Stages“, die jeweils eine Nummer zugeordnet haben. Stage 1 ist der gemeinsame Vorfahre, Stage 2 ist Ihre Version und Stage 3 ist von MERGE_HEAD, die Version, die Sie zusammenführen („deren“).
Sie können eine Kopie jeder dieser Versionen der kollidierenden Datei mit dem Befehl git show und einer speziellen Syntax extrahieren.
$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb
Wenn Sie etwas härter vorgehen möchten, können Sie auch den Plumbing-Befehl ls-files -u verwenden, um die tatsächlichen SHA-1s der Git-Blobs für jede dieser Dateien zu erhalten.
$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1 hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2 hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3 hello.rb
:1:hello.rb ist nur eine Kurzform für die Suche nach dieser Blob-SHA-1.
Nachdem wir nun den Inhalt aller drei Stages in unserem Arbeitsverzeichnis haben, können wir deren manuell reparieren, um das Leerzeichenproblem zu beheben, und die Datei mit dem wenig bekannten Befehl git merge-file neu zusammenführen, der genau das tut.
$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...
$ git merge-file -p \
hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb
$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
An diesem Punkt haben wir die Datei schön zusammengeführt. Tatsächlich funktioniert dies besser als die Option ignore-space-change, da dies die Leerzeichenänderungen tatsächlich vor der Zusammenführung behebt, anstatt sie einfach zu ignorieren. Bei der ignore-space-change-Zusammenführung hatten wir tatsächlich ein paar Zeilen mit DOS-Zeilenumbrüchen, was die Dinge gemischt machte.
Wenn Sie eine Vorstellung bekommen möchten, bevor Sie diesen Commit finalisieren, was tatsächlich zwischen der einen oder anderen Seite geändert wurde, können Sie git diff bitten, das zu vergleichen, was sich in Ihrem Arbeitsverzeichnis befindet, das Sie als Ergebnis der Zusammenführung committen werden, mit jedem dieser Stages. Gehen wir sie alle durch.
Um Ihr Ergebnis mit dem zu vergleichen, was Sie in Ihrem Branch vor der Zusammenführung hatten, mit anderen Worten, um zu sehen, was die Zusammenführung eingeführt hat, können Sie git diff --ours ausführen.
$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@
# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
Hier können wir leicht sehen, dass das, was in unserem Branch passiert ist, was wir mit dieser Zusammenführung tatsächlich in diese Datei einführen, die einzelne Zeile ändert.
Wenn wir sehen möchten, wie sich das Ergebnis der Zusammenführung von dem auf deren Seite unterschied, können Sie git diff --theirs ausführen. In diesem und den folgenden Beispielen müssen wir -b verwenden, um Leerzeichen zu entfernen, da wir sie mit dem in Git vergleichen, nicht mit unserer bereinigten Datei hello.theirs.rb.
$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
puts 'hello mundo'
end
Schließlich können Sie sehen, wie sich die Datei von beiden Seiten geändert hat, mit git diff --base.
$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
An diesem Punkt können wir den Befehl git clean verwenden, um die zusätzlichen Dateien zu löschen, die wir für die manuelle Zusammenführung erstellt haben, aber nicht mehr benötigen.
$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb
Konflikte auschecken
Vielleicht sind wir aus irgendeinem Grund mit der Auflösung an diesem Punkt nicht zufrieden, oder vielleicht hat das manuelle Bearbeiten einer oder beider Seiten immer noch nicht gut funktioniert und wir brauchen mehr Kontext.
Ändern wir das Beispiel ein wenig. Für dieses Beispiel haben wir zwei langlebige Branches, die jeweils einige Commits enthalten, aber bei der Zusammenführung einen legitimen Inhaltskonflikt verursachen.
$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) Update README
* 9af9d3b Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code
Wir haben nun drei eindeutige Commits, die nur im master-Branch leben, und drei weitere, die im mundo-Branch leben. Wenn wir versuchen, den mundo-Branch zusammenzuführen, erhalten wir einen Konflikt.
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.
Wir möchten sehen, wie der Merge-Konflikt aussieht. Wenn wir die Datei öffnen, sehen wir etwas wie das hier.
#! /usr/bin/env ruby
def hello
<<<<<<< HEAD
puts 'hola world'
=======
puts 'hello mundo'
>>>>>>> mundo
end
hello()
Beide Seiten der Zusammenführung haben Inhalte zu dieser Datei hinzugefügt, aber einige der Commits haben die Datei an derselben Stelle geändert, was diesen Konflikt verursacht hat.
Lassen Sie uns einige der Werkzeuge untersuchen, die Ihnen nun zur Verfügung stehen, um festzustellen, wie dieser Konflikt entstanden ist. Vielleicht ist es nicht offensichtlich, wie genau Sie diesen Konflikt beheben sollten. Sie benötigen mehr Kontext.
Ein hilfreiches Werkzeug ist git checkout mit der Option --conflict. Dies wird die Datei erneut auschecken und die Merge-Konfliktmarker ersetzen. Dies kann nützlich sein, wenn Sie die Marker zurücksetzen und versuchen möchten, sie erneut zu lösen.
Sie können --conflict entweder diff3 oder merge (was der Standard ist) übergeben. Wenn Sie diff3 übergeben, verwendet Git eine etwas andere Version von Konfliktmarkern, die Ihnen nicht nur die „ours“- und „theirs“-Versionen, sondern auch die „base“-Version inline liefert, um Ihnen mehr Kontext zu geben.
$ git checkout --conflict=diff3 hello.rb
Nachdem wir das ausgeführt haben, wird die Datei stattdessen so aussehen.
#! /usr/bin/env ruby
def hello
<<<<<<< ours
puts 'hola world'
||||||| base
puts 'hello world'
=======
puts 'hello mundo'
>>>>>>> theirs
end
hello()
Wenn Ihnen dieses Format gefällt, können Sie es als Standard für zukünftige Merge-Konflikte festlegen, indem Sie die Einstellung merge.conflictstyle auf diff3 setzen.
$ git config --global merge.conflictstyle diff3
Der Befehl git checkout kann auch die Optionen --ours und --theirs entgegennehmen, was eine sehr schnelle Möglichkeit sein kann, einfach eine der beiden Seiten zu wählen, ohne die Dateien überhaupt zusammenzuführen.
Dies kann besonders nützlich für Konflikte bei Binärdateien sein, bei denen Sie einfach eine Seite wählen können, oder wenn Sie nur bestimmte Dateien aus einem anderen Branch zusammenführen möchten – Sie können die Zusammenführung durchführen und dann bestimmte Dateien von einer Seite oder der anderen auschecken, bevor Sie committen.
Merge-Protokoll
Ein weiteres nützliches Werkzeug bei der Lösung von Merge-Konflikten ist git log. Dies kann Ihnen helfen, Kontext darüber zu erhalten, was zu den Konflikten beigetragen haben könnte. Manchmal kann es sehr hilfreich sein, ein wenig Verlauf zu überprüfen, um sich daran zu erinnern, warum zwei Entwicklungslinien denselben Codebereich berührt haben.
Um eine vollständige Liste aller eindeutigen Commits zu erhalten, die in einem der an dieser Zusammenführung beteiligten Branches enthalten waren, können wir die „Drei-Punkte“-Syntax verwenden, die wir in Drei Punkte gelernt haben.
$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'
Das ist eine schöne Liste der insgesamt sechs beteiligten Commits, sowie auf welcher Entwicklungslinie jeder Commit lag.
Wir können dies jedoch weiter vereinfachen, um spezifischeren Kontext zu erhalten. Wenn wir die Option --merge zu git log hinzufügen, werden nur die Commits auf beiden Seiten der Zusammenführung angezeigt, die eine Datei berühren, die derzeit in Konflikt steht.
$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'
Wenn Sie dies stattdessen mit der Option -p ausführen, erhalten Sie nur die Diffs zur Datei, die im Konflikt gelandet ist. Dies kann wirklich hilfreich sein, um Ihnen schnell den Kontext zu geben, den Sie benötigen, um zu verstehen, warum etwas kollidiert und wie Sie es intelligenter lösen können.
Kombiniertes Diff-Format
Da Git alle erfolgreichen Zusammenführungsergebnisse staged, erhalten Sie, wenn Sie git diff in einem kollidierenden Merge-Zustand ausführen, nur das, was noch in Konflikt steht. Dies kann hilfreich sein, um zu sehen, was Sie noch lösen müssen.
Wenn Sie git diff direkt nach einem Merge-Konflikt ausführen, erhalten Sie Informationen in einem ziemlich einzigartigen Diff-Ausgabeformat.
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
#! /usr/bin/env ruby
def hello
++<<<<<<< HEAD
+ puts 'hola world'
++=======
+ puts 'hello mundo'
++>>>>>>> mundo
end
hello()
Das Format wird „Combined Diff“ genannt und liefert Ihnen neben jeder Zeile zwei Datenspalten. Die erste Spalte zeigt Ihnen, ob diese Zeile (hinzugefügt oder entfernt) zwischen dem „ours“-Branch und der Datei in Ihrem Arbeitsverzeichnis unterschiedlich ist, und die zweite Spalte tut dasselbe zwischen dem „theirs“-Branch und Ihrer Arbeitskopie.
In diesem Beispiel sehen Sie also, dass die Zeilen <<<<<<< und >>>>>>> in der Arbeitskopie vorhanden sind, aber in keiner der beiden Seiten der Zusammenführung waren. Das ist sinnvoll, da das Zusammenführungswerkzeug sie zu unserem Kontext hinzugefügt hat, wir sie aber entfernen sollen.
Wenn wir den Konflikt lösen und git diff erneut ausführen, sehen wir dasselbe, aber es ist ein wenig nützlicher.
$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
#! /usr/bin/env ruby
def hello
- puts 'hola world'
- puts 'hello mundo'
++ puts 'hola mundo'
end
hello()
Dies zeigt uns, dass „hola world“ auf unserer Seite vorhanden war, aber nicht in der Arbeitskopie, dass „hello mundo“ auf deren Seite vorhanden war, aber nicht in der Arbeitskopie, und dass schließlich „hola mundo“ auf keiner Seite vorhanden war, aber jetzt in der Arbeitskopie ist. Dies kann nützlich sein, um es zu überprüfen, bevor Sie die Auflösung committen.
Sie können dies auch aus git log für jede Zusammenführung abrufen, um zu sehen, wie etwas nachträglich gelöst wurde. Git gibt dieses Format aus, wenn Sie git show auf einem Merge-Commit ausführen oder wenn Sie eine --cc-Option zu einem git log -p hinzufügen (das standardmäßig nur Patches für Nicht-Merge-Commits anzeigt).
$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date: Fri Sep 19 18:14:49 2014 +0200
Merge branch 'mundo'
Conflicts:
hello.rb
diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
#! /usr/bin/env ruby
def hello
- puts 'hola world'
- puts 'hello mundo'
++ puts 'hola mundo'
end
hello()
Zusammenführungen rückgängig machen
Nachdem Sie nun wissen, wie Sie einen Merge-Commit erstellen, werden Sie wahrscheinlich versehentlich einige erstellen. Eines der großartigen Dinge bei der Arbeit mit Git ist, dass es in Ordnung ist, Fehler zu machen, da es möglich ist (und in vielen Fällen einfach) ist, sie zu beheben.
Merge-Commits sind da keine Ausnahme. Nehmen wir an, Sie haben mit der Arbeit an einem Thema-Branch begonnen, ihn versehentlich in master zusammengeführt, und nun sieht Ihre Commit-Historie so aus:
Es gibt zwei Möglichkeiten, dieses Problem anzugehen, je nachdem, was Ihr gewünschtes Ergebnis ist.
Referenzen korrigieren
Wenn der unerwünschte Merge-Commit nur in Ihrem lokalen Repository existiert, ist die einfachste und beste Lösung, die Branches so zu verschieben, dass sie dort zeigen, wo Sie sie haben möchten. In den meisten Fällen wird, wenn Sie den fehlerhaften git merge mit git reset --hard HEAD~ gefolgt haben, dies die Branch-Zeiger zurücksetzen, sodass sie so aussehen:
git reset --hard HEAD~Wir haben reset bereits in Reset entmystifiziert behandelt, daher sollte es nicht allzu schwer sein herauszufinden, was hier vor sich geht. Hier eine kurze Auffrischung: reset --hard geht normalerweise durch drei Schritte:
-
Den Branch-HEAD verschieben. In diesem Fall möchten wir
masterdorthin verschieben, wo es vor dem Merge-Commit (C6) war. -
Den Index wie HEAD aussehen lassen.
-
Das Arbeitsverzeichnis wie den Index aussehen lassen.
Der Nachteil dieses Ansatzes ist, dass er die Historie umschreibt, was bei einem gemeinsam genutzten Repository problematisch sein kann. Lesen Sie Die Gefahren des Rebasings für mehr darüber, was passieren kann; kurz gesagt, wenn andere Leute die Commits haben, die Sie umschreiben, sollten Sie wahrscheinlich reset vermeiden. Dieser Ansatz funktioniert auch nicht, wenn seit der Zusammenführung weitere Commits erstellt wurden; das Verschieben der Referenzen würde diese Änderungen effektiv verlieren.
Commit rückgängig machen
Wenn das Verschieben der Branch-Zeiger für Sie nicht funktioniert, gibt Git Ihnen die Möglichkeit, einen neuen Commit zu erstellen, der alle Änderungen eines vorhandenen rückgängig macht. Git nennt diese Operation „revert“, und in diesem speziellen Szenario würden Sie sie wie folgt aufrufen:
$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"
Das Flag -m 1 gibt an, welcher Elternteil die „Hauptlinie“ ist und beibehalten werden soll. Wenn Sie eine Zusammenführung in HEAD (git merge topic) aufrufen, hat der neue Commit zwei Elternteile: Der erste ist HEAD (C6), und der zweite ist die Spitze des zusammengeführten Branches (C4). In diesem Fall möchten wir alle Änderungen rückgängig machen, die durch die Zusammenführung von Elternteil Nr. 2 (C4) eingeführt wurden, während wir den gesamten Inhalt von Elternteil Nr. 1 (C6) beibehalten.
Die Historie mit dem Revert-Commit sieht dann so aus:
git revert -m 1Der neue Commit ^M hat exakt die gleichen Inhalte wie C6, sodass es ab hier so aussieht, als ob die Zusammenführung nie stattgefunden hätte, außer dass die nun nicht zusammengeführten Commits immer noch in der Historie von HEAD vorhanden sind. Git wird verwirrt sein, wenn Sie versuchen, topic erneut mit master zusammenzuführen.
$ git merge topic
Already up-to-date.
Es gibt nichts in topic, das nicht bereits von master erreichbar ist. Was noch schlimmer ist, wenn Sie Arbeit zu topic hinzufügen und erneut zusammenführen, wird Git nur die Änderungen ab dem zurückgesetzten Merge bringen.
Der beste Weg, dies zu umgehen, ist, den ursprünglichen Merge rückgängig zu machen (da Sie nun die Änderungen wieder einbringen möchten, die zurückgesetzt wurden), dann einen neuen Merge-Commit zu erstellen.
$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
In diesem Beispiel heben sich M und ^M gegenseitig auf. ^^M führt effektiv die Änderungen von C3 und C4 zusammen, und C8 führt die Änderungen von C7 zusammen, sodass topic nun vollständig zusammengeführt ist.
Andere Arten von Zusammenführungen
Bisher haben wir die normale Zusammenführung von zwei Branches behandelt, die normalerweise mit der sogenannten „rekursiven“ Strategie gehandhabt wird. Es gibt jedoch andere Möglichkeiten, Branches zusammenzuführen. Lassen Sie uns einige davon kurz behandeln.
Präferenz für „unsere“ oder „deren“
Zunächst gibt es eine weitere nützliche Sache, die wir mit dem normalen „rekursiven“ Modus der Zusammenführung tun können. Wir haben bereits die Optionen ignore-all-space und ignore-space-change gesehen, die mit -X übergeben werden, aber wir können Git auch sagen, dass es bei einem Konflikt eine Seite oder die andere bevorzugen soll.
Standardmäßig fügt Git bei einem Konflikt zwischen zwei zusammengeführten Branches Merge-Konfliktmarker in Ihren Code ein und markiert die Datei als konfliktbehaftet, sodass Sie sie auflösen können. Wenn Sie bevorzugen, dass Git einfach eine bestimmte Seite wählt und die andere Seite ignoriert, anstatt Sie den Konflikt manuell auflösen zu lassen, können Sie dem Befehl merge entweder -Xours oder -Xtheirs übergeben.
Wenn Git dies sieht, werden keine Konfliktmarker hinzugefügt. Alle Unterschiede, die zusammenführbar sind, werden zusammengeführt. Alle Unterschiede, die kollidieren, werden einfach die von Ihnen angegebene Seite vollständig ausgewählt, einschließlich Binärdateien.
Wenn wir zum Beispiel „hello world“ zurückgehen, das wir zuvor verwendet haben, sehen wir, dass das Zusammenführen unseres Branches Konflikte verursacht.
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
Wenn wir es jedoch mit -Xours oder -Xtheirs ausführen, tut es das nicht.
$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
hello.rb | 2 +-
test.sh | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 test.sh
In diesem Fall wählt es einfach „hola world“, anstatt Konfliktmarker in der Datei mit „hello mundo“ auf der einen Seite und „hola world“ auf der anderen Seite zu erhalten. Alle anderen nicht kollidierenden Änderungen in diesem Branch werden jedoch erfolgreich zusammengeführt.
Diese Option kann auch an den Befehl git merge-file übergeben werden, den wir zuvor gesehen haben, indem wir etwas wie git merge-file --ours für einzelne Dateizusammenführungen ausführen.
Wenn Sie so etwas tun möchten, aber nicht möchten, dass Git überhaupt versucht, Änderungen von der anderen Seite zusammenzuführen, gibt es eine drakonischere Option, nämlich die „ours“-Merge-Strategie. Dies unterscheidet sich von der „ours“-rekursiven Merge-Option.
Dies führt im Grunde eine gefälschte Zusammenführung durch. Es wird ein neuer Merge-Commit mit beiden Branches als Elternteile aufgezeichnet, aber es wird nicht einmal der Branch betrachtet, den Sie zusammenführen. Es wird einfach der Code in Ihrem aktuellen Branch als Ergebnis der Zusammenführung aufgezeichnet.
$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$
Sie können sehen, dass es keinen Unterschied zwischen dem Branch, auf dem wir uns befanden, und dem Ergebnis der Zusammenführung gibt.
Dies kann oft nützlich sein, um Git im Grunde zu täuschen, indem es denkt, dass ein Branch bereits zusammengeführt wurde, wenn es später eine Zusammenführung durchführt. Zum Beispiel, sagen Sie, Sie haben von einem release-Branch abgezweigt und einige Arbeiten daran vorgenommen, die Sie zu einem späteren Zeitpunkt wieder in Ihren master-Branch zusammenführen möchten. In der Zwischenzeit muss ein Bugfix auf master zurückportiert werden in Ihren release-Branch. Sie können den Bugfix-Branch in den release-Branch zusammenführen und auch denselben Branch mit merge -s ours in Ihren master-Branch (obwohl der Fix bereits dort ist), sodass es keine Konflikte vom Bugfix gibt, wenn Sie später den release-Branch erneut zusammenführen.
Subtree-Zusammenführung
Die Idee der Subtree-Zusammenführung ist, dass Sie zwei Projekte haben und eines der Projekte einem Unterverzeichnis des anderen entspricht. Wenn Sie eine Subtree-Zusammenführung angeben, ist Git oft schlau genug, um zu erkennen, dass eines ein Unterverzeichnis des anderen ist, und entsprechend zusammenzuführen.
Wir werden ein Beispiel dafür durchgehen, wie ein separates Projekt zu einem bestehenden Projekt hinzugefügt und dann der Code des zweiten in ein Unterverzeichnis des ersten zusammengeführt wird.
Zuerst fügen wir die Rack-Anwendung zu unserem Projekt hinzu. Wir fügen das Rack-Projekt als Remote-Referenz in unser eigenes Projekt ein und checken es dann in seinen eigenen Branch aus.
$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
* [new branch] build -> rack_remote/build
* [new branch] master -> rack_remote/master
* [new branch] rack-0.4 -> rack_remote/rack-0.4
* [new branch] rack-0.9 -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"
Nun haben wir das Stammverzeichnis des Rack-Projekts in unserem rack_branch-Branch und unser eigenes Projekt im master-Branch. Wenn Sie das eine und dann das andere auschecken, können Sie sehen, dass sie unterschiedliche Projektwurzeln haben.
$ ls
AUTHORS KNOWN-ISSUES Rakefile contrib lib
COPYING README bin example test
$ git checkout master
Switched to branch "master"
$ ls
README
Das ist ein seltsames Konzept. Nicht alle Branches in Ihrem Repository müssen tatsächlich Branches desselben Projekts sein. Das ist nicht üblich, da es selten hilfreich ist, aber es ist ziemlich einfach, Branches mit völlig unterschiedlichen Historien zu haben.
In diesem Fall möchten wir das Rack-Projekt in unser master-Projekt als Unterverzeichnis ziehen. Wir können dies in Git mit git read-tree tun. Sie werden mehr über read-tree und seine Freunde in Git Internals lernen, aber für jetzt wissen Sie, dass es den Root-Tree eines Branches in Ihren aktuellen Staging-Bereich und Ihr Arbeitsverzeichnis liest. Wir sind gerade wieder zu unserem master-Branch zurückgekehrt und ziehen den rack_branch-Branch in das Unterverzeichnis rack unseres master-Branches unseres Hauptprojekts.
$ git read-tree --prefix=rack/ -u rack_branch
Wenn wir committen, sieht es so aus, als hätten wir alle Rack-Dateien unter diesem Unterverzeichnis – als ob wir sie aus einem Tarball kopiert hätten. Interessant wird es, wenn wir Änderungen von einem der Branches zum anderen relativ einfach zusammenführen können. Wenn sich also das Rack-Projekt aktualisiert, können wir Upstream-Änderungen übernehmen, indem wir zu diesem Branch wechseln und pullen.
$ git checkout rack_branch
$ git pull
Dann können wir diese Änderungen wieder in unseren master-Branch zusammenführen. Um die Änderungen zu übernehmen und die Commit-Nachricht vorab auszufüllen, verwenden Sie die Option --squash sowie die Option -Xsubtree der rekursiven Merge-Strategie. Die rekursive Strategie ist hier die Standardeinstellung, aber wir fügen sie zur Klarheit hinzu.
$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
Alle Änderungen aus dem Rack-Projekt werden zusammengeführt und sind bereit, lokal committet zu werden. Sie können auch das Gegenteil tun – Änderungen im Unterverzeichnis rack Ihres master-Branches vornehmen und sie dann später in Ihren rack_branch-Branch zusammenführen, um sie den Maintainern zu übermitteln oder sie Upstream zu pushen.
Dies gibt uns eine Möglichkeit, einen Workflow zu haben, der dem Submodule-Workflow ähnelt, ohne Submodule zu verwenden (die wir in Submodules behandeln werden). Wir können Branches mit anderen verwandten Projekten in unserem Repository aufbewahren und sie gelegentlich in unser Projekt per Subtree-Merge zusammenführen. Das ist in mancher Hinsicht gut, zum Beispiel werden alle Codes an einem einzigen Ort committet. Es hat jedoch auch andere Nachteile, da es etwas komplexer ist und es leichter ist, Fehler bei der Wiedereingliederung von Änderungen zu machen oder versehentlich einen Branch in ein unrelated Repository zu pushen.
Eine weitere, etwas seltsame Sache ist, dass Sie, um einen Diff zwischen dem, was Sie in Ihrem rack-Unterverzeichnis haben, und dem Code in Ihrem rack_branch-Branch zu erhalten – um zu sehen, ob Sie sie zusammenführen müssen – nicht den normalen diff-Befehl verwenden können. Stattdessen müssen Sie git diff-tree mit dem Branch ausführen, mit dem Sie vergleichen möchten.
$ git diff-tree -p rack_branch
Oder um zu vergleichen, was sich in Ihrem rack-Unterverzeichnis befindet und was der master-Branch auf dem Server zuletzt war, als Sie gefetcht haben, können Sie Folgendes ausführen:
$ git diff-tree -p rack_remote/master