2.3 Git Branches

Inhalt

Im vorherigen Kapitel haben wir gelernt, dass man nur dann seine Änderungen pushen kann, wenn einem niemand anders zuvorkam. Um diesem Problem aus dem Weg zu gehen, kann ein Branch (engl. für Ast/Zweig) verwendet werden, welcher zu einem späteren Zeitpunkt wieder auf den Hauptpfad gemerged wird.

Im ersten Kapitel haben wir gelernt, dass Git mit jedem Commit ein Snapshot der Daten und eine Referenz auf den vorgehenden Commit speichert. Ein Branch ist nichts anderes als ein Zeiger auf einen spezifischen Snapshot.

Erzeugen wir mit git branch testing einen neuen Branch namens “testing”, so wird ein Zeiger erstellt, welcher auf den selben Commit zeigt auf dem man sich im Moment befindet. Damit Git weiss, wo man sich im Moment gerade befindet, gibt es einen speziellen Zeiger namens HEAD:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
                                         +=======+
                                         | HEAD  |
                                         +===+===+
                                             |
                                             V
                                        +----+-----+
                                        |  master  |
                                        +----+-----+
                                             |
                                             v
+----------+        +----------+        +----+-----+
| Commit 1 +<-------+ Commit 2 +<-------+ Commit 3 |
+----------+        +----------+        +----+-----+
                                             ^
                                             |
                                        +----+-----+
                                        | testing  |
                                        +----------+

Wie man auf der Skizze erkennen kann, wurde der neue Branch “testing” erstellt. Wir befinden uns aber immer noch auf dem “master” Branch (HEAD). Um nun auf den neuen Branch zu wechseln, können wir den Befehl git checkout testing verwenden. Protipp: Um einen neuen Branch zu erstellen und gleich auf diesen zu wechseln, kann der Befehl git checkout -b <branchname> verwendet werden.

Erstellen wir nun einen Commit auf dem “testing” Branch, wechseln wieder zurück auf den Master und erstellen auch dort einen Commit, dann zeigt sich folgendes Bild:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
                                                                +=======+
                                                                | HEAD  |
                                                                +===+===+
                                                                    |
                                                                    V
                                                            +-------+-------+
                                                            | master branch |
                                                            +-------+-------+
                                                                    |
                                                                    |
                                                                    v
                                                            +-------+-------+
                                                    +-------+ Commit master |
+----------+        +----------+        +---- -----+<       +---------------+
| Commit 1 +<-------+ Commit 2 +<-------+ Commit 3 |
+----------+        +----------+        +----------+<       +---------------+
                                                    +-------+ Commit testing|
                                                            +-------+-------+
                                                                    ^
                                                                    |
                                                                    |
                                                            +-------+--------+
                                                            | testing branch |
                                                            +----------------+

Oder anders dargestellt:

1
2
3
4
5
6
7
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Commit master
| * 87ab2 (testing) Commit testing
|/
* f30ab Commit 3
* 34ac2 Commit 2
* 98ca9 Commit 1

Beispiel für Branching und Merging

Stell dir vor du bist TV, du hast bereits alle Mails und Alerts abgearbeitet, niemand will etwas von dir und du hast Zeit endlich an der Story zu arbeiten, welche du in diesem Sprint übernommen hast. Du wechselst also ins Ansible Repo und erstellst einen neuen Branch:

1
2
3
4
5
6
7
$ cd ansible-puzzle
$ git pull
Already up to date.
$ git checkout -b feature/#666/implementmystory
Switched to a new branch "feature/#666/implementmystory"
$ vim roles/mystory/tasks/main.yml
[...]

Wie du so am YAML-Programmieren bist wie ein grosser, kommt plötzlich ein Ticket rein. Auf einem Grafana-Dashboard muss etwas angepasst werden und zwar pronto. Um die Änderungen an deiner Story zu speichern, commitest du diese. Anschliessend wechselst du auf den Master Branch (damit du vom aktuellen produktiven Stand aus Arbeiten kannst). Wie immer pullst du die letzten Änderungen und erstellst dann einen weiteren Branch um das Dashboard zu flicken:

