Git als inhaltsadressierte Datenbank đ
Git unterscheidet sich fundamental von anderen Versionskontrollsystemen durch seine Architektur als inhaltsadressierte Datenbank (content-addressable storage). Dieses Konzept ist der SchlĂŒssel, um zu verstehen, warum Git so effizient, zuverlĂ€ssig und schnell ist â und warum bestimmte Operationen ĂŒberhaupt möglich sind.
Was âinhaltsadressiert" bedeutet
Bei einer inhaltsadressierten Datenbank wird der Speicherort eines Objekts nicht durch einen Namen, einen Pfad oder eine fortlaufende Nummer bestimmt, sondern direkt durch den Inhalt selbst. Genauer gesagt: Git berechnet aus dem Inhalt einer Datei (oder eines anderen Objekts) einen eindeutigen kryptografischen Hash und verwendet diesen Hash als âAdresse" oder Identifikator.
Das steht im Gegensatz zu herkömmlichen Dateisystemen:
| Eigenschaft | Klassisches Dateisystem | Git (inhaltsadressiert) |
|---|---|---|
| Identifikation | Pfad und Dateiname | Hash des Inhalts |
| Umbenennung | Ăndert den Identifikator | Hash bleibt gleich |
| Gleicher Inhalt, zwei Orte | Zwei separate Speicherungen | Nur einmal gespeichert |
| Inhalt geÀndert | Gleicher Name, neuer Inhalt | Neuer Hash = neues Objekt |
Der entscheidende Gedanke ist: Der Inhalt ist die Adresse. Wenn du den Inhalt kennst, kennst du auch, wo und wie er gespeichert ist. Und umgekehrt: Wenn du den Hash hast, weiĂt du exakt, welcher Inhalt dahintersteckt.
Das SHA-1-Hash-System im Detail
Was ist ein Hash?
Ein Hash (auch âPrĂŒfsumme" oder âFingerabdruck") ist das Ergebnis einer mathematischen Funktion, die eine beliebig groĂe Eingabe auf eine Ausgabe fester LĂ€nge abbildet. Git verwendet dafĂŒr den SHA-1-Algorithmus (Secure Hash Algorithm 1), der einen 160-Bit-Hash erzeugt, dargestellt als 40-stellige Hexadezimalzahl.
Beispiel eines Git-Hash:
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Eigenschaften von SHA-1
SHA-1 hat mehrere wichtige Eigenschaften, die es fĂŒr Git ideal machen:
-
Deterministisch: Derselbe Inhalt erzeugt immer denselben Hash â auf jedem Computer, zu jeder Zeit, in jedem Repository weltweit.
-
Kollisionsresistent: Es ist praktisch unmöglich, zwei verschiedene Inhalte zu finden, die denselben Hash erzeugen. Bei 160 Bit gibt es $2^{160}$ mögliche Hashes, das sind etwa $1{,}46 \times 10^{48}$ verschiedene Werte.
-
Einweg-Funktion: Aus dem Hash kann man den ursprĂŒnglichen Inhalt nicht rekonstruieren. Der Hash verrĂ€t nichts ĂŒber den Inhalt auĂer seiner IdentitĂ€t.
-
Lawineneffekt: Selbst kleinste Ănderungen am Inhalt fĂŒhren zu einem völlig anderen Hash:
Inhalt: "Hello World" Hash: 0a4d55a8d778e5022fab701977c5d840bbc486d0 Inhalt: "Hello World!" (nur ein Ausrufezeichen hinzugefĂŒgt) Hash: 2ef7bde608ce5404e97d5f042f95f89f1c232871
Wie Git den Hash berechnet
Git berechnet den Hash nicht einfach nur vom reinen Dateiinhalt, sondern fĂŒgt einen Header hinzu, der den Objekttyp und die GröĂe enthĂ€lt:
<typ> <gröĂe>\0<inhalt>
FĂŒr eine Datei mit dem Inhalt âHello World" (11 Bytes) wĂŒrde Git also hashen:
blob 11\0Hello World
Du kannst das selbst ĂŒberprĂŒfen:
# Manuell den Hash berechnen
echo -n "Hello World" | git hash-object --stdin
# Ergebnis: 5e1c309dae7f45e0f39b1bf3ac3cd9db12e7d689
# Zum Vergleich mit Unix-Tools
printf "blob 11\0Hello World" | sha1sum
# Gleiches Ergebnis!
Der Ăbergang zu SHA-256
Git arbeitet seit einiger Zeit an einer Migration zu SHA-256, da SHA-1 theoretische SchwĂ€chen zeigt (2017 wurde die erste praktische Kollision demonstriert). FĂŒr die allermeisten AnwendungsfĂ€lle ist SHA-1 aber weiterhin sicher, und das Konzept der Inhaltsadressierung bleibt identisch.
Warum identische Dateien denselben Hash haben
Hier liegt die Magie des Systems: Da der Hash ausschlieĂlich vom Inhalt abhĂ€ngt, erzeugen identische Inhalte garantiert identische Hashes â unabhĂ€ngig davon:
- Wer den Hash berechnet
- Wann der Hash berechnet wird
- Wo (in welchem Repository) der Hash berechnet wird
- Wie die Datei heiĂt
- In welchem Verzeichnis die Datei liegt
Ein konkretes Beispiel
Stell dir vor, du und ein Kollege arbeiten an verschiedenen Projekten auf verschiedenen Kontinenten, ohne voneinander zu wissen:
# Dein Repository in Berlin
projekt-alpha/
âââ src/
âââ utils.php # Inhalt: "<?php echo 'Hello';"
# Repository deines Kollegen in Tokyo
projekt-beta/
âââ lib/
âââ helper.php # Inhalt: "<?php echo 'Hello';"
Obwohl die Dateien unterschiedliche Namen haben und in komplett unverbundenen Projekten liegen, haben sie exakt denselben Git-Hash, weil ihr Inhalt identisch ist.
Du kannst das selbst testen:
# In deinem Repository
echo "<?php echo 'Hello';" > test1.php
git hash-object test1.php
# Ergebnis: z.B. 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# In einem komplett anderen Repository auf einem anderen Computer
echo "<?php echo 'Hello';" > andere_datei.php
git hash-object andere_datei.php
# Gleiches Ergebnis: 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
Was nicht in den Hash einflieĂt
Es ist wichtig zu verstehen, was alles keinen Einfluss auf den Hash einer Datei hat:
- â Der Dateiname
- â Der Verzeichnispfad
- â Die Dateiberechtigungen (auĂer dem Executable-Bit)
- â Der Zeitstempel der Datei
- â Der Autor oder Committer
- â Das Repository, in dem die Datei liegt
- â Die Git-Version
Nur der reine Dateiinhalt (Byte fĂŒr Byte) bestimmt den Hash.
Praktische Auswirkungen
Die Inhaltsadressierung hat weitreichende Konsequenzen fĂŒr die tĂ€gliche Arbeit mit Git:
1. Automatische Deduplizierung đŸ
Git speichert identischen Inhalt nur einmal, egal wie oft er vorkommt. Wenn du dieselbe Datei in 100 verschiedenen Commits hast und sie sich nie Àndert, existiert sie physisch nur einmal im .git/objects-Verzeichnis.
# Beispiel: Eine Datei in 100 Commits
# Naiver Ansatz: 100 Ă 10 KB = 1 MB Speicher
# Git: 1 Ă 10 KB = 10 KB Speicher
Das gilt auch fĂŒr Dateien, die kopiert oder umbenannt werden: Solange der Inhalt gleich bleibt, wird nichts doppelt gespeichert.
2. Extrem effizientes Klonen und Fetching đ
Wenn du ein Repository klonst oder Ănderungen fetchst, muss Git nur die Objekte ĂŒbertragen, die du noch nicht hast. Durch den Hash kann Git sofort feststellen: âDieses Objekt habe ich schon" â ohne den Inhalt vergleichen zu mĂŒssen.
# Beim git fetch:
# Dein lokales Repo: "Ich habe Objekte a1b2c3, d4e5f6, g7h8i9"
# Remote antwortet: "Der neue Commit braucht a1b2c3, x9y8z7"
# Git: "a1b2c3 habe ich schon, nur x9y8z7 muss ĂŒbertragen werden"
3. IntegritĂ€t und Datenvalidierung â
Der Hash dient als kryptografische PrĂŒfsumme. Wenn auch nur ein einziges Bit in einem Objekt verĂ€ndert wird (durch Festplattenfehler, Ăbertragungsprobleme oder absichtliche Manipulation), stimmt der Hash nicht mehr. Git kann das sofort erkennen:
# Git prĂŒft automatisch bei vielen Operationen
# Du kannst es auch manuell auslösen:
git fsck
# ĂberprĂŒft die IntegritĂ€t aller Objekte
# Mögliche Ausgabe bei Problemen:
# error: sha1 mismatch for objects/a1/b2c3...
# fatal: loose object a1b2c3... is corrupt
Diese Eigenschaft macht Git extrem robust gegen:
- Festplattenfehler
- Netzwerkprobleme beim Push/Pull
- Versehentliche oder absichtliche Manipulation
- Bitrot (schleichende Datenkorruption)
4. Verteilte Zusammenarbeit ohne zentrale AutoritĂ€t đ
Da Hashes deterministisch sind, braucht Git keine zentrale Instanz, die Objekte nummeriert oder IDs vergibt. Zwei Entwickler können unabhĂ€ngig voneinander arbeiten, und wenn sie spĂ€ter ihre Repositories zusammenfĂŒhren, sind identische Objekte automatisch âdieselben" â ohne Konflikte.
flowchart TD
subgraph Alice["Alice's Repository"]
A1[("Commit abc123")]
A2[("Blob def456")]
end
subgraph Bob["Bob's Repository"]
B1[("Commit xyz789")]
B2[("Blob def456")]
end
subgraph Merged["Nach dem Merge"]
M1[("Commit abc123")]
M2[("Commit xyz789")]
M3[("Blob def456\nnur einmal!")]
end
Alice --> Merged
Bob --> Merged
style A2 fill:#90EE90,color:#000000
style B2 fill:#90EE90,color:#000000
style M3 fill:#90EE90,color:#000000
5. Sichere Referenzierung von History đ
Da jeder Commit einen Hash hat, der von seinem Inhalt und von seinen Parent-Commits abhĂ€ngt, ist die gesamte Git-History kryptografisch verkettet â Ă€hnlich wie eine Blockchain:
flowchart LR
C1["Commit A\nhash: a1b2c3"]
C2["Commit B\nhash: d4e5f6\nparent: a1b2c3"]
C3["Commit C\nhash: g7h8i9\nparent: d4e5f6"]
C1 --> C2 --> C3
Wenn jemand versucht, einen alten Commit zu verĂ€ndern, Ă€ndert sich sein Hash. Da der nĂ€chste Commit auf diesen Hash verweist, mĂŒsste auch dieser Commit neu gehasht werden â und so weiter bis zum aktuellen Commit. Eine nachtrĂ€gliche Manipulation der History ist sofort erkennbar.
6. Commits sind global eindeutig đ
Die Wahrscheinlichkeit einer Hash-Kollision (dass zwei verschiedene Inhalte denselben Hash erzeugen) ist astronomisch gering:
$$
P(\text{Kollision}) \approx \frac{n^2}{2 \times 2^{160}}
$$
Selbst bei einer Milliarde Objekte ($n = 10^9$) ist die Wahrscheinlichkeit etwa:
$$
P \approx \frac{(10^9)^2}{2 \times 2^{160}} \approx 3{,}4 \times 10^{-31}
$$
Das bedeutet: Commit-Hashes sind praktisch weltweit eindeutig. Du kannst einen Hash wie a1b2c3d4 als absoluten Identifikator verwenden, der niemals mit einem anderen Commit kollidieren wird.
7. Effizientes Branching und Tagging đ·ïž
# Ein Branch ist nur eine Datei mit einem Hash
cat .git/refs/heads/main
# Ausgabe: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
# Ein Tag ist Àhnlich simpel
cat .git/refs/tags/v1.0.0
# Ausgabe: f1e2d3c4b5a6978... (ein Hash)
Das Erstellen eines neuen Branches ist daher eine O(1)-Operation â es wird nur eine 40-Byte-Datei geschrieben, egal wie groĂ das Repository ist.
8. Rename Detection funktioniert automatisch đ
Git speichert keine âRename"-Operationen explizit. Wenn du eine Datei umbenennst, erkennt Git das beim Betrachten der History automatisch, weil der Blob-Hash identisch bleibt:
# Datei umbenennen
mv old_name.php new_name.php
git add -A
git commit -m "Rename file"
# Git erkennt das Rename anhand des identischen Hashes
git log --follow --stat new_name.php
# Zeigt die komplette History, auch vor dem Rename!
Praktische Demonstration đ§Ș
Lass uns das Konzept in der Praxis erleben:
Schritt 1: Zwei Dateien mit gleichem Inhalt erstellen
# Neues Test-Repository
mkdir hash-demo && cd hash-demo
git init
# Zwei Dateien mit identischem Inhalt, aber verschiedenen Namen
echo "Identischer Inhalt" > datei1.txt
echo "Identischer Inhalt" > datei2.txt
# Hashes anzeigen (ohne zu committen)
git hash-object datei1.txt
# Ausgabe: z.B. 9c59e24b8b0c4c2a8d3e5f6a7b8c9d0e1f2a3b4c
git hash-object datei2.txt
# Ausgabe: 9c59e24b8b0c4c2a8d3e5f6a7b8c9d0e1f2a3b4c â Identisch!
Schritt 2: Ănderung demonstrieren
# Winzige Ănderung an datei2
echo "Identischer Inhalt!" > datei2.txt # Ausrufezeichen hinzugefĂŒgt
git hash-object datei2.txt
# Ausgabe: 7f8e9d0c1b2a3456... â Komplett anderer Hash!
Schritt 3: Objekte im Repository inspizieren
# Dateien zum Staging hinzufĂŒgen
git add datei1.txt
git add datei2.txt
git commit -m "Initial commit"
# Blobs im Repository anschauen
find .git/objects -type f
# Zeigt die gespeicherten Objekte
# Einen Blob inspizieren
git cat-file -t 9c59e24 # Typ anzeigen â "blob"
git cat-file -p 9c59e24 # Inhalt anzeigen â "Identischer Inhalt"
Zusammenfassung đ
Die Inhaltsadressierung ist das architektonische Fundament von Git:
| Aspekt | Bedeutung |
|---|---|
| IdentitÀt | Inhalt = Adresse (Hash) |
| Effizienz | Automatische Deduplizierung |
| IntegritĂ€t | Kryptografische PrĂŒfsummen |
| Verteilung | Keine zentrale ID-Vergabe nötig |
| Performance | Schnelles Vergleichen via Hash |
| ZuverlÀssigkeit | Manipulation sofort erkennbar |
Wenn du dieses Konzept verinnerlicht hast, werden viele Git-Mechanismen plötzlich logisch: Warum Branches so billig sind, warum git gc Objekte aufrĂ€umen kann, warum Rebasing die History âumschreibt" und warum das Ăndern alter Commits problematisch ist. Der Hash ist der SchlĂŒssel zu allem. đ