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 run
javac Main.java
Hello, 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~1
Unstaged 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-greeting
Switched to a new branch 'change/more-innovative-greeting'
$ git status
On branch change/more-innovative-greeting
Changes 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
no 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 Humanity
1 file changed, 1 insertion(+), 1 deletion(-)
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git pull
Already up to date.
$ git merge --no-ff change/more-innovative-greeting
Merge made by the 'recursive' strategy.
Main.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing 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.git
e87d25f..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.java
diff --git a/Main.java b/Main.java
index 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,?]? s
s
Split 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 status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Main.java
Changes 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 --cached
diff --git a/Main.java b/Main.java
index 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 diff
diff --git a/Main.java b/Main.java
index 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 master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git pull
remote: 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-schulung
e87d25f..e1b1f59 master -> origin/master
Updating e87d25f..e1b1f59
Fast-forward
Main.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-greeting
Auto-merging Main.java
CONFLICT (content): Merge conflict in Main.java
Automatic 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 status
On branch master
Your 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.java
no 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) {
<<<<<<< HEAD
System.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):

Der vergangene, aktuelle, und im Commit verfügbare Stand der selben Datei in 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".