Grundlagen

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.

Lesestand

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:

Ein Commit kennt seine ganze Geschichte
Ändere die Nachricht des zweiten Commits.

Echte SHA-256-Berechnung, auf 7 Zeichen gekürzt. Git selbst benutzt standardmäßig SHA-1, neuere Versionen können auch SHA-256.

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:

Arbeitsverzeichnisdie Dateien, an denen du arbeitest
git add →← git restore
Staging-Bereich„Index“: was in den nächsten Commit soll
git commit →← git restore --staged
Repositorydie Commits in .git

Der 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:

Der Commit-Graph

Vorschlag zum Ausprobieren: Branch anlegen, zwei Commits, zurück auf main, ein Commit, dann einmal mergen – und nach „Neu“ dasselbe mit Rebase.

MergeRebase
Was passiertein neuer Commit mit zwei Eltern verbindet die Liniendie eigenen Commits werden auf die neue Basis kopiert – mit neuen Hashes
Geschichtezeigt, wie es wirklich war, auch die Parallelitäteine gerade Linie, leichter zu lesen
Bestehende Commitsbleiben unverändertwerden ersetzt; die alten verschwinden aus dem Graphen
Gefahrunübersichtliche Graphen bei vielen Mergeswer 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
>>>>>>> feature

Man 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
ThemaFür Java-Projekte
.gitignoretarget/, build/, .idea/, *.class – Erzeugtes gehört nicht ins Repository, nur Quellen.
ZeilenendenEine .gitattributes mit * text=auto verhindert, dass Windows- und Linux-Entwickler sich gegenseitig jede Zeile „ändern“. Für Shell-Skripte ausdrücklich eol=lf.
git bisectDie 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 DateienBinä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 JavaDie Bibliothek JGit implementiert Git vollständig in Java – sie steckt etwa in Eclipse und vielen Build-Werkzeugen.

Häufige Irrtümer

„Git speichert Unterschiede.“ – Git speichert Schnappschüsse. Unterschiede rechnet es bei Bedarf aus. Intern komprimiert es ähnliche Inhalte zwar platzsparend, aber das ist ein Speicherdetail, kein Modell.
„Ein Branch ist eine Kopie des Codes.“ – Er ist ein Zeiger auf einen Commit. Einen Branch anzulegen kostet 41 Byte, egal wie groß das Projekt ist.
„Was weg ist, ist weg.“ – Nach einem missglückten Rebase oder Reset sind die alten Commits nur nicht mehr sichtbar. git reflog zeigt, wo HEAD vorher stand; standardmäßig werden solche Einträge erst nach Wochen aufgeräumt.
„git pull ist harmlos.“ – Es ist fetch plus Merge und kann unerwartete Merge-Commits oder Konflikte erzeugen. Viele Teams stellen pull.rebase ein oder holen lieber erst mit fetch und entscheiden dann.
„Git ist GitHub.“ – Git ist das Werkzeug, GitHub, GitLab und Bitbucket sind Plattformen drumherum. Pull Requests, Issues und Pipelines gehören zur Plattform, nicht zu Git.

Verwandte Begriffe