1
2
3
4
5
6
7
git add roles/mystory/*
git commit -m 'initial commit for mystory'
git checkout master
git pull
git checkout -b bugfix/stuermicheib/grafnana
# frickel frickel
git commit -a -m 'Changed background color of failed check to green so everything seems to be good'

Zwischenbemerkung: in unserem Fall würdest du nun deine Änderungen pushen, einen Mergerequest erstellen und jemand für die Peerreview suchen und anschliessend im Gitlab mergen (Siehe dazu mehr im Kapitel Gitlab ). Da wir in diesem Kapitel jedoch Git im generellen anschauen wollen, wird hier dieser Workflow beschrieben.

An diesem Punkt kannst du auf dem “bugfix” Branch noch Tests ausführen und evtl. in einem weiteren Commit noch nachbessern. Gehen wir aber davon aus, dass alles in Ordnung ist und die Änderungen in den “master” soll. Also zurück auf den Masterbranch wechseln und die Änderungen vom “bugfix” Branch mergen:

1
2
3
4
5
6
$ git checkout master
$ git merge bugfix/stuermicheib/grafnana
Updating f42c576..3a0874c
Fast-forward
  dashboard/allesgruen.yml | 2 ++
  1 file changed, 2 insertions(+)

Skizziert sieht das so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

                                                      +=======+
                                                      | HEAD  |
                                                      +===+===+
                                                          |
                                                          V
                                                  +-------+-------+
                                                  | master branch |
                                                  +-------+-------+
                                                          |
                                                          v
                                                  +-------+-------+
                                                  | bugfix branch |
                                                  +-------+-------+
                                                          |
                                                          v
+-------+-------+        +-------+-------+        +-------+-------+
|    Commit 1   |<-------+    Commit 2   |<-------+    Commit 4   |
+-------+-------+        +-------+-------+        +-------+-------+
                                 ^
                                 |
                                 |                 +--------------+
                                 +-----------------+   Commit 3   |
                                                   +------+-------+
                                                          ^
                                                          |
                                                   +------+-------+
                                                   | story branch |
                                                   +-------+------+

Da der Commit 4 direkt nach dem Commit 2 in der Abfolge kommt, konnte der Master-Pointer einfach ein Commit nach vorne verschoben werden (Siehe “Fast-forward” im Command output)

Da nun alle wieder zufrieden sind, die Infrastruktur immer noch ruhig und du wieder Zeit hast an der Story weiterzuarbeiten machst du folgende Schritte:

1
2
3
4
5
6
$ git branch -d bugfix/stuermicheib/grafnana
Deleted branch bugfix/stuermicheib/grafnana (3a0874c)
$ git checkout feature/#666/implementmystory
Switched to branch "feature/#666/implementmystory"
# frickel frickel
$ git commit -a -m 'finished story #666'

Somit ergibt sich folgendes Bild:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

                                                      +=======+
                                                      | HEAD  |
                                                      +===+===+
                                                          |
                                                          V
                                                  +-------+-------+
                                                  | master branch |
                                                  +-------+-------+
                                                          |
                                                          v
+---------------+        +---------------+        +-------+-------+
|    Commit 1   |<-------+    Commit 2   |<-------+    Commit 4   |
+---------------+        +---------------+        +-------+-------+
                                         ^
                                         |
                                         |         +--------------+        +---------------+
                                         +---------+   Commit 3   |<-------+    Commit 5   |
                                                   +--------------+        +-------+-------+
                                                                                   ^
                                                                                   |
                                                                           +-------+-------+
                                                                           |  story branch |
                                                                           +-------+-------+

Da nun der Masterbranch weiter ist als der Punkt, wo wir mit unserem Storybranch abzweigten, kann der Pointer bei einem Merge nicht einfach nach vorne gerückt werden wie vorher. Stattdessen macht Git einen Drei-Wege-Merge. Git verwendet die Branch-Spitzen (also Commit 5 und Commit 4) sowie den letzten gemeinsamen Vorfahren (Commit 2) und erstellt daraus einen neuen Snapshot/Commit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

                                                                                                     +=======+
                                                                                                     | HEAD  |
                                                                                                     +===+===+
                                                                                                         |
                                                                                                         V
                                                                                                 +-------+-------+
                                                                                                 | master branch |
                                                                                                 +-------+-------+
                                                                                                         |
                                                                                                         v
+---------------+        +---------------+        +-------+-------+                              +-------+-------+
|    Commit 1   |<-------+    Commit 2   |<-------+    Commit 4   |<-----------------------------+    Commit 6   |
+---------------+        +---------------+        +-------+-------+                              +---------------+
                                         ^                                                       |
                                         |                                                       |
                                         |         +--------------+        +---------------+     |
                                         +---------+   Commit 3   |<-------+    Commit 5   +<----+
                                                   +--------------+        +-------+-------+
                                                                                   ^
                                                                                   |
                                                                           +-------+-------+
                                                                           |  story branch |
                                                                           +-------+-------+
1
2
3
4
5
6
7
$ git checkout master
Switched to branch 'master'
$ git merge feature/#666/implementmystory
Merge made by the 'recursive' strategy.
roles/mystory/tasks/main.yml |    100 +
1 file changed, 100 insertion(+)
$ git branch -d feature/#666/implementmystory

Einfache Mergekonflikte

Im oben erwähnten Beispiel ist alles automatisch gegangen beim Mergen. Es gibt jedoch Fälle, bei denen Git nicht mehr in der Lage ist, automatisch die Dateien zusammenzuführen, zum Beispiel wenn eine Änderung an der gleichen Stelle einer Datei in beiden Branches vorgenommen wird. Gehen wir vom Beispiel oben aus, die Story, die man da umsetzt, macht auch etwas mit dem Dashboard, welches wir kurzum anpassen mussten:

1
2
3
4
$ git merge feature/#666/implementmystory
Auto-merging dashboard/allesgruen.yml
CONFLICT (content): Merge conflict in dashboard/allesgruen.yml
Automatic merge failed; fix conflicts and then commit the result.

Git konnte nicht automatisch mergen und hat somit keinen commit erstellt. Wir müssen den Mergekonflikt von Hand lösen, bevor wir weiter arbeiten können. Weitere Infos liefert git status:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      dashboard/allesgruen.yml

no changes added to commit (use "git add" and/or "git commit -a")

Git fügt automatisch eine Markierung in die Dateien ein, welche gmerged werden müssen:

1
2
3
4
5
<<<<<<< HEAD:dashboard/allesgruen.yml
"color": "green",
=======
"color": "auto"
>>>>>>> feature/#666/implementmystory:dashboard/allesgruen.yml

Das bedeutet, dass der HEAD (also der Masterbranch, weil auf den haben wir vor dem Mergen gewechselt) den oberen Teil (also immer alles auf Grün) und unsere neuen Änderungen den unteren Teil auf dieser Zeile hatten. Man kann den Konflikt nun lösen, indem man den ganzen Block mit der gewünschten Änderung ersetzt. In unserem Beispiel könnte dies "color": "auto", sein, da wir in der Story was umgesetzt haben, dass die Farbe automatisch richtig gewählt wird, uns jedoch dank dem Mergekonflikt aufgefallen ist, dass wir ein Koma vergessen haben. Ist der Konflikt gelöst, können wir die Datei ganz normal stagen und commiten. Gerade bei grösseren Mergekonflikten kann es praktisch sein, mit tools zu Arbeiten, welche einem die Unterschiede zwischen den beiden Branches grafisch darstellen. Dafür gibt es den Befehl git mergetool.