Skip to main content
swissICT Booster  |  M&F Academy  |  M&F Events   |  +41 44 747 44 44  | 
17 Minuten Lesezeit (3483 Worte)

Git under the hood: Werkzeuge und Tricks für erfahrene Git User

Im März 2020 hat M&F im Rahmen des Trainee-Programms einen Workshop zum Thema Git veranstaltet. Als langjähriger User von Git in verschiedenen Projekten sah ich diesem Workshop sehr gelassen entgegen. Was konnte mich denn da wirklich noch überraschen? Jetzt im Nachhinein kann ich aber sagen, dass Pablo Vergés ein echter Git-Experte ist, der gerne auch mal unter die Haube schaut. Es können also auch erfahrene Git-User von seinem Workshop profitieren. In diesem Blogpost erfährst du, welche neue Werkzeuge und Tricks ich für meinen Programmieralltag mit Git aus diesem Workshop mitnehme.

 

An wen sich dieser Blog richtet

Wenn du diesen Blog liest, gehe ich davon aus, dass du zumindest etwas Erfahrung mit Git mitbringst und mit den grundlegenden Befehlen und Begriffen vertraut bist. Diese Dinge wurden im Workshop zwar besprochen, würden aber diesen Artikel massiv verlängern. Wenn du dich zuerst in diese Dinge vertiefen möchtest, gibt es das Git-Manual und weitere gute Tutorials von Github oder Atlassian.

 

Über Pablo Vergés

Pablo studierte Experimental-Physik am Labor für Hochenergiephysik der Universität Bern und arbeitet als Software Engineer für unsere Partnerfirma DECTRIS. Seine arbeitsfreie Zeit verbringt er gerne im grossen blauen Raum, wo er die Strassen auf seinem Rennrad unsicher macht oder die Gravitation in einer Seilschaft herausfordert.

Vor etwas über einem Jahr wurde er von der Universität Bern beauftragt, einen Git-Workshop im Rahmen eines Certificate of Advanced Studies (CAS) zu geben. Der Workshop fand grossen Anklang, weil dieser nicht nur neue Benutzer in Git eingeführte, sondern auch erfahrenen Git-Anwender wesentliche Erkenntnisse einbrachte. Er geniesst es sehr, seine Kenntnisse weiter zu geben und sein Wissen zu Git im Rahmen von Workshops auf Probe zu stellen.

 

Git under the Hood

Ein spannendes Konzept, das Pablo direkt zu Beginn seines Workshops eingeführt hat, ist "Gitception": Er hat die Interna eines Git Repository (den .git-Ordner) selbst in einem Git-Repository eingecheckt. Dies hat es uns ermöglicht, genau zu verfolgen, was sich "under the hood" bei Git verändert hat, wenn wir Befehle ausgeführt haben.

Was ich dabei gelernt habe, ist, dass ein Git-Commit einen oder mehrere Trees hat. An diesen Trees angehängt sind Blobs mit dem Inhalt der Dateien, die für diesen Commit wichtig sind. Die Commits selbst haben Parents, die einen früheren Zustand darstellen. Git orientiert sich an dieser Struktur. Wenn wir beispielsweise einen Commit auschecken, sucht Git für jede Datei den aktuellsten Blob, der diese Datei betrifft, und kann so den gewünschten Zustand in der Datei herstellen.

Ein Beispiel mit zwei Commits. Commit 1 ist der Parent von Commit 2. Die Datei A wurde in beiden Commits geändert und ist deshalb als Blob in beiden Trees enthalten.

Wenn man also einmal aus Versehen eine Datei oder einen Commit verloren hat, kann man sich im schlimmsten Fall immer noch im .git Ordner nach den betreffenden Blobs umsehen.

Branches hingegen sind nur Referenzen auf einen Commit. Sie können verschoben werden, ohne das sich die dahinter liegenden Commits verändern. Deshalb kann man Branches auch so schnell erstellen.

 

Hunks: Die Atome der Git Welt

