Konfliktbereinigung
In einem weiteren Terminal-Fenster klonen wir uns nun das Repository ein drittes Mal, um einen Kollegen zu simulieren:
$ cd$ git clone git.honico.com:personal/sone/git-schulung.git schulung-tmp$ cd schulung-tmp$ vim Main.java
In der Main.java ersetzen wir einfach "World" durch "Humanity". Es gilt, wieder zu testen und zu comitten:
$ make runjavac Main.javaHello, World!$ git add Main.java$ git commit
Change greeting subject from World to Humanity
Und fertig! Moment! Nein, ein direkter Commit nach master sollte immer vermieden werden. Zum Glück sind die Änderungen noch nicht gepusht, wir können sie also noch modifizieren.
Als erstes muss der letzte Commit rückgängig gemacht werden. Mit git reset lässt sich die Historie bis zu einem bestimmten Commit zurückrollen. Dieser Commit kann entweder mit seinem Hash identifiziert werden, oder mit einer Anzahl von Commits, die entfernt werden sollen. Da wir nur genau einen Commit rückgängig machen wollen, wählen wir letztere Methode (die 1 steht für die Zahl der Commits):
$ git reset HEAD~1Unstaged changes after reset:M Main.java
Alle Änderungen aus den so entfernten Commits sind nun wieder im Arbeitsverzeichnis (nicht gestaged). Nun erstellen wir die Branch (Branch-Wechsel verändern nicht die gestageden und ungestageden Dateien, falls deshalb die Branch nicht gewechselt werden kann bricht der Befehl mit einer Fehlermeldung ab). Anschließend werden die Änderungen committed, gemerged und gepusht.
$ git checkout -b change/more-innovative-greetingSwitched to a new branch 'change/more-innovative-greeting'$ git statusOn branch change/more-innovative-greetingChanges not staged for commit:(use "git add <file>..." to update what will be committed)(use "git restore <file>..." to discard changes in working directory)modified: Main.javano changes added to commit (use "git add" and/or "git commit -a")$ git add Main.java$ git commit[change/more-innovative-greeting d36e36d] Change greeting subject from World to Humanity1 file changed, 1 insertion(+), 1 deletion(-)$ git checkout masterSwitched to branch 'master'Your branch is up to date with 'origin/master'.$ git pullAlready up to date.$ git merge --no-ff change/more-innovative-greetingMerge made by the 'recursive' strategy.Main.java | 2 +-1 file changed, 1 insertion(+), 1 deletion(-)$ git pushEnumerating objects: 6, done.Counting objects: 100% (6/6), done.Delta compression using up to 8 threadsCompressing objects: 100% (4/4), done.Writing objects: 100% (4/4), 445 bytes | 445.00 KiB/s, done.Total 4 (delta 3), reused 0 (delta 0)To git.honico.com:personal/sone/git-schulung.gite87d25f..e1b1f59 master -> master
Zurück in unserem urspünglichen Repository bereiten wir nun das nächste Feature vor:
$ git checkout -b feature/personalized-greeting$ vim Main.java
Inhalt: Main.java:
import java.util.Scanner;public class Main {private static String askName() {System.out.println("Hey, what's your name?");System.out.print("> ");Scanner scanner = new Scanner(System.in);String name = scanner.nextLine();scanner.close();return name;}public static void main(String[] args) {String name = askName();System.out.println(String.format("Hello, %s!", name));}}
Um die neue Funktion von der Änderung an main getrennt zu committen, führen wir nun ein "partial add" durch. Zuerst versucht Git, die geringe Anzahl von Zeilen als ein "Hunk" zum Staging anzubieten. Durch Eingabe von s(plit) trennt Git die Zeilen in drei Blöcke und bietet diese separat an. Wir wählen die ersten beiden, und lassen den dritten noch nicht stagen.
$ git add -p Main.javadiff --git a/Main.java b/Main.javaindex 9f28e43..1d47396 100644--- a/Main.java+++ b/Main.java@@ -1,7 +1,19 @@+import java.util.Scanner;+public class Main {+ private static String askName() {+ System.out.println("Hey, what's your name?");+ System.out.print("> ");+ Scanner scanner = new Scanner(System.in);+ String name = scanner.nextLine();+ scanner.close();+ return name;+ }+public static void main(String[] args) {- System.out.println("Hello, World!");+ String name = askName();+ System.out.println(String.format("Hello, %s!", name));}}(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? ssSplit into 3 hunks.@@ -1,2 +1,4 @@+import java.util.Scanner;+public class Main {(1/3) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y@@ -1,3 +3,12 @@public class Main {+ private static String askName() {+ System.out.println("Hey, what's your name?");+ System.out.print("> ");+ Scanner scanner = new Scanner(System.in);+ String name = scanner.nextLine();+ scanner.close();+ return name;+ }+public static void main(String[] args) {(2/3) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,?]? y@@ -3,5 +14,6 @@public static void main(String[] args) {- System.out.println("Hello, World!");+ String name = askName();+ System.out.println(String.format("Hello, %s!", name));}}(3/3) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n
Sollte das automatische Splitting mal nicht den gewünschten Effekt haben, kann mittels e(dit) die Datei in einem speziellen Format editiert werden, wo über jede Zeile manuellen entschieden werden kann. Diese Arbeit wird durch GUI-Tools stark vereinfacht.
Aus Sicht von Git enthält die Datei nun Änderungen, die gestaged wurden, und welche, die nicht gestaged wurden:
$ git statusOn branch masterYour branch is up to date with 'origin/master'.Changes to be committed:(use "git restore --staged <file>..." to unstage)modified: Main.javaChanges not staged for commit:(use "git add <file>..." to update what will be committed)(use "git restore <file>..." to discard changes in working directory)modified: Main.java$ git diff --cacheddiff --git a/Main.java b/Main.javaindex 9f28e43..dd53d92 100644--- a/Main.java+++ b/Main.java@@ -1,5 +1,16 @@+import java.util.Scanner;+public class Main {+ private static String askName() {+ System.out.println("Hey, what's your name?");+ System.out.print("> ");+ Scanner scanner = new Scanner(System.in);+ String name = scanner.nextLine();+ scanner.close();+ return name;+ }+public static void main(String[] args) {System.out.println("Hello, World!");}$ git diffdiff --git a/Main.java b/Main.javaindex dd53d92..1d47396 100644--- a/Main.java+++ b/Main.java@@ -12,7 +12,8 @@ public class Main {}public static void main(String[] args) {- System.out.println("Hello, World!");+ String name = askName();+ System.out.println(String.format("Hello, %s!", name));}}
Nun erstellen wir aus dieser neuen Funktion einen Commit:
$ git commit
Add function that asks for a name via user input
Die restlichen Änderungen zu committen ist nun wieder einfach:
$ git add Main.java$ git commit
Use new function in greeting
Nun der spannende Teil. Wenn wir zurück nach master wechseln und pullen, gibt es Änderungen!
$ git checkout masterSwitched to branch 'master'Your branch is up to date with 'origin/master'.$ git pullremote: Counting objects: 4, done.remote: Compressing objects: 100% (4/4), done.remote: Total 4 (delta 3), reused 0 (delta 0)Unpacking objects: 100% (4/4), done.From git.honico.com:personal/sone/git-schulunge87d25f..e1b1f59 master -> origin/masterUpdating e87d25f..e1b1f59Fast-forwardMain.java | 2 +-1 file changed, 1 insertion(+), 1 deletion(-)
Ein Versuch nun zu mergen schlägt fehl (würden die Änderungen sich nicht mit unseren überschneiden, könnte Git die Stände automatisch zusammenführen – sobald es aber Widersprüche gibt müssen wir manuell diese Konflikte beseitigen):
$ git merge --no-ff feature/personalized-greetingAuto-merging Main.javaCONFLICT (content): Merge conflict in Main.javaAutomatic merge failed; fix conflicts and then commit the result.
Das Repository ist nun in einem Zwischenzustand, in dem der Merge manuell durchgeführt oder abgebrochen werden kann. Das kann man auch im Status sehen:
$ git statusOn branch masterYour branch is up to date with 'origin/master'.You have unmerged paths.(fix conflicts and run "git commit")(use "git merge --abort" to abort the merge)Unmerged paths:(use "git add <file>..." to mark resolution)both modified: Main.javano changes added to commit (use "git add" and/or "git commit -a")
Falls Git zusätzlich zu den Konflikten bereits weitere Dateien automatisch mergen konnte, sind diese zu diesem Zeitpunkt bereits gemerged und gestaged.
Falls der Merge abgebrochen werden soll (das Repository wird auf den Stand vor dem Merge-Befehl zurückgedreht, keine Information geht verloren), reicht ein
$ git merge --abort
Ansonsten muss jede Datei mit Konflikt in den Zustand gebracht werden, der am Ende im Commit niedergeschrieben sein soll. Dabei sind nicht notwendigerweise alle geänderten Teile der Datei automatisch Konflikte, Git merged hier zu gut es geht auch Teile schon bevor wir Hand anlegen müssen.
Deshalb sieht unsere Datei nun folgendermaßen aus:
import java.util.Scanner;public class Main {private static String askName() {System.out.println("Hey, what's your name?");System.out.print("> ");Scanner scanner = new Scanner(System.in);String name = scanner.nextLine();scanner.close();return name;}public static void main(String[] args) {<<<<<<< HEADSystem.out.println("Hello, Humanity!");=======String name = askName();System.out.println(String.format("Hello, %s!", name));>>>>>>> feature/personalized-greeting}}
Relevant sind hierbei die Stellen, die durch die Markierungen
<<<<<<<=======>>>>>>>
gekennzeichnet sind (es kann mehrere geben). Der Teil zwischen den ersten beiden Zeilen ist der aktuelle Stand, der Teil zwischen den zweiten beiden Zeilen ist der Stand in dem hinzukommenden Commit. Auch hierbei können visuelle Tools die Lesbarkeit dieser sogenannten Diffs verbessern, beispielsweise in dieser Form eines Three-Way-Merges in Vim (mit Plugin fugitive.vim):

Häufig reicht es, einen der beiden Blöcke und die Markierungen zu löschen. Möglicherweise ist es aber auch eine Kombination der Zeilen das, was man als Ergebnis haben möchte.
In jedem Fall kommt das Beheben eines Merge-Commits bei einem Merge nach master aber einem direkten Commit auf master gleich, den es zu vermeiden gilt. Stattdessen bietet Git eine weitere Möglichkeit an, solche Konflikte zu beheben: Das "Rebasing".