Git Refs und HEAD: Wie Git Zeiger verwaltet
git
HEAD
Git · Version Control · Refs · Internals · Entwickler-Basics
Git Refs und HEAD: Wie Git Zeiger verwaltet
Branches, Tags und HEAD als bewegliche Zeiger im Commit-Graphen

Ein Commit ohne Ref ist für Git praktisch unsichtbar. Branches, Tags und HEAD sind allesamt nur benannte Zeiger, die aus einer losen Ansammlung von Objekten einen navigierbaren Verlauf machen. Dieser Artikel zeigt, wie .git/refs/heads, .git/refs/tags und packed-refs auf der Festplatte aufgebaut sind, wie HEAD in zwei Schritten zu einem Commit aufgelöst wird, und wie man Refs mit git show-ref, git symbolic-ref und git update-ref direkt inspiziert und manipuliert.

13 Min. Lesezeit Branches · Tags · HEAD · packed-refs Git 2.x · CLI · Internals

1. Warum Refs die eigentliche Landkarte des Commit-Graphen sind

Ein Commit-Objekt in Git ist für sich genommen nutzlos, sobald niemand mehr weiß, wo es sich im Verlauf befindet. Erst Refs, benannte Zeiger auf Commits, machen aus einer losen Menge von Objekten im Objektspeicher einen navigierbaren Graphen. Ohne einen Ref, der auf einen Commit-Hash zeigt, existiert dieser Commit zwar weiterhin physisch unter .git/objects, ist aber für Befehle wie git log, git branch oder git merge unerreichbar. Genau diesen Zustand nennt Git intern "unreachable" - ein Zustand, der Objekte irgendwann für die Garbage Collection freigibt.

Diese Erkenntnis dreht die übliche Anfängerperspektive um: Nicht der Commit ist die zentrale Einheit von Git, sondern der Ref, der auf ihn zeigt. Ein Branch-Name wie main ist letztlich nichts weiter als ein Etikett, das an einem bestimmten Punkt im Commit-Graphen klebt und sich bei jedem neuen Commit automatisch mitbewegt. Wer versteht, dass refs/heads/main, refs/tags/v1.0 und HEAD alle demselben einfachen Prinzip folgen, nämlich einer Datei, die einen Hash oder einen weiteren Ref-Namen enthält, hat die Grundlage für praktisch jede fortgeschrittene Git-Operation gelegt.

2. Branches: bewegliche Zeiger auf einen Commit

Ein Branch ist technisch nichts anderes als eine Textdatei unter .git/refs/heads/, deren gesamter Inhalt aus einem einzigen 40 Zeichen langen SHA-1-Hash (oder 64 Zeichen bei SHA-256-Repositories) besteht, gefolgt von einem Zeilenumbruch. Eine Datei namens .git/refs/heads/main enthält also wortwörtlich nur den Hash des aktuellen Commits auf diesem Branch, sonst nichts. Das macht Branches in Git außergewöhnlich billig: git branch feature-x erstellt lediglich eine wenige Bytes große Datei mit dem aktuellen Commit-Hash als Inhalt, ohne den Objektspeicher überhaupt anzufassen.

Der entscheidende Mechanismus: Sobald ein neuer Commit erstellt wird, während HEAD auf einen Branch zeigt, aktualisiert Git automatisch genau diese eine Datei mit dem Hash des neuen Commits. Der Branch-Zeiger bewegt sich also mit, während ältere Commits über die Eltern-Referenz im Commit-Objekt weiterhin erreichbar bleiben. Diese Einfachheit der Implementierung ist der eigentliche Grund, warum Branching und Merging in Git so schnell sind, im Vergleich zu älteren Versionskontrollsystemen, die für einen Branch oft eine komplette Kopie des Verzeichnisbaums anlegen mussten. Auch das Tracking-Verhalten eines Branches gegenüber einem Remote wird nicht im Ref selbst gespeichert, sondern separat in der Konfigurationsdatei.


# .git/config: tracking configuration ties a local branch to a ref
[branch "main"]
	remote = origin
	merge = refs/heads/main

[branch "feature-x"]
	remote = origin
	merge = refs/heads/feature-x
	rebase = true