$ git diff
diff --git a/README.md b/README.md
index 0cd58fb..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-
+> "Ein Zauberer kommt nie zu spät. Ebenso wenig zu früh. Er trifft genau dann ein, wann er es beabsichtigt." - Gandalf
​
> Ich könnte es dir beschreiben. Oder soll ich dir eine Kiste holen?. - Legolas
​
@@ -18,5 +17,6 @@
​
​
> Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin

git diff und das entsprechende Resultat in einer shell ist sicher vielen Git Usern ein Begriff. Ich muss aber zugeben, dass ich mich selbst nie gross darum gekümmert habe, was es genau bedeutet, wenn Änderungen in einer einzelnen Datei in verschiedenen Abschnitten (getrennt durch die Zeilennotationen @@ -18,5 +17,6 @@) angezeigt werden.

Solche einzelnen Teile von Änderungen werden Hunks genannt. Sie stellen einen essentiellen Baustein der Logik von Git dar. Wer sie versteht und anwenden kann, hat viel mehr Macht in der Git-Welt.

Was also ist ein Hunk genau? Der Teminus Hunk ist nicht einmal spezifisch für Git, sondern wird auch im Gnu diffutils format verwendet. Dort steht: "Beim Vergleich von zwei Dateien findet diff die Zeilen, die beiden Dateien gemeinsam sind, und dazwischen Gruppen von unterschiedlichen Zeilen, die als "hunks" bezeichnet werden."

Für Git sind Hunks auch die kleinste Einheit von Veränderung mit der gearbeitet werden kann. Viele der häufig verwendeten Git Befehle erlauben es mit dem -p Flag auf der Ebene von Hunks zu arbeiten und so mehr Kontrolle darüber zu haben, was in welchem Commit landet.

Schauen wir uns einige Beispiele an, wie man mit Hunks arbeiten kann: Mit git add -p können wir nun beispielsweise das Zitat von Gandalf (aus dem vorherigen Beispiel) stagen und somit auch commiten, ohne dass das Zitat von Pippin ebenfalls commitet wird. Und dies obwohl es im gleichen File ist:

$ git add -p
diff --git a/README.md b/README.md
index 0cd58fb..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-
+> "Ein Zauberer kommt nie zu spät. Ebenso wenig zu früh. Er trifft genau dann ein, wann er es beabsichtigt." - Gandalf
​
> Ich könnte es dir beschreiben. Oder soll ich dir eine Kiste holen?. - Legolas
​
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -18,5 +17,6 @@
​
​
> Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin
​
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n

Wir werden gefragt, welche Hunks wir stagen wollen. Wenn wir diese Frage nun nur für das Zitat von Gandalf mit y (Yes) beantworten und beim Zitat von Pippin mit n (No), dann erhalten wir folgendes Resultat:

$ git diff
diff --git a/README.md b/README.md
index d059896..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -17,5 +17,6 @@
​
​
> Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin

Ein git diff zeigt jetzt nur noch das Zitat von Pippin an, das nicht gestaged wurde. Das Zitat von Gandalf ist gestaged und somit nicht mehr für git diff sichtbar. Um das Zitat von Gandalf wieder zu sehen, müssen wir git diff --cached benützen:

$ git diff --cached
diff --git a/README.md b/README.md
index 0cd58fb..d059896 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-
+> "Ein Zauberer kommt nie zu spät. Ebensowenig zu früh. Er trifft genau dann ein, wann er es beabsichtigt." - Gandalf
​
> Ich könnte es dir beschreiben. Oder soll ich dir eine Kiste holen?. - Legolas

Somit ist es viel einfacher an zwei Features gleichzeitig zu arbeiten oder sonstige Änderungen an einer Datei vorzunehmen, die beim Entwickeln hilfreich sein können aber nicht in den Commit gehören. Praktisch, nicht? :-)

Nach einem ähnlichen Prinzip funktioniert es mit weiteren Befehlen:

git reset -p [file]

Dieser Befehl erlaubt es, gewisse Hunks von einem gestaged File auch wieder aus dem Stage-Bereich zu entfernen, sollten sie versehentlich dort gelandet sein. 

git checkout -p

Dies erlaubt es, gewisse Änderungen an einer Datei zu verwerfen und andere beizubehalten.

git stash -p

