Versionskontrolle mit Git
Git wird oft auswendig gelernt statt verstanden. Dabei reichen drei Ideen: Commits sind Schnappschüsse mit Fingerabdruck, sie bilden einen Graphen, und ein Branch ist nur ein Zettel mit einem Namen darauf. Danach erklären sich Merge, Rebase und Force-Push von selbst.
Git speichert die Geschichte eines Projekts als Folge von Commits. Ein Commit ist ein vollständiger Schnappschuss aller Dateien, plus Autor, Nachricht und Verweis auf den oder die Vorgänger. Jeder Commit wird über einen Hash seines gesamten Inhalts benannt – ändert man irgendetwas, entsteht ein anderer Commit.
Zusammen bilden die Commits einen Graphen. Ein Branch ist nichts weiter als ein beweglicher Zeiger auf einen Commit, HEAD sagt, auf welchem Branch man gerade steht. Merge führt zwei Linien mit einem neuen Commit zusammen; Rebase schreibt Commits neu auf eine andere Basis. Wer das Bild vom Graphen mit Zetteln im Kopf hat, versteht fast jeden Git-Befehl.
Schnappschüsse mit Fingerabdruck
Ältere Systeme wie CVS und Subversion speicherten pro Datei die Unterschiede zur Vorversion. Git denkt anders: Jeder Commit ist ein vollständiger Schnappschuss. Damit das nicht explodiert, speichert Git jeden Dateiinhalt nur einmal – unter seinem Hash. Eine unveränderte Datei in hundert Commits liegt genau einmal im Repository.
Auch der Commit selbst hat einen Hash, berechnet aus allem, was ihn ausmacht: dem Schnappschuss, dem Autor, der Nachricht – und dem Hash des Vorgängers. Daraus folgt etwas Wichtiges:
Weil jeder Hash den Hash des Vorgängers enthält, kann man die Vergangenheit nicht unbemerkt ändern: Jede Änderung erzeugt neue Hashes für alle Nachfolger. Ein Commit-Hash garantiert also die gesamte Geschichte bis zum ersten Commit. Dasselbe Prinzip – eine Kette von Hashes – steckt auch in Blockchains.
Drei Bereiche
Zwischen dem Bearbeiten einer Datei und dem Commit liegt in Git eine Zwischenstation, die bei anderen Systemen fehlt:
.gitDer Staging-Bereich erlaubt, aus einem Durcheinander von Änderungen saubere Commits zu bauen: erst nur die Fehlerkorrektur, dann getrennt die Umbenennung. git add -p geht sogar einzelne Stellen innerhalb einer Datei durch.
Branches sind Zettel
Ein Branch kostet in Git praktisch nichts: Er ist eine winzige Datei mit einem Commit-Hash darin. Neuer Commit auf einem Branch heißt: Commit anlegen, Zettel einen Schritt weiterschieben. HEAD ist der Zettel, der sagt, welcher Branch gerade dran ist. Spiel es durch – und vergleiche dann Merge und Rebase:
| Merge | Rebase | |
|---|---|---|
| Was passiert | ein neuer Commit mit zwei Eltern verbindet die Linien | die eigenen Commits werden auf die neue Basis kopiert – mit neuen Hashes |
| Geschichte | zeigt, wie es wirklich war, auch die Parallelität | eine gerade Linie, leichter zu lesen |
| Bestehende Commits | bleiben unverändert | werden ersetzt; die alten verschwinden aus dem Graphen |
| Gefahr | unübersichtliche Graphen bei vielen Merges | wer die alten Commits schon hatte, hat jetzt eine abweichende Geschichte |
Daraus folgt die eine Regel, die man sich merken muss: Commits, die andere schon haben, nicht rebasen. Den eigenen Feature-Branch vor dem Zusammenführen auf den neuesten Stand von main zu rebasen, ist dagegen gängige Praxis. Danach muss man ihn mit --force-with-lease pushen – die Variante, die abbricht, wenn jemand anderes inzwischen etwas hochgeladen hat.
Konflikte
Haben beide Seiten dieselbe Stelle derselben Datei geändert, kann Git nicht entscheiden und markiert die Stelle:
<<<<<<< HEAD
return preis * 0.9; // main: 10 % Rabatt
=======
return preis * rabatt; // feature: Rabatt konfigurierbar
>>>>>>> featureMan entscheidet von Hand, entfernt die Markierungen, git add und schließt mit git commit (beim Merge) oder git rebase --continue ab. Kleine, häufig integrierte Branches haben deutlich weniger Konflikte als große, wochenlang getrennte.
Verteilt: jeder hat alles
Git ist verteilt: Jeder Klon enthält die vollständige Geschichte. Ein „Server“ wie GitHub oder GitLab ist technisch nur ein weiteres Repository, auf das sich alle geeinigt haben. git fetch holt neue Commits von dort und aktualisiert die Remote-Branches wie origin/main – ohne die eigenen anzufassen. git pull ist fetch plus Merge (oder Rebase), git push schickt die eigenen Commits hin und schiebt dort den Zettel weiter – aber nur, wenn das ohne Verlust geht.
Entstanden ist Git 2005 in wenigen Wochen: Linus Torvalds schrieb es für die Linux-Entwicklung, nachdem das bis dahin genutzte Werkzeug nicht mehr kostenlos zur Verfügung stand.
Für Entwickler übersetzt
git switch -c feature/rabatt # neuer Branch, gleich draufwechseln
git add -p # Änderungen stückweise auswählen
git commit -m "Rabatt für Stammkunden"
git fetch && git rebase origin/main # eigenen Branch aktualisieren
git push --force-with-lease # nach Rebase – nur auf eigenen Branches
git log --oneline --graph --all # der Graph aus dem Widget, im Terminal
git reflog # wo stand HEAD früher? „verlorene“ Commits wiederfinden
git bisect start # Fehler suchen per binärer Suche:
git bisect bad # jetzt ist er da
git bisect good v1.4 # damals nicht – Git halbiert, du testest| Thema | Für Java-Projekte |
|---|---|
| .gitignore | target/, build/, .idea/, *.class – Erzeugtes gehört nicht ins Repository, nur Quellen. |
| Zeilenenden | Eine .gitattributes mit * text=auto verhindert, dass Windows- und Linux-Entwickler sich gegenseitig jede Zeile „ändern“. Für Shell-Skripte ausdrücklich eol=lf. |
| git bisect | Die binäre Suche angewandt auf die Geschichte: Unter 1000 Commits findet man den schuldigen in etwa 10 Schritten. Mit git bisect run mvn test sogar automatisch. |
| Große Dateien | Binärdateien wie JARs oder Videos bleiben für immer in der Geschichte. Dafür gibt es Git LFS – oder besser ein Artefakt-Repository. |
| Git aus Java | Die Bibliothek JGit implementiert Git vollständig in Java – sie steckt etwa in Eclipse und vielen Build-Werkzeugen. |
Häufige Irrtümer
git reflog zeigt, wo HEAD vorher stand; standardmäßig werden solche Einträge erst nach Wochen aufgeräumt.pull.rebase ein oder holen lieber erst mit fetch und entscheiden dann.