Git refs: An overview
In diesem Tutorial mit vermitteln wir dir anhand eines Weltraumszenarios die Grundlagen von Git.
Wenn du die vielen Methoden zur Referenzierung eines Commits kennst, gewinnst du einiges mehr an Möglichkeiten bei der Nutzung dieser Befehle. In diesem Kapitel beleuchten wir häufig genutzte Befehle wie git checkout
, git branch
und git push
genauer, indem wir du vielen Methoden zur Referenzierung eines Commits durchgehen.
Außerdem lernen wir, wie anscheinend "verlorene" Commits wiederbelebt werden, indem man über den Reflog-Mechanismus von Git auf sie zugreift.
Hashes
Die direkteste Methode zur Referenzierung eines Commits führt über seinen SHA-1-Hash. Dieser fungiert als unverwechselbare ID für jeden Commit. Den Hash eines Commits findest du in der Ausgabe von git log
.
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message
Wenn du einen Commit in einem Git-Befehl angibst, müssen lediglich ausreichend Zeichen vorhanden sein, um den Commit eindeutig zu identifizieren. Den obigen Commit könntest du z. B. mit git show
untersuchen, indem du den folgenden Befehl ausführst:
git show 0c708f
Manchmal muss ein Branch, Tag oder eine andere indirekte Referenz in den entsprechenden Commit-Hash umgewandelt werden. Hierfür kannst du den Befehl git rev-parse
verwenden. Der folgende Befehl gibt den Hash des Commits zurück, auf den der main-
Branch verweist:
Zugehöriges Material
Verschieben eines vollständigen Git-Repositorys
Lösung anzeigen
Git kennenlernen mit Bitbucket Cloud
git rev-parse main
Dies ist besonders hilfreich beim Schreiben benutzerdefinierter Skripte, die eine Commit-Referenz akzeptieren. Statt die Commit-Referenz manuell zu parsen, kann git rev-parse
die Eingabe für dich normalisieren.
Referenzen
Eine Referenz bzw. ref ist eine indirekte Methode zur Referenzierung eines Commits. Du kannst sie als einen benutzerfreundlichen Alias für einen Commit-Hash betrachten. Sie sind der interne Mechanismus von Git zur Repräsentierung von Branches und Tags.
Refs werden als normale Textdateien im .git/refs
-Verzeichnis gespeichert, in dem .git
normalerweise .git
heißt. Zum Durchsuchen der Refs in einem deiner Repositorys gehst du zu .git/refs
. Dort solltest du die folgende Struktur sehen, wobei natürlich je nach den in deinem Repo vorhandenen Branches, Tags und Remotes andere Dateien enthalten sein werden.
.git/refs/ heads/ main some-feature remotes/ origin/ main tags/ v0.9
Das Verzeichnis heads
definiert alle lokalen Branches in deinem Repository. Jeder Dateiname entspricht dem Namen des jeweiligen Branches. In der Datei selbst gibt es einen Commit-Hash. Dieser Commit-Hash gibt an, wo sich die Branch-Spitze befindet. Das kannst du überprüfen, indem du die folgenden beiden Befehle aus dem Root-Verzeichnis des Git-Repositorys ausführst:
# Output the contents of `refs/heads/main` file: cat .git/refs/heads/main # Inspect the commit at the tip of the `main` branch: git log -1 main
Der Commit-Hash, der vom Befehl cat
zurückgegeben wird, sollte mit der Commit-ID übereinstimmen, die mit git log
angezeigt werden kann.
Um den Ort des main
-Branch zu ändern, muss Git dies lediglich in der refs/heads/main
-Datei ändern. Genauso lässt sich ein neuer Branch einfach erstellen, indem ein Commit-Hash in eine neue Datei eingetragen wird. Dies ist einer der Gründe, warum Git-Branches so viel schlanker sind als das SVN-Modell.
Das tags
-Verzeichnis funktioniert auf dieselbe Weise, enthält jedoch Tags statt Branches. Das remotes
-Verzeichnis listet alle Remote-Repositorys, die du mit git remote
erstellt hast, als separate Unterverzeichnisse auf. In jedem einzelnen findest du alle Remote Branches, die du in dein Repository gezogen hast.
Specifying refs
Wenn du einem Git-Befehl eine Referenz anfügst, kannst du entweder den vollen Namen der Referenz oder einen Kurznamen verwenden, anhand dessen Git nach einer übereinstimmenden Referenz sucht. Du solltest bereits mit Kurznamen für Referenzen vertraut sein, da du diese jedes Mal verwendest, wenn du einen Branch beim Namen nennst.
git show some-feature
Das Argument some-feature
im obigen Befehl ist eigentlich ein Kurzname für den Branch. Git wandelt dies vor der Verwendung in refs/heads/some-feature
um. Du kannst in der Befehlszeile auch die komplette Ref angeben:
git show refs/heads/some-feature
Auf diese Weise werden Unklarheiten bezüglich des Orts der Ref vermieden. Dies ist z. B. dann erforderlich, wenn sowohl ein Branch und ein Tag namens some-feature
vorhanden sind. Sofern du dich jedoch an die korrekten Benennungskonventionen hältst, sollten eigentlich keine Zweideutigkeiten aufkommen.
Wir werden im Abschnitt zu den Refspecs noch weitere vollständige Referenznamen sehen.
Packed refs
In großen Repositorys entfernt Git im Rahmen einer Speicherbereinigung in regelmäßigen Abständen überflüssige Objekte und komprimiert Referenzen in eine einzige Datei für eine Verbesserung der Performance. Diese Komprimierung während der Bereinigung kannst du mit dem folgenden Befehl erzwingen:
git gc
Hiermit werden alle Branch- und Tag-Dateien im refs
-Ordner in eine einzige Datei namens packed-refs
verschoben. Diese befindet sich ganz oben im .git
-Verzeichnis. Wenn du diese Datei öffnest, wird dir eine Zuordnung von Commit-Hashes zu Referenzen angezeigt.
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/main bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
Äußerlich wirkt sich dies überhaupt nicht auf die normale Git-Funktionalität aus. Aber wenn du dich gewundert hast, warum dein .git/refs
-Ordner leer ist, weißt du jetzt, wo deine Referenzen sich nun befinden.
Special refs
Neben dem refs
-Verzeichnis gibt es noch einige spezielle Refs, die sich im .git
-Verzeichnis der obersten Ebene befinden. Diese sind im Folgenden aufgelistet:
HEAD
: Der aktuell ausgecheckte Commit/BranchFETCH_HEAD
: Der letzte von einem Remote-Repo abgerufene BranchORIG_HEAD
: Eine Backup-Referenz für denHEAD
, bevor drastische Änderungen daran vorgenommen werdenMERGE_HEAD
: Die Commits, die du mitgit merge
in den aktuellen Branch mergstCHERRY_PICK_HEAD
: Ein gezielt ausgewählter Commit
Diese Referenzen werden alle bei Bedarf von Git erstellt und aktualisiert. Der Befehl git pull
führt z. B. zuerst git fetch
aus, wodurch die FETCH_HEAD
-Referenz aktualisiert wird. Anschließend wird git merge FETCH_HEAD
ausgeführt, um den Pull der abgerufenen Branches in das Repository abzuschließen. Natürlich kannst du diese speziellen Refs wie alle anderen Refs auch verwenden, genauso wie du dies sicherlich auch mit HEAD
gemacht hast.
Diese Dateien enthalten je nach Dateityp und Status in deinem Repository unterschiedliche Inhalte. Die HEAD
-Ref kann entweder eine symbolische Ref, die einfach eine Referenz zu einer anderen Ref anstatt von einem Commit-Hash ist, oder einen Commit-Hash enthalten. Wirf z. B. einmal einen Blick auf die Inhalte von HEAD
, wenn du dich im main
-Branch befindest:
git checkout main cat .git/HEAD
This will output ref: refs/heads/main
, which means that HEAD
points to the refs/heads/main
ref. This is how Git knows that the main
branch is currently checked out. If you were to switch to another branch, the contents of HEAD
would be updated to reflect the new branch. But, if you were to check out a commit instead of a branch, HEAD
would contain a commit hash instead of a symbolic ref. This is how Git knows that it’s in a detached HEAD state.
Die meiste Zeit wird HEAD
die einzige Referenz sein, die du direkt verwendest. Die anderen sind in der Regel nur dann von Nutzen, wenn du Low-Level-Skripte schreibst, die einen Zugriff auf die tieferliegende Funktionsweise von Git benötigen.
Refspecs
Eine Refspec ordnet einen Branch im lokalen Repository einem Branch in einem Remote-Repository zu. Dies ermöglicht das Management von Remote Branches mit Git-Befehlen und die Konfiguration von erweitertem git push
- und git fetch
-Verhalten.
Eine Refspec wird mit [+]
<src>
:
<dst>
angegeben. Der Parameter <src>
steht für den Quell-Branch im lokalen Repository und der Parameter <dst>
steht für den Ziel-Branch im Remote-Repository. Das optionale +
-Zeichen zwingt das Remote-Repository zu einem Nicht-Fast-Forward-Update.
Refspecs können zusammen mit git push
dazu genutzt werden, dem Remote-Branch einen anderen Namen zu geben. Der folgende Befehl pusht z. B. den main
-Branch zum origin
-Remote-Repo, genauso wie dies mit einem einfachen git push
-Befehl geschehen würde, aber in diesem Fall wird qa-main
als Branch-Name für den Branch im origin
-Repository verwendet. Dies ist für QS-Teams, die ihre eigenen Branches in ein Remote-Repository pushen müssen, hilfreich.
git push origin main:refs/heads/qa-main
You can also use refspecs for deleting remote branches. This is a common situation for feature-branch workflows that push the feature branches to a remote repo (e.g., for backup purposes). The remote feature branches still reside in the remote repo after they are deleted from the local repo, so you get a build-up of dead feature branches as your project progresses. You can delete them by pushing a refspec that has an empty parameter, like so:
git push origin :some-feature
Das ist äußerst praktisch, da du dich nicht in dein Remote-Repository einloggen und den Remote Branch manuell löschen musst. Hinweis: Ab Git v1.7.0 kannst du die Option --delete
statt der oben beschriebenen Methode anwenden. Der folgende Befehl hat denselben Effekt wie der obige:
git push origin --delete some-feature
Du kannst das Verhalten von git fetch
mit Refspecs ändern, indem du deiner Git-Konfigurationsdatei ein paar Zeilen hinzufügst. In den Standardeinstellungen ruft git fetch
alle Branches im Remote-Repository ab. Dies geschieht aufgrund des folgenden Abschnitts in der .git/config
-Datei:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/*
Die fetch
-Zeile weist git fetch
an, alle Branches vom origin
-Repository herunterzuladen. Aber manche Workflows benötigen nicht alle Branches. In vielen Continuous-Integration-Workflows ist beispielsweise nur der main
-Branch von Bedeutung. Um nur den main
-Branch abzurufen, änderst du die fetch
-Zeile folgendermaßen:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main
Du kannst auch git push
auf ähnliche Weise konfigurieren. Wenn du z. B. den main
-Branch zum qa-main
im origin
-Remote pushen möchtest (wie wir dies oben gemacht haben), änderst du die Konfigurationsdatei folgendermaßen:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main push = refs/heads/main:refs/heads/qa-main
Mithilfe der Refspecs erhältst du die absolute Kontrolle darüber, wie die verschiedenen Git-Befehle Branches zwischen Repositorys übertragen. Über sie kannst du Branches umbenennen und von deinem lokalen Repository löschen, Branches mit anderen Namen abrufen/pushen und git push
sowie git fetch
so konfigurieren, dass die Befehle nur auf die gewünschten Branches angewendet werden.
Relative refs
Du kannst auch Commits im Verhältnis zu einem anderen Commit referenzieren. Mit dem Zeichen ~
erreichst du Parent-Commits. Folgendermaßen wird beispielsweise der Grandparent von HEAD
angezeigt:
git show HEAD~2
Doch wenn du mit Merge-Commits arbeitest, wird es ein wenig komplizierter. Da Merge-Commits mehr als einen Parent haben, kannst du auch mehrere Pfaden folgen. Bei einem 3-Wege-Merge stammt der erste Parent von dem Branch, in dem du dich befandst, als du den Merge durchgeführt hast, und der zweite Parent stammt von dem Branch, den du in deinem git merge
-Befehl angegeben hast.
Das Zeichen ~
folgt immer dem ersten Parent eines Merge-Commits. Wenn du dem Pfad eines anderen Parents folgen möchtest, musst du diesen mit dem Zeichen ^
bestimmen. Ist beispielsweise HEAD
ein Merge-Commit, gibt Folgendes den zweiten Parent von HEAD
zurück.
git show HEAD^2
Du kannst das Zeichen ^
mehrmals verwenden, wenn du mehr als eine Generation zurückgehen möchtest. Beim folgenden Beispiel wir der Grandparent vom HEAD
(angenommen, es handelt sich um einen Merge-Commit), der sich im zweiten Parent befindet, angezeigt.
git show HEAD^2^1
Um zu verdeutlichen, wie ~
und ^
funktionieren, ist in der folgenden Abbildung dargestellt, wie ein Commit von A
aus mit relativen Referenzen erreicht wird. In manchen Fällen gibt es mehrere Wege zum Erreichen eines Commits.
Relative Referenzen können mit demselben Befehl wie eine normale Referenz verwendet werden. Die folgenden Befehle nutzen z. B. alle eine relative Referenz:
# Only list commits that are parent of the second parent of a merge commit git log HEAD^2 # Remove the last 3 commits from the current branch git reset HEAD~3 # Interactively rebase the last 3 commits on the current branch git rebase -i HEAD~3
The reflog
Der Reflog ist das Sicherheitsnetz von Git. Er zeichnet fast jede Änderung auf, die du in deinem Repository vornimmst – unabhängig davon, ob du einen Snapshot committet hast oder nicht. Er ist im Prinzip ein chronologischer Verlauf aller Aktionen, die du in deinem lokalen Repository durchgeführt hast. Um den Reflog anzuzeigen, führst du den Befehl git reflog
aus. Die Ausgabe sollte in etwa folgendermaßen aussehen:
400e4b7 HEAD@{0}: checkout: moving from main to HEAD~2 0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `main` 00f5425 HEAD@{2}: commit (merge): Merge branch ';feature'; ad8621a HEAD@{3}: commit: Finish the feature
Dies kann folgendermaßen übersetzt werden:
- Du hast soeben
HEAD~2
ausgecheckt. - Davor hast du eine Commit-Nachricht geändert.
- Davor hast du den
feature
-Branch in denmain
-Branch gemergt. - Davor hast du einen Snapshot committet.
Über die Syntax HEAD{
kannst du im Reflog gespeicherte Commits referenzieren. Dies funktioniert so ähnlich wie mit den HEAD~
-Referenzen aus dem vorigen Abschnitt, aber
bezieht sich hier auf einen Eintrag im Reflog anstatt auf den Commit-Verlauf.
Du kannst auf diese Weise einen Commit auf einen andernfalls verloren gegangenen Status zurücksetzen. Nehmen wir an, du hast gerade ein neues Feature mit git reset
verworfen. Dein Reflog könnte in etwa so aussehen:
ad8621a HEAD@{0}: reset: moving to HEAD~3 298eb9f HEAD@{1}: commit: Some other commit message bbe9012 HEAD@{2}: commit: Continue the feature 9cb79fa HEAD@{3}: commit: Start a new feature
Die drei Commits vor dem git reset
sind nun defekt, d. h. sie können nicht referenziert werden – außer über das Reflog. Nehmen wir an, du stellst fest, dass du nicht deine ganze Arbeit hättest verwerfen sollen. In diesem Fall musst du lediglich den HEAD@{1}
-Commit auschecken, um zurück zu dem Zustand deines Repositorys vor dem Ausführen von git reset
zu gelangen.
git checkout HEAD@{1}
Dadurch wechselst du in einen Zustand mit losgelöstem HEAD
. Von hier aus kannst du einen neuen Branch erstellen und an deinem Feature weiterarbeiten.
Zusammenfassung
Nun solltest du in der Lage sein, problemlos Commits in einem Git-Repository referenzieren. Wir haben gelernt, wie Branches und Tags als Referenzen im .git
-Unterverzeichnis gespeichert werden, wie eine packed-refs
-Datei gelesen wird, wie HEAD
dargestellt wird, wie Refspecs für erweiterte Push- und Fetch-Vorgänge genutzt wird und wie mit den relativen Operatoren ~
und ^
eine Branch-Hierarchie durchkämmt werden kann.
Außerdem haben wir uns das Reflog angesehen, das zur Referenzierung von Commits dient, die durch andere Mittel nicht verfügbar sind. Dies ist eine hervorragende Möglichkeit zur Wiederherstellung in Situationen, in denen wir denken: "Ups, das hätte ich besser nicht machen sollen."
Der Zweck dieser Übung war, zu lernen, wie du in jedem erdenklichen Entwicklungsszenario genau den Commit auswählst, den du benötigst. Diese Kenntnisse kannst du leicht mit deinem bereits vorhandenen Wissen zu Git kombinieren, da die gebräuchlichsten Befehle Refs als Argument akzeptieren. Hierzu zählen git log
, git show
, git checkout
, git reset
, git revert
, git rebase
und noch viele andere Befehle.
Diesen Artikel teilen
Nächstes Thema
Lesenswert
Füge diese Ressourcen deinen Lesezeichen hinzu, um mehr über DevOps-Teams und fortlaufende Updates zu DevOps bei Atlassian zu erfahren.