Git Branches: Features unabhängig entwickeln und mit Git verwalten
Die Commits in Git bauen aufeinander auf und bilden dabei eine verkettete "Liste". Diese "Liste" nennt man auch Branch (Entwicklungszweig). Beim Initialisieren eines Repositories wird automatisch ein Default-Branch angelegt, auf dem die Commits dann eingefügt werden.
Weitere Branches kann man mit git branch
anlegen, und die Workingcopy kann mit git switch
oder git checkout
auf einen anderen Branch umgeschaltet werden. Auf diese Weise kann man an mehreren Features parallel arbeiten,
ohne dass die Arbeiten sich gegenseitig stören.
Zum Mergen (Vereinigen) von Branches gibt es git merge
. Dabei werden die Änderungen im angegebenen Branch in
den aktuell in der Workingcopy ausgecheckten Branch integriert und hier ggf. ein neuer Merge-Commit erzeugt. Falls
es in beiden Branches inkompatible Änderungen an der selben Stelle gab, entsteht beim Mergen ein Merge-Konflikt.
Dabei zeigt Git in den betroffenen Dateien jeweils an, welche Änderung aus welchem Branch stammt und man muss
diesen Konflikt durch Editieren der Stellen manuell beheben.
Mit git rebase
kann die Wurzel eines Branches an eine andere Stelle verschoben werden. Dies wird später bei
Workflows eine Rolle spielen.
- (K3) Erzeugen von Branches
- (K3) Mergen von Branches, Auflösen möglicher Konflikte
- (K3) Rebasen von Branches
Neues Feature entwickeln/ausprobieren
A---B---C master
- Bisher nur lineare Entwicklung: Commits bauen aufeinander auf (lineare Folge von Commits)
master
ist der (Default-) Hauptentwicklungszweig- Pointer auf letzten Commit
- Default-Name: "
master
" (muss aber nicht so sein bzw. kann geändert werden)
Anmerkung: Git und auch Github haben den Namen für den Default-Branch von master
auf main
geändert. Der Name an sich ist aber für Git bedeutungslos und kann mittels
git config --global init.defaultBranch <name>
geändert werden. In Github hat der
Default-Branch eine gewisse Bedeutung, beispielsweise ist der Default-Branch das
automatische Ziel beim Anlegen von Pull-Requests. In Github kann man den Default-Namen
global in den User-Einstellungen (Abschnitt "Repositories") und für jedes einzelne
Repository in den Repo-Einstellungen (Abschnitt "Branches") ändern.
Entwicklung des neuen Features soll stabilen master
-Branch nicht beeinflussen
=> Eigenen Entwicklungszweig für die Entwicklung des Features anlegen:
- Neuen Branch erstellen:
git branch wuppie
- Neuen Branch auschecken:
git checkout wuppie
odergit switch wuppie
Alternativ: git checkout -b wuppie
oder git switch -c wuppie
(neuer Branch und auschecken in einem Schritt)
A---B---C master, wuppie
Startpunkt: prinzipiell beliebig (jeder Commit in der Historie möglich).
Die gezeigten Beispiel zweigen den neuen Branch direkt vom aktuell ausgecheckten Commit/Branch ab. Also aufpassen, was gerade in der Workingcopy los ist!
Alternativ nutzen Sie die Langform: git branch wuppie master
(mit master
als
Startpunkt; hier kann jeder beliebige Branch, Tag oder Commit genutzt werden).
Nach Anlegen des neuen Branches zeigen beide Pointer auf den selben Commit.
Anmerkung: In neueren Git-Versionen wurde der Befehl "switch
" eingeführt,
mit dem Sie in der Workingcopy auf einen anderen Branch wechseln können. Der
bisherige Befehl "checkout
" funktioniert aber weiterhin.
Arbeiten im Entwicklungszweig ...
D wuppie
/
A---B---C master
- Entwicklung des neuen Features erfolgt im eigenen Branch: beeinflusst den
stabilen
master
-Branch nicht - Wenn in der Workingcopy der Feature-Branch ausgecheckt ist, gehen die
Commits in den Feature-Branch; der
master
bleibt auf dem alten Stand - Wenn der
master
ausgecheckt wäre, würden die Änderungen in denmaster
gehen, d.h. dermaster
würde sich ab CommitC
parallel zuwuppie
entwickeln
Problem: Fehler im ausgelieferten Produkt
D wuppie
/
A---B---C master
Fix für master
nötig:
git checkout master
git checkout -b fix
- Änderungen in
fix
vornehmen ...
Das führt zu dieser Situation:
D wuppie
/
A---B---C master
\
E fix
git checkout <branchname>
holt den aktuellen Stand des jeweiligen
Branches in die Workingcopy. (Das geht in neueren Git-Versionen auch
mit git switch <branchname>
.)
Man kann weitere Branches anlegen, d.h. hier im Beispiel ein neuer
Feature-Branch fix
, der auf dem master
basiert. Analog könnte man
auch Branches auf der Basis von wuppie
anlegen ...
Fix ist stabil: Integration in master
D wuppie
/
A---B---C master
\
E fix
git checkout master
git merge fix
=> fast forward vonmaster
git branch -d fix
Der letzte Schritt entfernt den Branch fix
.
D wuppie
/
A---B---C---E master
-
Allgemein:
git merge <branchname>
führt die Änderungen im angegebenen Branch<branchname>
in den aktuell in der Workingcopy ausgecheckten Branch ein. Daraus resultiert für den aktuell ausgecheckten Branch ein neuer Commit, der Branch<branchname>
bleibt dagegen auf seinem bisherigen Stand.Beispiel:
- Die Workingcopy ist auf
A
git merge B
führtA
undB
zusammen:B
wird inA
gemergt- Wichtig: Der Merge-Commit (sofern nötig) findet hierbei in
A
statt!
In der Abbildung ist
A
dermaster
undB
derfix
. - Die Workingcopy ist auf
-
Nach dem Merge existieren beide Branches weiter (sofern sie nicht explizit gelöscht werden)
-
Hier im Beispiel findet ein sogenannter "Fast forward" statt.
"Fast forward" ist ein günstiger Spezialfall beim Merge: Beide Branches liegen in einer direkten Kette, d.h. der Zielbranch kann einfach "weitergeschaltet" werden. Ein Merge-Commit ist in diesem Fall nicht notwendig und wird auch nicht angelegt.
Feature weiter entwickeln ...
D---F wuppie
/
A---B---C---E master
git switch wuppie
- Weitere Änderungen im Branch
wuppie
...
git switch <branchname>
holt den aktuellen Stand des jeweiligen Branches in
die Workingcopy. Man kann also jederzeit in der Workingcopy die Branches wechseln
und entsprechend weiterarbeiten.
Hinweis: Während der neue git switch
-Befehl nur Branches umschalten kann,
funktioniert git checkout
sowohl mit Branchnamen und Dateinamen - damit kann
man also auch eine andere Version einer Datei in der Workingcopy "auschecken".
Falls gleiche Branch- und Dateinamen existieren, muss man für das Auschecken
einer Datei noch "--
" nutzen: git checkout -- <dateiname>
.
Feature ist stabil: Integration in master
D---F wuppie D---F wuppie
/ => / \
A---B---C---E master A---B---C---E---G master
git checkout master
git merge wuppie
=> Kein fast forward möglich: Git sucht nach gemeinsamen Vorgänger
Hier im Beispiel ist der Standardfall beim Mergen dargestellt: Die beiden Branches liegen nicht in einer direkten Kette von Commits, d.h. hier wurde parallel weitergearbeitet.
Git sucht in diesem Fall nach dem gemeinsamen Vorgänger beider Branches und führt die jeweiligen Änderungen (Differenzen) seit diesem Vorgänger in einem Merge-Commit zusammen.
Im master
entsteht ein neuer Commit, da kein fast forward beim
Zusammenführen der Branches möglich!
Anmerkung: git checkout wuppie; git merge master
würde den master
in den
wuppie
mergen, d.h. der Merge-Commit wäre dann in wuppie
.
Beachten Sie dabei die "Merge-Richtung":
- Die Workingcopy ist auf
A
git merge B
führtA
undB
zusammen:B
wird inA
gemergt- Wichtig: Der Merge-Commit (sofern nötig) findet hierbei in
A
statt!
In der Abbildung ist A
der master
und B
der wuppie
.
Achtung: Richtung beachten! git checkout A; git merge B
führt beide Branches zusammen,
genauer: führt die Änderungen von B
in A
ein, d.h. der entsprechende Merge-Commit ist in A
!
Konflikte beim Mergen
(Parallele) Änderungen an selber Stelle => Merge-Konflikte
$ git merge wuppie
Auto-merging hero.java
CONFLICT (content): Merge conflict in hero.java
Automatic merge failed; fix conflicts and then commit the result.
Git fügt Konflikt-Marker in die Datei ein:
<<<<<<< HEAD:hero.java
public void getActiveAnimation() {
return null;
=======
public Animation getActiveAnimation() {
return this.idleAnimation;
>>>>>>> wuppie:hero.java
- Der Teil mit
HEAD
ist aus dem aktuellen Branch in der Workingcopy - Der Teil aus dem zu mergenden Branch ist unter
wuppie
notiert - Das
=======
trennt beide Bereiche
Merge-Konflikte auflösen
Manuelles Editieren nötig (Auflösung des Konflikts):
- Entfernen der Marker
- Hinzufügen der Datei zum Index
- Analog für restliche Dateien mit Konflikt
- Commit zum Abschließen des Merge-Vorgangs
Alternativ: Nutzung graphischer Oberflächen mittels git mergetool
Rebasen: Verschieben von Branches
D---F wuppie D---F wuppie
/ => / \
A---B---C---E master A---B---C---E---G master
Bisher haben wir Branches durch Mergen zusammengeführt. Dabei entsteht in der Regel ein extra
Merge-Commit (im Beispiel G
), außer es handelt sich um ein fast forward. Außerdem erkennt
man in der Historie sehr gut, dass hier in einem separaten Branch gearbeitet wurde, der irgendwann
in den master
gemergt wurde.
Leider wird dieses Vorgehen in großen Projekten recht schnell sehr unübersichtlich. Außerdem werden Merges in der Regeln nur von besonders berechtigten Personen (Manager) durchgeführt, die im Falle von Merge-Konflikten diese dann selbst auflösen müssten (ohne aber die fachliche Befähigung zu haben). Hier greift man dann häufig zur Alternative Rebase. Dabei wird der Ursprung eines Branches auf einen bestimmten Commit verschoben. Im Anschluss ist dann ein Merge mit fast forward, also ohne die typischen rautenförmigen Ketten in der Historie und ohne extra Merge-Commit möglich. Dies kann aber auch als Nachteil gesehen werden, da man in der Historie den früheren Branch nicht mehr erkennt! Ein weiterer schwerwiegender Nachteil ist, dass alle Commits im verschobenen Branch umgeschrieben werden und damit neue Commit-IDs bekommen. Das verursacht bei der Zusammenarbeit in Projekten massive Probleme! Als Vorteil gilt, dass man mögliche Merge-Konflikte bereits beim Rebasen auflösen muss, d.h. hier muss derjenige, der den Merge "beantragt", durch einen vorherigen Rebase den konfliktfreien Merge sicherstellen. Mehr dazu in “Branching-Strategien” und “Workflows”.
git rebase master wuppie
führt zu
D'---F' wuppie
/
A---B---C---E master
Nach dem Rebase von wuppie
auf master
sieht es so aus, als ob der Branch wuppie
eben erst vom master
abgezweigt wurde. Damit ist dann ein fast forward Merge von wuppie
in den master
möglich, d.h. es gibt keine Raute und auch keinen extra Merge-Commit (hier nicht
gezeigt).
Man beachte aber die Änderung der Commit-IDs von wuppie
: Aus D
wird D'
! (Datum, Ersteller
und Message bleiben aber erhalten.)
Don't lose your HEAD
-
Branches sind wie Zeiger auf letzten Stand (Commit) eines Zweiges
-
HEAD
: Spezieller Pointer- Zeigt auf den aktuellen Branch der Workingcopy
-
Früheren Commit auschecken (ohne Branch): "headless state"
-
Workingcopy ist auf früherem Commit
-
Kein Branch => Änderungen gehen verloren!
Eventuelle Änderungen würden ganz normal als Commits auf dem
HEAD
-Branch aufgezeichnet. Sobald man aber einen anderen Branch auscheckt, wird derHEAD
auf diesen anderen Branch gesetzt, so dass die eben gemachten Commits "in der Luft hängen". Sofern man die SHA's kennt, kommt man noch auf die Commits zurück. Allerdings laufen von Zeit zu Zeit interne Aufräum-Aktionen, so dass die Chance gut steht, dass die "kopflosen" Commits irgendwann tatsächlich verschwinden.
-
Wrap-Up
- Anlegen von Branches mit
git branch
- Umschalten der Workingcopy auf anderen Branch:
git checkout
odergit switch
- Mergen von Branches und Auflösen von Konflikten:
git merge
- Verschieben von Branches mit
git rebase
Branches und Merges
-
Legen Sie in Ihrem Projekt einen Branch an. Ändern Sie einige Dateien und committen Sie die Änderungen. Checken Sie den Master-Branch aus und mergen Sie die Änderungen. Was beobachten Sie?
-
Legen Sie einen weiteren Branch an. Ändern Sie einige Dateien und committen Sie die Änderungen. Checken Sie den Master-Branch aus und ändern Sie dort ebenfalls:
- Ändern Sie eine Datei an einer Stelle, die nicht bereits im Branch modifiziert wurde.
- Ändern Sie eine Datei an einer Stelle, die bereits im Branch manipuliert wurde.
Committen Sie die Änderungen.
Mergen Sie den Branch jetzt in den Master-Branch. Was beobachten Sie? Wie lösen Sie Konflikte auf?
Mergen am Beispiel
Sie verwalten Ihr Projekt mit Git. Es existieren zwei Branches: master
(zeigt
auf Commit $C$) und feature
(zeigt auf Version $F$). In Ihrer Workingcopy
haben Sie den Branch feature
ausgecheckt:
(1) Mit welcher Befehlsfolge können Sie den Branch feature
in den Branch master
mergen, so dass nach dem Merge die im folgenden Bild dargestellte Situation
entsteht?
(Der Merge läuft ohne Konflikte ab. Es ist irrelevant, welcher Branch am Ende in der Workingcopy ausgecheckt ist.)
(2) Wie können Sie erreichen, dass es keinen Merge-Commit gibt, sondern dass die Änderungen
in $D$ und $F$ im master
als eine lineare Folge von Commits erscheinen?
Interaktive Git-Tutorials: Schaffen Sie die Rätsel?
- [AtlassianGit] Become a git guru.
Atlassian Pty Ltd, 2022. - [Chacon2014] Pro Git
Chacon, S. und Straub, B., Apress, 2014. ISBN 978-1-4842-0077-3.
Kapitel 3 - [GitCheatSheet] Git Cheat Sheets
Github Inc., 2022.