Enterprise GIT – Shared ReReRe Cache

GIT bietet einige Gimmicks, die den meisten Entwicklern nicht bekannt sind, die aber ein Leuchten in Entwickleraugen zaubern können.

Eines dieser Features ist der GIT-ReReRe Cache.

ReReRe ist ein Akronym für Reuse Recorded Resolutions  und ist ein GIT-Feature, das es erlaubt, Merge-Konflikte die bereits einmal gelöst wurden nicht nochmals manuell lösen zu müssen. GIT verwaltet die Lösung aller Merge-Konflikte in einem eigenen Cache-Bereich.

Betrachten wir meine zwei typischen GIT Protagonisten Lars und Karl.

Entwickler Karl

Quelle sp-studio.de

Entwickler Karl

Quelle sp-studio.de

Die beiden Entwickler arbeiten in einem Team und an einem Projekt. Beide arbeiten am selben Projekt und mit GIT. Erzeugen wir uns zunächst zwei leere Repositories und verbinden sie miteinander.

mkdir karl-repo
cd karl-repo
git init

cd ..
mkir lars-repo
cd lars-repo
git init
git remote add karl ../karl-repo

cd..
cd karl-repo
git remote add lars ../lars-repo

ReReRe-Cache in Akion

Der ReReRe-Cache ist per default inaktiv und wird mit folgender Konfiguration aktiviert. rerere.autoupdate sorgt dafür, dass automatisch aufgelöste Merge-Konflikte automatisch gestaged werden.

git config rerere.enabled true
<code>git config rerere.autoupdate true</code>

Wir machen einen einfachen Commit.

echo "erster commit" &gt;&gt; file1.txt
git add .
git commit -m "erster commit"

Anschließend arbeitet Lars auf einem Branch weiter.

git checkout -b "fb-4711"
echo "zweiter commit" &gt;&gt; file1.txt
git commit -am "zweiter commit"

Anschließend macht Lars einen Hotfix auf dem master.

git checkout master
echo "Dritter Commit" &gt;&gt; file1.txt
git commit -am "Das wird einen Merge Konflikt erzeugen"

Anschließend führen wir die beiden Features wieder zusammen.

git merge fb-4711 
Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt
Recorded preimage for 'file1.txt'
Automatic merge failed; fix conflicts and then commit the result.

Die Ausgabe sieht beinahe aus wie bei jedem Merge-Konflikt. Zusätzlich sehen wir aber folgende Ausgabe

Recorded preimage for 'file1.txt'

Der ReReRe-Cache ist aktiv und GIT ist bereit unseren Merge-Konflikt aufzuzeichnen.

Hierzu wurde im .git Verzeichnis ein neues Verzeichnis “rr-cache” angelegt.

ls .git/rr-cache
365f4a8f75ca95a525b9840a75a0d462a151e69b
ls .git/rr-cache/365f4a8f75ca95a525b9840a75a0d462a151e69b/
preimage
cat .git/rr-cache/365f4a8f75ca95a525b9840a75a0d462a151e69b/preimage 
erster commit
&lt;&lt;&lt;&lt;&lt;&lt;&lt;
Dritter Commit
=======
zweiter commit
&gt;&gt;&gt;&gt;&gt;&gt;&gt;

Im rr-cache Verzeichnis wurde ein neues Verzeichnis angelegt mit dem Hash-Wert des Merge-Konfliktes und einer Datei pre-image. In dieser Datei hält GIT den Merge-Konflikt vor wie er vor dem Resolve aussieht.

Lars löst den Merge-Konflikt auf

git commit -m "resolved merge conflict"
Recorded resolution for 'file1.txt'.
[master ded8479] resolved merge conflict

Im .git-Verzeichnis finden wir jetzt folgendes.

less file1.txt
erster commit
Dritter Commit
zweiter commit
ls .git/rr-cache/365f4a8f75ca95a525b9840a75a0d462a151e69b/
postimage	preimage
cat .git/rr-cache/365f4a8f75ca95a525b9840a75a0d462a151e69b/postimage 
erster commit
Dritter Commit
zweiter commit

GIT zeichnet also den Merge-Konflikt vor einem resolve und dessen Lösung in zwei separaten Dateien auf.

Jetzt simulieren wir den Merge-Konflikt erneut, indem wir den HEAD zurücksetzen.

git reset --hard a2b322a
HEAD is now at a2b322a Das wird einen Merge Konflikt erzeugen
git merge fb-4711
git merge fb-4711 
Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt
Resolved 'file1.txt' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

GIT löst den Konflikt automatisch auf.

Resolved 'file1.txt' using previous resolution.

