🧬Git intern

Git ist im Kern ein inhaltsadressierter Schlüssel-Wert-Speicher: Du gibst Git Daten, Git gibt dir einen Schlüssel – den Hash dieser Daten. Darauf bauen vier Objekttypen auf, und darüber liegen Referenzen wie Branches und HEAD.

🧱Vier Objekttypen

📄
blob

Inhalt einer Datei – ohne Namen, ohne Rechte. Zwei gleiche Dateien = ein Blob.

📁
tree

Ein Verzeichnis: Liste aus Modus, Name und ID von Blobs bzw. Unter-Trees.

📸
commit

Schnappschuss: Wurzel-Tree + Eltern-Commits + Autor/Committer + Nachricht.

🏷
tag

Annotiertes Tag: zeigt auf ein Objekt (meist Commit), mit Tagger, Datum, Nachricht.

HEAD ─▶ refs/heads/main ─▶ commit 4ac0b51 ──parent──▶ commit ab61007
refs/tags/v1.0.0 ─▶ tag e1d0779 ─▶ commit 4ac0b51

commit 4ac0b51 ─▶ tree c8e6bcf                 commit ab61007 ─▶ tree 3b8dd6b
  .gitignore  ─▶ blob c2658d7  ◀──── gleich ────  .gitignore
  README.md   ─▶ blob (Version 2, neu)            README.md   ─▶ blob 0805455
  src-a.txt   ─▶ blob 587be6b  ◀──── gleich ────  src-a.txt
  src/        ─▶ tree d60698e  ◀──── gleich ────  src/
                  └─ app.js ─▶ blob 226344d

🔑Inhalt → SHA-1: git hash-object im Browser

Tippe einen Dateiinhalt. Die Seite baut das Blob-Objekt Byte für Byte und berechnet SHA-1 (eigene Implementierung, kein Server) – das Ergebnis ist identisch mit git hash-object.
① Git baut das Objekt: "blob " + Länge in Bytes + \0 + Inhalt
b62l6co6fb62␣20636\000h68e65l6cl6co6f\n0a

6 Bytes Inhalt (6 Zeichen – Umlaute brauchen in UTF-8 zwei Bytes) + 7 Bytes Header. Dateiname und Rechte stehen nicht im Blob – die speichert der Tree.

② SHA-1 über alle Bytes = Objekt-ID
ce013625030ba8dba906f756967f9e9ca394464a
# identisch mit echtem Git:
$ printf 'hello\n' | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
③ zlib-komprimiert gespeichert unter
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
78 9c 4b ca c9 4f 52 30 63 c8 48 cd c9 c9 e7 02 00 1d c5 04 14

13 → 21 Bytes. Die ersten zwei Bytes 78 9c sind der zlib-Header (Standardstufe). Bei sehr kleinen Objekten wird die Datei durch Header und Prüfsumme sogar größer. Andere zlib-Implementierungen können andere, aber gleichwertige Bytes erzeugen – die ID hängt nur vom unkomprimierten Inhalt ab.

④ Dasselbe Objekt in einem SHA-256-Repository
2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4

git init --object-format=sha256 – 64 statt 40 Hex-Zeichen. Laut Git-Doku gibt es derzeit keine Interoperabilität zwischen SHA-1- und SHA-256-Repositories; Standard bleibt sha1.

💡 Warum Hashes?
Gleicher Inhalt ergibt immer dieselbe ID – Git speichert ihn nur einmal. Jede Änderung, auch an einem einzigen Bit, ergibt eine völlig andere ID. Weil Commits die IDs ihrer Trees und Eltern enthalten, sichert die ID eines Commits die gesamte Geschichte davor ab (ähnlich einem Merkle-Baum).
⚠️ SHA-1 und SHA-256
2017 wurde mit „SHAttered“ die erste praktische SHA-1-Kollision veröffentlicht. Git verwendet seitdem die gehärtete Variante SHA-1DC, die solche Angriffe erkennt. Zusätzlich gibt es SHA-256-Repositories (--object-format=sha256); der Standard ist laut Git-Dokumentation weiterhin sha1.

🔍Objekt-Explorer

Zwei Commits, ein annotiertes Tag – alle Objekte mit echten IDs. Klicke ein Objekt an, um es wie mit git cat-file -p zu sehen.

Ändere ein Zeichen: neuer Blob → neuer Tree → neuer Commit → neues Tag. Alles, was gleich bleibt, wird wiederverwendet.

