Rebase

Das obenstehende Szenario ist kein seltenes. Während man selbst an einem Feature arbeitet, hat ein Kollege an anderer Stelle seine eigenen Features erstellt. Und gelegentlich kommt es dabei zu Kollisionen.

Mit einem "Rebase" können Änderungen einer Branch in eine andere eingespielt werden. Aber im Gegenteil zum Merge, der versucht die Änderungen der zu mergenden Branch in die aktuelle einzupflegen, wird beim Rebase versucht, die in der aktuell aktiven Branch gemachten Änderungen auf die in der Rebase-Branch geschehenen Änderungen anzuwenden. Deshalb wird, so wie häufig nach master gemerged wird, auf master rerebased. Und auch dabei kann es zu manueller Merge-Konfliktbeseitigung kommen, wenn Git diese nicht automatisch erledigen kann.

Wir beginnen unser Rebase, in dem wir den aktuellen Merge abbrechen:

$ git merge --abort

Anschließend wechseln wir in unsere Entwicklungsbranch und bereinigen dort die Fehler.

$ git checkout feature/personalized-greeting
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Add function that asks for a name via user input
Using index info to reconstruct a base tree...
M Main.java
Falling back to patching base and 3-way merge...
Auto-merging Main.java
Applying: Use new function in greeting
Using index info to reconstruct a base tree...
M Main.java
Falling back to patching base and 3-way merge...
Auto-merging Main.java
CONFLICT (content): Merge conflict in Main.java
error: Failed to merge in the changes.
Patch failed at 0002 Use new function in greeting
hint: Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

Auch ein Rebase ließe sich (via Git uns auch hilfreich mitteilt) via --abort abbrechen. In diesem Fall wollen wir das aber nicht, sondern überschreiben (da seine und unsere Änderungen sich direkt widersprechen) einfach die Änderungen des Kollegen.

Vorher:

<<<<<<< HEAD
System.out.println("Hello, Humanity!");
=======
String name = askName();
System.out.println(String.format("Hello, %s!", name));
>>>>>>> Use new function in greeting

Nachher:

String name = askName();
System.out.println(String.format("Hello, %s!", name));

Nun die Datei wie gewohnt in ihrem jetzigen Zustand stagen und committen:

$ git add Main.java

Laut Git gibt es nun keine Probleme mehr. Wir lassen das Rebasing fortsetzen:

$ git status
rebase in progress; onto e1b1f59
You are currently rebasing branch 'feature/personalized-greeting' on 'e1b1f59'.
(all conflicts fixed: run "git rebase --continue")
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Main.java
$ git rebase --continue

Bei genauerem Nachdenken ließe sich die Idee des Kollegen aber doch als Standard-Wert verwenden. Das passt zwar thematisch zu unserem Feature, ist aber nicht Teil des Auftrages, an welchem wir gerade arbeiten. Dennoch können wir uns die Zeit nehmen, das eben zu implementieren:

Die neue main-Methode.:

public static void main(String[] args) {
String name = askName();
if ("".equals(name)) {
name = "Human";
}
System.out.println(String.format("Hello, %s!", name));
}

Diese Änderung soll nun aber nicht in diese Branch committed werden. Wir könnten die Datei einfach herum liegen lassen (ohne Staging wird sie ja nicht in Commits eingefügt), aber es gibt einen besseren Weg. Git lässt uns Änderungen in einem so genannten "Stash" für später aufbewahren.

Ein Stash lässt sich als eine Art temporärer, außerhalb der History befindlicher Commit verstehen. Es können beliebig viele solche Stashes erstellt, und auch mit Kommentaren versehen werden (via git stash push -m 'Message'). In unserem Fall reicht uns aber der autogenerierte Name, um den Stash wieder zu finden:

$ git status
git status
On branch feature/personalized-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 stash
Saved working directory and index state WIP on rebase-demonstration: fc29161 Use new function in greeting
$ git status
On branch feature/personalized-greeting
nothing to commit, working tree clean
$ git stash list
stash@{0}: WIP on rebase-demonstration: fc29161 Use new function in greeting

Der Teil stash@{0} ist eine Durchnummerierung aller Stashes, den wir zum wiederherstellen des Stashes verwenden können. Der zuletzt erstellte Stash ist @{0}, der davor @{1} usw.

Nun, da wir unser Rebase und anschließende Aufräumarbeiten abgeschlossen haben, können wir konfliktfrei nach master mergen. Da damit das Feature auch abgeschlossen ist, kann die Branch im Anschluss entfernt werden. Ein push stellt die Änderungen dann anderen Entwicklern zur Verfügung.

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git merge --no-ff feature/personalized-greeting
Merge made by the 'recursive' strategy.
Main.java | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
$ git branch -d feature/personalized-greeting
Deleted branch feature/personalized-greeting (was 7eafc37).
$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (7/7), 1015 bytes | 1015.00 KiB/s, done.
Total 7 (delta 3), reused 0 (delta 0)
To git.honico.com:personal/sone/git-schulung.git
e1b1f59..870ed70 master -> master

Theoretisch kann natürlich das push erneut fehlschlagen, da während unserer Konfliktbereinigung weitere Änderungen auf den Server gepusht wurden. In diesem Fall schlägt unser push fehl, und wir wiederholen den Bereinigungsvorgang.

Auch diese Änderungen werden nun deployed.

$ git checkout production
Switched to branch 'production'
Your branch is up to date with 'origin/production'.
$ git pull
Already up to date.
$ git merge --no-ff master
Merge made by the 'recursive' strategy.
Main.java | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
$ git tag -a prd/2019-11-29-personalized-greeting
$ git push --tags
Enumerating objects: 2, done.
Counting objects: 100% (2/2), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 406 bytes | 406.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0)
To git.honico.com:personal/sone/git-schulung.git
* [new tag] prd/2019-11-29-personalized-greeting -> prd/2019-11-29-personalized-greeting
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

Auf dem Deployment-Repo könnte nun nach einem pull dieser Tag via checkout aktiviert werden.