Wann ist das sinnvoll. Normalerweise löst ein Entwickler Merge-Konflikte nicht mehrmals? Doch! und zwar wenn Lars auf einem Feature-Branch arbeitet und regelmäßig den master in den Feature-Branch merged.

Wir setzen den master erneut zurück, wechseln auf den Feature Branch und mergen den master in den Branch um von den Neuentwicklungen auf dem master zu profitieren.

git reset --hard a2b322a
git checkout fb-4711
git merge master
Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt
Resolved 'file1.txt' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
git commit -m "resolved merge conflict by rerere"
[fb-4711 3d2e82c] resolved merge conflict by rerere

Der Merge-Konflikt wird erneut automatisch gelöst, es spielt also keine Rolle in welcher Richtung der Merge passiert.

Die Arbeit auf dem FB geht noch ein wenig weiter. Irgendwann ist der Zeitpunkt erreicht, an dem wir den Feature Branch zurückführen möchten.

Üblicherweise ist jetzt der Zeitpunkt für einen rebase gekommen, um einen sauberen fast-forward-merge auf den master machen zu können.

echo "ein neuer fb commit" &gt;&gt; file1.txt
git add file1.txt
git commit -m "ein neues commit"
git rebase master
First, rewinding head to replay your work on top of it...
Applying: zweiter commit
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt
Resolved 'file1.txt' using previous resolution.
Failed to merge in the changes.
Patch failed at 0001 zweiter commit

Durch den rebase linearisiert git unsere FB-Historie und in diesem Fall treten alle zuvor über Merges aufgelösten Merge-Konflikte erneut auf! Auftritt des ReReRe-Caches.

git rebase --continue
Applying: zweiter commit
Applying: ein neues commit

Eine andere Möglichkeit wäre natürlich, auf den Rebase zu verzichten, dies bedingt aber eine unschöne Historie weil alle Merges vom master in der Historie bleiben.

Verteilter ReReRe-Cache

Gesetzt den Fall, nicht nur Lars sondern auch Karl arbeiten auf dem Feature-Branch. Nehmen wir weiterhin an, Karl möchte den Feature Branch zurückführen. Die meisten Merges vom master auf den Feature Branch hat aber Lars gemacht.

Das bedeutet, die Lösung der jetzt auftretenden Merge-Konflikte befindet sich im lokalen ReReRe-Cache von Lars. Was ist jetzt, wenn Lars sich ein verlängertes Wochenende genehmigt und nicht im Büro ist. Karl müsste die Merge Konflikte erneut manuell auflösen. Das birgt hohes Gefahrenpotential und ist eine Verschwendung von Karls Zeit.

Der ReReRe-Cache ist per default ein lokaler Cache und GIT bietet keine Hausmittel um diesen Cache zu teilen. Was nicht bedeutet, dass es nicht möglich ist. Es gibt verschiedene Vorschläge dies zu bewerkstelligen. Der meiner Ansicht nach praktikabelste ist die Verwaltung des Cache in einem separaten Repository.

mkdir rerere-cache
git init --bare
Initialized empty Git repository in ...

Um den Cache zu teilen muss Lars macht Lars sein .git/rr-cache Verzeichnis einfach zu einem lokalen Repository.

cd .git/rr-cache
git init
git remote add origin ../../../rerere-cache/
git add .
git commit -m "new cache"
3 files changed, 15 insertions(+)
 create mode 100644 365f4a8f75ca95a525b9840a75a0d462a151e69b/postimage
 create mode 100644 365f4a8f75ca95a525b9840a75a0d462a151e69b/preimage
 create mode 100644 365f4a8f75ca95a525b9840a75a0d462a151e69b/thisimage
 git push origin master
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 402 bytes, done.
Total 5 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
To ../../../rerere-cache/
 * [new branch]      master -&gt; master

Kommen wir zu Karl.

cd karl-repo
git clone ../rerere-cache/ .git/rr-cache

Ab diesem Zeitpunkt profitiert Karl von allen von Lars aufgelösten Merge-Konflikten.

Nehmen wir an, Karl hat sich die Inhalte von Lars geholt bevor er den Rebase gemacht hat.

git checkout fb-4711 
git merge master
Auto-merging file1.txt
CONFLICT (content): Merge conflict in file1.txt
Resolved 'file1.txt' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Das Teilen des Caches

Damit das Sharing des ReReRe-Caches effektiv funktioniert müssen alle Entwickler ihre Updates möglichst zeitnah einspielen. Am besten lässt sich dies über einen Post-Commit Hook realisieren. Ziel ist es, nach jedem Commit mögliche ReReRe Updates an den globalen Cache zu senden.

 vi .git/hooks/post-commit