[remote "origin"]
	url = git@github.com:example/shop.git
	fetch = +refs/heads/*:refs/remotes/origin/*

3. Tags: unveränderliche Referenzen für Meilensteine

Tags markieren einen Commit dauerhaft und sollen sich, anders als Branches, nach dem Erstellen nicht mehr bewegen. Git unterscheidet dabei zwei grundverschiedene Typen. Ein lightweight Tag ist strukturell identisch mit einem Branch: eine Datei unter .git/refs/tags/, die direkt den Hash eines Commits enthält. git tag v1.0.0 ohne weitere Optionen erzeugt genau das, eine simple Namensreferenz ohne zusätzliche Metadaten.

Ein annotated Tag dagegen, erzeugt mit git tag -a v1.0.0 -m "Release 1.0.0", legt ein eigenständiges Tag-Objekt im Objektspeicher an, das Tagger-Name, E-Mail, Zeitstempel, eine Nachricht und optional eine GPG-Signatur enthält. Die Datei unter .git/refs/tags/v1.0.0 zeigt in diesem Fall nicht direkt auf den Commit, sondern auf den Hash dieses Tag-Objekts, welches wiederum auf den Commit verweist, ein zusätzlicher Zeigerschritt. Für Releases sind annotated Tags deshalb die empfohlene Wahl, weil sie signierbar sind und eine nachvollziehbare Historie liefern, während lightweight Tags eher für private, temporäre Markierungen taugen.

HEAD ist selbst kein Branch und kein Commit, sondern ein Zeiger auf einen Zeiger. Im Normalfall enthält die Datei .git/HEAD nicht etwa einen Hash, sondern die Zeile ref: refs/heads/main, eine sogenannte symbolische Referenz. Um vom aktuellen HEAD zum eigentlichen Commit zu gelangen, muss Git also zwei Schritte auflösen: zuerst HEAD zu refs/heads/main, dann diese Datei zu ihrem Inhalt, dem eigentlichen Commit-Hash. Dieser doppelte Indirektionsschritt ist der Grund, warum ein Commit den aktuellen Branch automatisch mitbewegt, ohne dass HEAD selbst verändert werden muss.

Dieses Zwei-Ebenen-Design ist bewusst gewählt. Würde HEAD stattdessen direkt einen Commit-Hash enthalten, müsste jeder Commit zwei Dateien aktualisieren, HEAD und den Branch. Stattdessen genügt es, ausschließlich refs/heads/main zu aktualisieren, während HEAD unverändert auf den symbolischen Namen zeigt. Befehle wie git switch main oder das ältere git checkout main tun im Kern nichts anderes, als den Inhalt von .git/HEAD auf ref: refs/heads/main umzuschreiben und anschließend Working Directory sowie Index an den neuen Zielcommit anzupassen.


$ git show-ref --heads --tags
a1b2c3d4e5f6789012345678901234567890abcd refs/heads/main
b2c3d4e5f6789012345678901234567890abcde1 refs/heads/feature-x
c3d4e5f6789012345678901234567890abcdef12 refs/tags/v1.0.0
d4e5f6789012345678901234567890abcdef1234 refs/tags/v1.1.0

$ cat .git/HEAD
ref: refs/heads/main

# HEAD is a symbolic reference, not a hash: Git resolves it in two hops
# HEAD -> refs/heads/main -> a1b2c3d4e5f6789012345678901234567890abcd

5. Detached HEAD: wenn HEAD direkt auf einen Commit zeigt

Checkt man stattdessen direkt einen Commit-Hash oder einen Tag aus, etwa mit git checkout a1b2c3d, schreibt Git keinen symbolischen Verweis mehr in HEAD, sondern direkt den Commit-Hash selbst. Dieser Zustand heißt detached HEAD, weil HEAD von jedem Branch abgekoppelt ist. Commits sind in diesem Zustand weiterhin technisch möglich, sie erhalten reguläre Hashes und landen im Objektspeicher, allerdings zeigt kein Branch-Zeiger auf sie.

Das eigentliche Risiko entsteht beim nächsten Branch-Wechsel: Ohne einen Ref, der auf die neuen Commits zeigt, werden sie für Standardbefehle wie git log unsichtbar und laufen Gefahr, nach 30 bis 90 Tagen (dem Standardwert von gc.reflogExpire) durch die Garbage Collection entfernt zu werden. Das Sicherheitsnetz dagegen ist die Reflog: Sie protokolliert jede Bewegung von HEAD lokal, sodass sich verlorene Commits mit git reflog und einem anschließenden git switch -c rescue-branch <hash> fast immer wiederherstellen lassen, solange die Reflog-Frist noch nicht abgelaufen ist.


$ git checkout a1b2c3d
Note: switching to 'a1b2c3d'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

HEAD is now at a1b2c3d Fix checkout redirect for guest customers

$ git log --oneline -3
a1b2c3d (HEAD detached at a1b2c3d) Fix checkout redirect for guest customers
9f8e7d6 Add wishlist AJAX handler
7c6b5a4 Update Hyva_Theme layout for sticky header

# Commit while detached: valid object, but no branch points to it yet
$ echo "fix" >> Checkout.php
$ git commit -am "Hotfix: correct redirect URL"
[detached HEAD c5d6e7f] Hotfix: correct redirect URL

# Rescue the work before switching branches and losing the pointer
$ git switch -c hotfix/checkout-redirect
Switched to a new branch 'hotfix/checkout-redirect'

6. .git/refs auf der Festplatte: Verzeichnisstruktur und Dateiformat

Physisch ist .git/refs ein simples Verzeichnis mit zwei Hauptunterordnern, heads/ für Branches und tags/ für Tags, ergänzt um remotes/ für Remote-Tracking-Branches. Jede einzelne Referenz entspricht exakt einer Datei, deren Pfad relativ zu .git/refs dem vollständigen Ref-Namen entspricht: Ein Branch feature/checkout-redesign liegt unter .git/refs/heads/feature/checkout-redesign, wobei der Schrägstrich im Namen sogar ein echtes Unterverzeichnis erzeugt. Der Dateiinhalt ist bewusst minimal, ein einzelner Hash als Text, ohne Binärformat und ohne zusätzliche Metadaten.

Dieses Design ist elegant für kleine Repositories, skaliert aber schlecht mit wachsender Anzahl an Refs. Ein Repository mit zehntausend Branches oder Tags erzeugt zehntausend einzelne, winzige Dateien im Dateisystem, was Dateisystemoperationen wie das rekursive Auflisten aller Refs spürbar verlangsamt, insbesondere auf Netzwerklaufwerken oder in Docker-Volumes mit hohem I/O-Overhead pro Datei. Genau dieses Skalierungsproblem war der ursprüngliche Anlass für die Einführung von packed-refs.

7. packed-refs: Refs komprimieren für große Repositories

Die Datei .git/packed-refs löst das Skalierungsproblem, indem sie viele einzelne lose Refs (loose refs) in einer einzigen Textdatei zusammenfasst, ein Hash und ein Ref-Name pro Zeile. Der Befehl git pack-refs --all durchsucht .git/refs, schreibt alle gefundenen Referenzen in .git/packed-refs und entfernt anschließend die ursprünglichen Einzeldateien. Annotated Tags erhalten dabei zusätzlich eine Zeile mit vorangestelltem Karat-Zeichen, die auf den tatsächlichen Commit hinter dem Tag-Objekt verweist, damit Werkzeuge diesen ohne zusätzliche Objektauflösung finden.

Entscheidend für das Verständnis ist die Koexistenz beider Mechanismen: Ein loser Ref unter .git/refs überschreibt immer den gleichnamigen Eintrag in packed-refs, niemals umgekehrt. Wird ein bereits gepackter Branch aktualisiert, etwa durch einen neuen Commit, schreibt Git wieder eine lose Datei unter .git/refs/heads/, die den veralteten Eintrag in packed-refs faktisch überschattet, ohne die gepackte Datei selbst sofort zu bereinigen. Erst ein erneutes git pack-refs räumt diese Überschneidung wieder auf. git clone und git gc rufen pack-refs routinemäßig automatisch auf, sodass die meisten Entwickler diesen Mechanismus nie manuell anstoßen müssen.

8. Refs direkt inspizieren: show-ref, symbolic-ref und update-ref

git show-ref listet standardmäßig alle lokalen Refs mit ihrem vollständigen Hash und Namen auf, unabhängig davon, ob sie lose oder gepackt vorliegen, Git behandelt beide Quellen transparent gleich. Mit den Flags --heads oder --tags lässt sich die Ausgabe gezielt auf Branches beziehungsweise Tags einschränken, ein optionales Musterargument filtert zusätzlich nach Namen, etwa git show-ref --heads 'feature/*'. git symbolic-ref HEAD liest den rohen Inhalt der symbolischen Referenz aus, während git symbolic-ref HEAD refs/heads/main sie direkt setzt, exakt der Schritt, den git switch intern ausführt.

Für Automatisierung und Skripte ist git update-ref das richtige Werkzeug, weil es Refs transaktional und ohne Umweg über einen Checkout erstellt, verschiebt oder löscht, ohne Working Directory oder Index anzufassen. Ein CI-Skript kann so beispielsweise git update-ref refs/heads/deploy-snapshot HEAD ausführen, um einen Zustand als Branch zu fixieren, ohne dass ein Entwickler jemals dorthin wechselt. Zum sicheren Löschen sollte grundsätzlich git update-ref -d refs/heads/alter-branch verwendet werden, nicht das manuelle Entfernen der Datei, weil update-ref den erwarteten alten Wert prüfen und Race Conditions bei gleichzeitigem Zugriff vermeiden kann.


# Read the raw symbolic reference stored in .git/HEAD
$ git symbolic-ref HEAD
refs/heads/main

# Write it directly: this is what "git switch main" does internally
$ git symbolic-ref HEAD refs/heads/main

# Attempting this while HEAD is detached fails, because HEAD then
# holds a raw commit hash, not a symbolic reference
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref

# Create a branch pointer without checking it out
$ git update-ref refs/heads/feature-x a1b2c3d4e5f6789012345678901234567890abcd
$ git show-ref --heads feature-x
a1b2c3d4e5f6789012345678901234567890abcd refs/heads/feature-x

# Delete a ref safely: update-ref checks the current value first
$ git update-ref -d refs/heads/feature-x

# Force-move an existing branch to a specific commit, scripted
$ git update-ref refs/heads/main c5d6e7f8901234567890abcdef1234567890abc

9. Ref-Typen im direkten Vergleich

Jeder in diesem Artikel behandelte Ref-Typ hat einen eigenen Speicherort, ein eigenes Beweglichkeitsverhalten und einen typischen Einsatzzweck im Entwickleralltag. Die folgende Tabelle fasst die Unterschiede zusammen und löst die häufigste Verwechslung auf, nämlich dass alle Refs gleich behandelt werden könnten.

Ref-Typ Speicherort Beweglich? Typischer Einsatz
Branch .git/refs/heads/<name> Ja, bei jedem Commit Aktive Feature- und Release-Entwicklung
Lightweight Tag .git/refs/tags/<name> Ja, aber nur mit -f Schnelle, private Markierungen ohne Metadaten
Annotated Tag .git/refs/tags/<name> + Tag-Objekt Nein, konventionell fix Offizielle Releases mit Signatur und Message
HEAD (attached) .git/HEAD (symbolische Referenz) Ja, folgt dem Branch Normaler Arbeitszustand auf einem Branch
HEAD (detached) .git/HEAD (roher Commit-Hash) Ja, aber ohne Sicherung Historie inspizieren, Bisect, CI-Checkouts

Wer diese Unterschiede verinnerlicht hat, kann jede Fehlermeldung rund um Refs, etwa "not a valid ref" oder "ambiguous refname", gezielt einordnen, statt sie als kryptische Zufälle hinzunehmen.

Mironsoft

Git-Workflows, Repository-Hygiene und CI/CD-Pipelines für PHP- und Magento-Teams

Refs, Branches und Reflog sicher im Griff?

Wir helfen Entwicklerteams, Branching-Strategien, Release-Tagging und Skript-Automatisierung rund um Git-Refs sauber und nachvollziehbar aufzusetzen, ohne verlorene Commits und ohne wilde Ref-Verwaltung.

Git-Schulung

Praxisnahe Workshops zu Refs, HEAD, Reflog und sicherer Ref-Manipulation

Release-Tagging-Setup

Konsistente, signierte Tags und Branch-Konventionen im Team etablieren

CI/CD-Integration

Ref-basierte Deploy-Pipelines mit git update-ref und Automatisierung aufsetzen

10. Zusammenfassung

Refs sind das eigentliche Fundament, das einen Haufen loser Commit-Objekte in einen navigierbaren Verlauf verwandelt. Branches sind bewegliche Zeiger unter .git/refs/heads/, die sich bei jedem Commit automatisch mitbewegen. Tags markieren Meilensteine dauerhaft, wobei annotated Tags zusätzlich ein eigenes, signierbares Objekt im Repository anlegen. HEAD selbst ist ein Zeiger auf einen Zeiger, meist eine symbolische Referenz auf den aktuellen Branch, die in zwei Schritten zu einem Commit aufgelöst wird.

Auf der Festplatte beginnt jeder Ref als einzelne, winzige Datei unter .git/refs, wird aber bei wachsender Anzahl über packed-refs zu einer einzigen Datei zusammengefasst, wobei lose Refs stets Vorrang vor gepackten Einträgen behalten. Mit git show-ref, git symbolic-ref und git update-ref lassen sich all diese Mechanismen direkt inspizieren und sogar skriptbar manipulieren, ohne den Umweg über einen Checkout zu gehen.

Git Refs und HEAD: Das Wichtigste auf einen Blick

Branches

Bewegliche Zeiger unter .git/refs/heads/, die einen einzelnen Commit-Hash als Textinhalt speichern.

Tags

Lightweight zeigt direkt auf einen Commit, annotated legt ein eigenes signierbares Objekt an.

HEAD

ref: refs/heads/main in .git/HEAD, aufgelöst in zwei Schritten zu einem Commit.

packed-refs

Fasst viele lose Refs in einer Datei zusammen, lose Refs überschreiben stets gepackte Einträge.

11. FAQ: Git Refs und HEAD

1Was ist ein Ref in Git genau?
Ein benannter Zeiger auf einen Commit oder ein anderes Objekt, meist eine Textdatei unter .git/refs mit einem Hash als Inhalt. Branches, Tags und HEAD sind alles Varianten davon.
2Was ist der Unterschied zwischen einem Branch und einem Tag?
Ein Branch bewegt sich automatisch mit jedem Commit, ein Tag soll fest bleiben. Ein annotated Tag legt zusätzlich ein eigenes Objekt mit Metadaten an.
3Lightweight vs. annotated Tag?
Lightweight zeigt direkt auf einen Commit ohne Metadaten. Annotated legt ein eigenes Tag-Objekt mit Tagger, Zeitstempel, Nachricht und optionaler Signatur an.
4Was steht genau in .git/HEAD?
Normalerweise ref: refs/heads/main, eine symbolische Referenz. Im detached HEAD Zustand steht dort stattdessen ein roher Commit-Hash.
5Ist detached HEAD gefährlich?
HEAD zeigt direkt auf einen Commit statt auf einen Branch. Commits bleiben möglich, sind aber ohne Branch-Zeiger schwer wiederzufinden. Die Reflog dient als Sicherheitsnetz.
6Wie finde ich verlorene Commits wieder?
git reflog zeigt frühere HEAD-Positionen. Mit git switch -c rescue-branch <hash> lässt sich ein verlorener Commit an einen neuen Branch anhängen.
7Was ist packed-refs?
Eine einzelne Datei, die viele lose Refs zu Performance-Zwecken zusammenfasst. git pack-refs --all erstellt sie manuell, git gc und git clone rufen sie automatisch auf.
8Loser Ref und packed-refs gleichzeitig?
Der lose Ref hat immer Vorrang vor dem gleichnamigen Eintrag in packed-refs. Das passiert typischerweise nach einem neuen Commit auf einem bereits gepackten Branch.
9Wofür wird git update-ref verwendet?
Erstellt, verschiebt oder löscht Refs direkt und transaktional, ohne Checkout. Besonders nützlich für Skripte und CI-Pipelines.
10Wie zeige ich alle Refs mit Hash an?
git show-ref listet alle lokalen Refs auf. --heads oder --tags schränken auf Branches oder Tags ein, ein Musterargument filtert zusätzlich nach Namen.