Mit diesem Befehl kann man für jeden Hunk im Working Directory entscheiden, ob man ihn stashen will oder nicht. Was mich dabei überrascht hat: Hunks, die man nicht stasht, bleiben unverändert im Working Directory liegen.

 

Geschichte wird von den Gewinnern geschrieben

Bis zu diesem Workshop war ich der Auffassung, dass ich schon einen gewichtigen Grund brauche, um nicht nur einen, sondern gerade mehrere Commits mit einem git reset zu vernichten oder umzuschreiben. Pablo hat uns aber gezeigt, dass dies sogar eine gute Idee ist: So kann man die Commits in einer Weise anordnen, dass sie der Reviewer einfacher nachvollziehen kann und man kann sicherstellen, dass jeder Commit auch wirklich buildet und alle vorgesehenen Tests besteht.

Git gibt einem hier verschiedene Möglichkeiten, und je nach Situation oder Vorliebe kann man entscheiden, welchen Weg man wählen will.

 

Der 1. Weg: Git interactive rebase

Zuerst checken wir unseren Feature-Branch aus:

$ git checkout -b quotes-feature
Switched to a new branch 'quotes-feature'

 Dann arbeiten wir darauf weiter und fügen einige Änderungen hinzu. (Als Beispiel ein Zitat von Pippin)

$ git diff
diff --git a/README.md b/README.md
index 0cd58fb..b49e8cd 100644
--- a/README.md
+++ b/README.md
@@ -18,5 +18,6 @@
​
​
> Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin

Commiten:


$ git commit -am "Quote Pippin" [quotes-feature 4e5af43] Quote Pippin 1 file changed, 2 insertions(+), 1 deletion(-)

Wir arbeiten noch weiter. (Und fügen ein Zitat von Gandalf hinzu.)

$ git diff
diff --git a/README.md b/README.md
index b49e8cd..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-
+> "Ein Zauberer kommt nie zu spät. Ebensowenig zu früh. Er trifft genau dann ein, wann er es beabsichtigt." - Gandalf
​
> Ich könnte es dir beschreiben. Oder soll ich dir eine Kiste holen?. - Legolas

Und commiten diese Änderung ebenfalls:

$ git commit -am "Quote Gandalf"
[quotes-feature ebc01d6] Quote Gandalf
1 file changed, 1 insertion(+), 2 deletions(-)

Unsere Git History sieht nun folgendermassen aus:

commit ebc01d6ed1326f46159d6f6a069d08ac5ba28897 (HEAD -> quotes-feature)
Author: Simon Walter >
Date:   Mon Mar 30 16:07:44 2020 +0200
​
  Quote Gandalf
​
commit 4e5af438bc9d692f2df1beea65a43a72d86c72f1
Author: Simon Walter >
Date:   Mon Mar 30 16:05:26 2020 +0200
​
  Quote Pippin
​
commit 9975ec210812b2bdb915f4c66a908adae7bc4d13 (master)
Author: Simon Walter >
Date:   Fri Mar 27 16:30:39 2020 +0100
​
  Some awesome LotR Quotes

Nun aber realisieren wir, dass es besser wäre, wenn das Zitat von Gandalf chronologisch vor jenem von Pippin kommt. Wir haben verschiedene Möglichkeiten dies zu erreichen.

Die erste ist git rebase -i (Das i-Flag steht für interactive.):

$ git rebase -i master
hint: Waiting for your editor to close the file...

Dies sollte ein Editor mit einer Datei öffnen, die uns anzeigt, in welcher Reihenfolge die Commits auf den Master überspielt werden:

pick 4e5af43 Quote Pippin
pick ebc01d6 Quote Gandalf
​
# Rebase 9975ec2..ebc01d6 onto 9975ec2 (2 commands)
# ... (Der Rest der Datei ist für diesen Artikel unwichtig.)

Wir können hier nun die Reihenfolge neu und frei bestimmen:

pick ebc01d6 Quote Gandalf
pick 4e5af43 Quote Pippin
​
# Rebase 9975ec2..ebc01d6 onto 9975ec2 (2 commands)
# ... (Der Rest der Datei ist für diesen Artikel unwichtig.)