Tags & Commits
Trees (Verzeichnisse)
Blobs (Dateiinhalte)
$ git cat-file -t 4ac0b51
commit
$ git cat-file -s 4ac0b51
215
$ git cat-file -p 4ac0b51
tree c8e6bcf4426f27ebb16bd645e01bcb1af203fddc
parent ab610079079d544e6843bd481b91698ba4540803
author Alice <alice@example.org> 1767225660 +0100
committer Alice <alice@example.org> 1767225660 +0100

README erweitert
Gespeichert als
.git/objects/4a/c0b51a7286ce03c5e933a313716311f5a7d9c8
  • Ein Commit zeigt auf genau einen Wurzel-Tree (vollständiger Schnappschuss, kein Diff!), auf seine Eltern, und enthält Autor, Committer (mit Unix-Zeit und Zeitzone) und Nachricht.
  • Hell hervorgehoben: alles, was von diesem Objekt aus erreichbar ist.
  • Die IDs stimmen mit echtem Git überein: GIT_AUTHOR_DATE="1767225600 +0100" usw. mit git commit-tree nachgerechnet (Tests).

📂Das .git-Verzeichnis

Klicke dich durch die wichtigsten Dateien und Ordner. Die Beispielinhalte gehören zum Repository aus dem Objekt-Explorer.
.git/HEAD

HEAD – wo stehe ich?

Eine Textdatei. Normalerweise eine symbolische Referenz auf den aktuellen Branch. Im „detached HEAD“-Zustand steht hier direkt eine Commit-ID.

ref: refs/heads/main

👉Referenzen und HEAD

Branches und Tags sind nur Namen für Commit-IDs. HEAD sagt, wo du gerade stehst – normalerweise indirekt über einen Branch.
b8aa7c5Ad727a8eB1e84e1aCmainfeatureHEAD
.git/HEAD
ref: refs/heads/main
.git/refs/heads/main
1e84e1ac8ef2a6298e3b5fc250dd1a84af5d110a

HEAD ist eine symbolische Referenz auf main. Ein Commit schreibt die neue ID in refs/heads/main – HEAD selbst bleibt unverändert und „wandert“ mit.

📋Der Index

Der Index (.git/index) ist eine flache, sortierte Liste aller Dateien des nächsten Commits – nicht nur der geänderten. git add schreibt den Blob in die Objekt-Datenbank und trägt seine ID im Index ein. git commit baut aus dem Index die Trees und den Commit.

Die Spalte „Stage“ ist normalerweise 0. Während eines Merge-Konflikts stehen für eine Datei bis zu drei Einträge im Index: 1 = gemeinsame Basis, 2 = „ours“ (HEAD), 3 = „theirs“ (der hereinkommende Branch). Erst git add ersetzt sie durch einen Eintrag mit Stage 0.

$ git ls-files --stage          # normal
100644 c2658d7d1b31848c3b71960543cb0368e56cd4c7 0	.gitignore
100644 0805455a24b6c68fbc38d0fa5d121f735984285d 0	README.md

$ git ls-files --stage          # während eines Konflikts
100644 <id der Basis>   1	config.txt
100644 <id von HEAD>    2	config.txt
100644 <id von theirs>  3	config.txt

$ git show :1:config.txt        # Basis-Fassung ansehen
$ git checkout --theirs config.txt

📦Lose Objekte und Packfiles

Vorher: jede Version einzeln (zlib)       Nachher: git gc
.git/objects/                              .git/objects/pack/
├─ 08/05455a…  README v1  (voll)           ├─ pack-3f1c….pack
├─ 7e/…        README v2  (voll)           │    ├─ README v3   (voll)
├─ a1/…        README v3  (voll)           │    ├─ README v2 = Δ gegen v3
└─ …                                       │    └─ README v1 = Δ gegen v2
                                           └─ pack-3f1c….idx  (ID → Offset)

Neue Objekte landen zunächst einzeln („lose“) in .git/objects/xx/…. Bei git gc (auch automatisch) und bei jeder Übertragung per clone, fetch oder push fasst Git sie in Packfiles zusammen. Darin dürfen Objekte als Delta gegen ein ähnliches Objekt gespeichert werden – typischerweise bleibt die neueste Version vollständig, ältere werden als Differenz abgelegt.

Wichtig: Das ist nur eine Speicheroptimierung. Logisch bleibt jeder Commit ein vollständiger Schnappschuss. Nachsehen mit git count-objects -vH und git verify-pack -v.

Quellen: Pro Git, Kapitel 10 „Git Interna“ (git-scm.com/book/de/v2), git-scm.com/docs/git-hash-object, git-init, gitrepository-layout, gitignore. Hash-Werte lokal mit Git 2.55 geprüft.