chmod +x .git/hooks/post-commit

Der Post-Commit Hook wird nach jedem Commit im Repository durchlaufen. Ein idealer Zeitpunkt, da nach dem Auflösen eines Mergekonfliktes üblicherweise ein neuer Commt gemacht wird.

Das einfachste Skript hierfür könnte so aussehen und wird in .git/hooks/post-commit hinterlegt.

#!/bin/bash
cd .git/rr-cache
git pull --rebase origin master
git add .
git commit -m "new cache"
git push origin master

Simulieren wir, dass Karl den Merge-Konflikt erneut löst.

git commit -m "resolved merge conflict"
updating rerere
From /Users/martindilger/development/git/playground/rerere/karl-repo/../rerere-cache
 * branch            master     -&gt; FETCH_HEAD
Current branch master is up to date.
# On branch master
nothing to commit (working directory clean)
Everything up-to-date
[fb-4711 bcf523d] resolved merge conflict

Der verteilte Cache in Aktion

Nehmen wir an, Lars und Karl arbeiten an einem neuem Feature.

git checkout -b "fb-4811"
echo "neues feature" &gt;&gt; file2.txt
git add file2.txt
git commit -m "file2.txt"
updating rerere
From ./karl-repo/../rerere-cache
 * branch            master     -&gt; FETCH_HEAD
Current branch master is up to date.
# On branch master
nothing to commit (working directory clean)
Everything up-to-date
[fb-4811 9b987ab] file2.txt
 1 file changed, 1 insertion(+)
 create mode 100644 file2.txt

Auf dem master ist in der Zwischenzeit eine Änderung gemacht worden die wiederum Konflikte verursacht.

git checkout master
echo "Konflikt!" &gt;&gt; file2.txt
git add file2.txt
git commit -m "Konfliktpotential"

git push lars master
git push lars fb-4811

Nun löst Karl den Konflikt bei sich auf.

git checkout fb-4811
git merge master
#resolve conflict
vi file2.txt 
git add file2.txt
git commit -m "resolved merge conflict"
Recorded resolution for 'file2.txt'.
updating rerere
From /Users/martindilger/development/git/playground/rerere/karl-repo/../rerere-cache
 * branch            master     -&gt; FETCH_HEAD
Current branch master is up to date.
[master dfeac2e] new cache
 2 files changed, 7 insertions(+)
 create mode 100644 89133ae8ffbaeee45c6288ce9ee24314333ba10a/postimage
 create mode 100644 89133ae8ffbaeee45c6288ce9ee24314333ba10a/preimage
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 481 bytes, done.
Total 5 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (5/5), done.
To ./karl-repo/../rerere-cache/
   3eb97d0..dfeac2e  master -&gt; master
[fb-4811 b2c1444] resolved merge conflict

Und fährt anschließend in Urlaub.

cp ../karl-repo/.git/hooks/post-commit .git/hooks
cd .git/rr-cache
git pull --rebase origin master
cd ../cd ..
git checkout fb-4811 
git merge master
Auto-merging file2.txt
CONFLICT (add/add): Merge conflict in file2.txt
Resolved 'file2.txt' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Lars profitiert also von der Arbeit die Karl bereits gemacht hat.

Wann beispielsweise der rr-cache automatisch geupdated wird kann auch über Hooks gesteuert werden. Hierfür könnte beispielsweise der update-Hook verwendet werden.

Andere Möglichkeiten

Es gibt noch andere Möglichkeiten den ReReRe-Cache zu teilen. Man findet beispielsweise einen Vorschlag hierfür einen Orphan-Branch zu verwenden. Dieser Vorschlag ist prinzipiell gut bringt aber den großen Nachteil mit sich, dass nicht mit dem Post-Commit Hook gearbeitet werden kann, da zum Zeitpunkt dieses Hooks das Repository noch gelockt ist und kein Branch-Wechsel stattfinden kann. Die Lösung mit einem externen Git-Repository ist einfacher und flexibler.

Weitere Enterprise-GIT Posts:

Enterprise GIT – Automatisches Einfügen der Jira-Task Nummer

Effective Trainings & Consulting - Martin Dilger



Hat Ihnen dieser Blog-Eintrag gefallen? Ich stelle in diesem Blog Informationen über Tools, Frameworks und Werkzeuge zur Verfügung, die mich produktiver machen. Vielleicht kann ich auch Ihnen helfen, produktiver zu werden.


Ich unterstütze Sie als freier Mitarbeiter bei der Entwicklung von Software-Projekten, Agiler Arbeit sowie Schulungen / Fortbildungen.


Jeden Tag ein bisschen produktiver - ab heute