Wenn wir nun speichern (ctrl + S) und den Editor schliessen, werden die Commits in der Reihenfolge von oben nach unten auf den Master abgespielt. Wenn man nun git log ausführt, kann man sehen, dass der Commit mit dem Zitat von Gandalf früher angezeigt wird, obwohl er zu einem späteren Zeitpunkt verfasst wurde.

commit 323a158c9a4af7fb4d46f154120c87666c6903df (HEAD -> quotes-feature)
Author: Simon Walter >
Date:   Mon Mar 30 16:05:26 2020 +0200
​
  Quote Pippin
​
commit 2e6eff167630aee48391f9fbc7be12168c2cf3a1
Author: Simon Walter >
Date:   Mon Mar 30 16:07:44 2020 +0200
​
  Quote Gandalf
​
commit 9975ec210812b2bdb915f4c66a908adae7bc4d13 (master)
Author: Simon Walter >
Date:   Fri Mar 27 16:30:39 2020 +0100
​
  Some awesome LotR Quotes

Diese Variante hat aber einen Nachteil: Es ist immer auch ein Rebase damit verbunden. Das kann problematisch sein, wenn sich der Master schon weiterentwickelt hat, seit der Feature-Branch abgezweigt wurde und man eigentlich noch nicht rebasen will. In diesem Szenario kann man auch den zweiten Weg wählen:

 

Der 2. Weg: Git rebase kombiniert mit Git merge-base

git merge-base [commit] [commit] ... ist ein Befehl, den ich bis zu diesem Workshop auch nicht gekannt habe. Dieser Befehl sucht den Commit, der sich am besten als Basis für einen Merge mit den angegebenen Commits (oder Branches, die ja Referenzen zu Commits sind).

         o---o---o---B
        /
---o---1---o---o---o---A

In diesem Beispiel sehen wir zwei Commits A und B, die gemeinsame Vorfahren haben. git merge-base A B würde sich die Vorfahren von A und B anschauen und den Commit 1 als beste Basis vorschlagen.

Dies ist speziell nützlich, wenn man nur die Commits ändern, aber gar noch nicht auf den fortgeschrittenen Master-Branch rebasen will. In diesem Fall möchte man den Rebase auf dem Commit ausführen, bei dem man mit dem Feature-Branch vom Master-Branch abgezweigt ist. Für einen solchen Rebase kann man den folgenden Befehl benutzen:

$ git rebase -i $(git merge-base HEAD origin/master)

Anschliessend würde dieser Rebase genau gleich ablaufen wie die erste Variante.

 

Der 3. Weg: Git reset -p

Das interactive rebasing bietet einem viele Optionen, Commits auch im Nachhinein zu verändern. In den meisten Fällen sollte dies ausreichen, um ein Ziel zu erreichen. Wenn man aber einige Back-Up Commits gemacht hat, z.B. um seine Arbeit nicht zu verlieren, diese aber gar nicht nach Features aufgeteilt sind und daher in ihrer Form für eine andere Person nicht so viel Sinn machen können, ist git reset -p nützlich.

Wir starten wieder mit derselben Ausgangslage: Es wurde commited und erst im Nachhinein festgestellt, dass wir die Zitate gerne in einer anderen Reihenfolge hätten.

Hier siehst du noch einmal den Git log:

commit ebc01d6ed1326f46159d6f6a069d08ac5ba28897 (HEAD -> quotes-feature)
Author: Simon Walter >
Date:   Mon Mar 30 16:07:44 2020 +0200
​
  Quote Gandalf
​
commit 4e5af438bc9d692f2df1beea65a43a72d86c72f1
Author: Simon Walter >
Date:   Mon Mar 30 16:05:26 2020 +0200
​
  Quote Pippin
​
commit 9975ec210812b2bdb915f4c66a908adae7bc4d13 (master)
Author: Simon Walter >
Date:   Fri Mar 27 16:30:39 2020 +0100
​
  Some awesome LotR Quotes

Mit git reset --mixed master setzen wir den Branch auf den Stand des Master-Branches zurück, aber behalten die Änderungen im Working-Directory:

$ git reset --mixed master
Unstaged changes after reset:
M       README.md

Ein git diff zeigt uns, dass die Änderungen jetzt wieder im Working-Directory sind:

$ git diff
diff --git a/README.md b/README.md
index 0cd58fb..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-
+> "Ein Zauberer kommt nie zu spät. Ebensowenig zu früh. Er trifft genau dann ein, wann er es beabsichtigt." - Gandalf
 
 > Ich könnte es dir beschreiben. Oder soll ich dir eine Kiste holen?. - Legolas
 
@@ -18,5 +17,6 @@
 
 
 > Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin

Nun können wir mit git add -p zuerst das Zitat von Gandalf und dann jenes von Pippin commiten, wie das gewünscht war.

Die Befehle für den ersten Commit:

$ git add -p README.md
diff --git a/README.md b/README.md
index 0cd58fb..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,4 @@
-
-
+> "Ein Zauberer kommt nie zu spät. Ebensowenig zu früh. Er trifft genau dann ein, wann er es beabsichtigt." - Gandalf
​
> Ich könnte es dir beschreiben. Oder soll ich dir eine Kiste holen?. - Legolas
​
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -18,5 +17,6 @@
​
​
> Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin
​
(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? n
​
​
$ git commit -m "Quote Gandalf"
[quotes-feature be6d537] Quote Gandalf
1 file changed, 1 insertion(+), 2 deletions(-)

 Die Befehle für den zweiten Commit:

$ git add -p README.md
diff --git a/README.md b/README.md
index d059896..cdba21e 100644
--- a/README.md
+++ b/README.md
@@ -17,5 +17,6 @@
​
​
> Und meine Axt! - Gimli
->
+
+>Wir hatten das Erste, ja, aber was ist mit dem zweiten Frühstück? - Pippin
​
(1/1) Stage this hunk [y,n,q,a,d,e,?]? y
​
​
$ git commit -m "Quote Pippin"
[quotes-feature d209333] Quote Pippin
1 file changed, 2 insertions(+), 1 deletion(-) 

Dies ergibt den neuen Git Log:

commit d209333fda99219fb68fe61c813af99bdf40d745 (HEAD -> quotes-feature)
Author: Simon Walter >
Date:   Mon Mar 30 16:40:06 2020 +0200
 
    Quote Pippin
 
commit be6d53740648d5ae6009cff90fb79644e4153628
Author: Simon Walter >
Date:   Mon Mar 30 16:38:36 2020 +0200
 
    Quote Gandalf
     
commit 9975ec210812b2bdb915f4c66a908adae7bc4d13 (master)
Author: Simon Walter >
Date:   Fri Mar 27 16:30:39 2020 +0100
 
    Some awesome LotR Quotes

 

Weitere nützliche Tricks, die ich aus diesem Workshop mitnehme

Die folgenden Tricks konnte ich thematisch nirgendwo anders unterbringen. Weshalb ich sie hier zum Schluss aufliste:

git log --oneline --all --graph

Zeigt einen aus ASCII-Zeichen erstellten Graph von den letzten Commits an.

git rebase -i [commit] -x [script]

Dieser Command ermöglicht das Erkennen von "nonobvious Conflicts", wenn sich beispielsweise die Funktionsweise einer API geändert hat. Das spezifizierte Skript wird jedes Mal ausgeführt, wenn ein Commit auf den neuen Brach abgespielt wurde. Es bietet sich also beispielsweise an, hier ein Skript laufen zu lassen, welches die Unit Tests ausführt, so weiss man genau, welcher Commit Schuld daran ist, dass die Tests nicht mehr bestanden werden.

 

Zu guter Letzt möchten wir uns ganz herzlich bei Pablo Vergés für den tollen Workshop bedanken und wünschen allen Entwickler und Entwicklerinnen weiterhin viel Spass mit Git!

Diesen und viele weitere Workshops können wir auch für externe Teams organisieren. Mehr Infos unter M&F Academy.

0
Cloud Application Monitoring at M&F
Neuer Trainee aus der Ostschweiz

Ähnliche Beiträge

 

Kommentare

Derzeit gibt es keine Kommentare. Schreibe den ersten Kommentar!
Sonntag, 19. Mai 2024

Sicherheitscode (Captcha)