Git

Tooling
Reproduzierbarkeit
Commits, Branches, Merge-Konflikte und der Umgang mit Remotes.

Kernideen

  • Git speichert Zustände, nicht Unterschiede: Jeder Commit hält den vollständigen Stand fest und verweist auf seinen Vorgänger.
  • Drei Orte: Arbeitsverzeichnis, Staging-Bereich, Commit. Der Umweg über das Staging macht Commits inhaltlich sauber.
  • Ein Branch ist nur ein Zeiger auf einen Commit und deshalb billig.
  • Konflikte sind normal. Git markiert die Stelle und überlässt die Entscheidung dem Menschen.
  • Rückgängig machen heisst je nach Ziel restore, reset oder revert. Nur revert ist für bereits geteilte Commits richtig.
  • Was nicht hineingehört, hält .gitignore draussen. Ein einmal committetes Passwort bleibt in der Historie.

Erklärung

Vorwissen: keines. Hilfreich ist Linux und Shell, weil Git auf der Kommandozeile bedient wird. Die Ausgaben dieser Seite stammen aus einem echten Repository, das beim Bauen der Website angelegt wird. Git ist sprachunabhängig, deshalb gibt es hier keine getrennten R- und Python-Reiter.

Das Problem hinter der Versionsverwaltung

Der Ordner mit analyse_final.R, analyse_final_v2.R und analyse_final_wirklich.R ist bekannt. Er beantwortet keine der Fragen, die später gestellt werden: Was war der Unterschied? Warum wurde das geändert? Mit welcher Fassung ist die Zahl im Bericht entstanden? Git beantwortet genau diese Fragen.

Die drei Orte

Ort Inhalt Befehl dorthin
Arbeitsverzeichnis die Dateien, an denen gerade gearbeitet wird Datei speichern
Staging-Bereich (Index) was in den nächsten Commit soll git add
Repository die festgeschriebene Historie git commit

git status --short zeigt beide Spalten: links der Staging-Bereich, rechts das Arbeitsverzeichnis.

Anzeige Bedeutung
?? unverfolgt, Git kennt die Datei nicht
A neu und vorgemerkt
M geändert und vorgemerkt
M geändert, nicht vorgemerkt
MM vorgemerkt und danach erneut geändert
UU Konflikt, von beiden Seiten geändert

Rückgängig machen

Ziel Befehl Wirkung
Änderung im Arbeitsverzeichnis verwerfen git restore datei Datei zurück auf den letzten Commit
Vormerkung zurücknehmen git restore --staged datei Änderung bleibt, verlässt das Staging
letzten Commit auflösen, Änderung behalten git reset --soft HEAD~1 Commit weg, Inhalt wieder vorgemerkt
Commit inhaltlich zurücknehmen git revert <commit> neuer Commit, der die Änderung aufhebt

Die Faustregel: Solange nichts geteilt wurde, ist reset bequem. Sobald andere den Commit haben, ist revert der einzige saubere Weg, weil es die Historie nicht umschreibt.

Remote und Zusammenarbeit

Das Remote ist eine gleichwertige Kopie, nicht die Wahrheit. Lokal liegt die vollständige Historie, weshalb sich auch ohne Netzverbindung arbeiten lässt.

git remote add origin https://github.com/nutzer/projekt.git
git push -u origin main        # erstmalig, merkt sich die Zuordnung
git pull                       # Aenderungen anderer holen
git clone https://github.com/nutzer/projekt.git

Kurz nachgeschlagen

Aufgabe Befehl
Stand ansehen git status, git status --short
abschnittsweise vormerken git add -p
Verlauf git log --oneline --graph --all
Änderungen git diff, git diff --staged
Branch anlegen und wechseln git switch -c thema
zusammenführen git merge thema
Konfliktdateien auflisten git diff --name-only --diff-filter=U
Datei aus der Verfolgung nehmen git rm --cached datei
wer hat diese Zeile geändert git blame datei
Zwischenstand parken git stash, git stash pop

Beispiele

Frage und Datenlage

Ein leerer Ordner, eine neue Datei analyse.R. Welche Schritte führen zum ersten Commit, und was zeigt git status dazwischen?

Rechnung

git("init -q -b main")
schreibe("analyse.R", c("werte <- c(1, 2, 3)", "mean(werte)"))

git("status --short")          # Git kennt die Datei noch nicht
?? analyse.R
git("add analyse.R")
git("status --short")          # jetzt vorgemerkt
A  analyse.R
git("commit -q -m 'Mittelwert berechnen'")
git("log --oneline")
e3003f2 Mittelwert berechnen

Output Zeile für Zeile

Schritt Ausgabe Erklärung
nach dem Anlegen ?? analyse.R Git sieht die Datei, verfolgt sie aber nicht. Das ist Absicht: Sonst landeten Zwischenergebnisse und Zugangsdaten ungewollt im Repository.
nach git add A analyse.R Das A steht links, also im Staging-Bereich: neu und vorgemerkt.
nach git commit keine Ausgabe (durch -q)
git log --oneline eine Zeile mit Kurzkennung und Nachricht Der Commit ist da; die Kennung ist eine Prüfsumme über den Inhalt und deshalb bei jedem Lauf anders.

Interpretation und Ergebnissatz

Der Weg führt immer über dieselben zwei Schritte. add beantwortet “was gehört dazu”, commit beantwortet “und das halte ich jetzt fest”. Eine gute Commit-Nachricht benennt das Warum: Fix oder Update sind in einem halben Jahr wertlos, Dezimalkomma beim Import beruecksichtigen ist es nicht.

Eine neue Datei erscheint zunächst als ?? analyse.R, nach git add als A analyse.R, und nach dem Commit steht sie mit ihrer Nachricht im Verlauf.

Frage und Datenlage

Zwei Änderungen gleichzeitig: Die Analyse ist fertig, die Notizen sind es nicht. Wie hält man nur die fertige Änderung fest?

Rechnung

schreibe("analyse.R", c("werte <- c(1, 2, 3, 4)", "mean(werte)", "sd(werte)"))
schreibe("notizen.txt", "unfertige Gedanken")

git("add analyse.R")           # nur diese eine Datei
git("status --short")
M  analyse.R
?? notizen.txt
git("diff --staged --stat")    # was in den Commit ginge
 analyse.R | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
git("diff --stat")             # was draussen bleibt
git("commit -q -m 'Standardabweichung ergaenzen'")
git("status --short")
?? notizen.txt

Output Zeile für Zeile

Zeile Ausgabe Erklärung
nach add analyse.R M analyse.R und ?? notizen.txt Das M steht links: vorgemerkt. Die Notizen sind unverfolgt und bleiben es.
diff --staged --stat analyse.R \| 3 ++-, eine Datei geändert Das ist genau der Inhalt des nächsten Commits.
diff --stat leer Im Arbeitsverzeichnis gibt es keine unvorgemerkte Änderung an verfolgten Dateien; notizen.txt ist gar nicht verfolgt.
nach dem Commit nur noch ?? notizen.txt Die fertige Änderung ist festgehalten, die unfertige liegt unberührt daneben.

Interpretation und Ergebnissatz

Der Zwischenschritt wirkt umständlich und ist der Grund, warum Commits inhaltlich sauber bleiben können. Man wählt aus, was zusammengehört, statt pauschal alles Geänderte einzupacken. Mit git add -p geht das sogar abschnittsweise innerhalb einer Datei.

Nach git add analyse.R ist nur diese Datei vorgemerkt (M), die unfertigen Notizen bleiben unverfolgt (??) und überstehen den Commit unberührt.

Frage und Datenlage

Eine Zeile wird ergänzt. Wie sieht Git den Unterschied, und warum funktioniert das bei Textdateien und nicht bei Excel-Dateien?

Rechnung

schreibe("analyse.R", c("werte <- c(1, 2, 3, 4)", "mean(werte)",
                        "sd(werte)", "median(werte)"))
git("diff --stat")
 analyse.R | 1 +
 1 file changed, 1 insertion(+)
git("diff")
diff --git a/analyse.R b/analyse.R
index ab4a8d2..5f82b67 100644
--- a/analyse.R
+++ b/analyse.R
@@ -1,3 +1,4 @@
 werte <- c(1, 2, 3, 4)
 mean(werte)
 sd(werte)
+median(werte)

Output Zeile für Zeile

Teil der Ausgabe Bedeutung
analyse.R \| 1 + eine Datei, eine hinzugefügte Zeile
@@ -1,3 +1,4 @@ der Abschnitt: vorher Zeilen 1 bis 3, nachher 1 bis 4
Zeilen ohne Vorzeichen unverändert, nur als Zusammenhang gezeigt
+median(werte) die hinzugefügte Zeile

Bei einer Änderung statt einer Ergänzung stünden - und + untereinander: die alte und die neue Fassung derselben Zeile.

Interpretation und Ergebnissatz

Genau diese Ansicht ist der Grund, warum Textformate versioniert werden können und Binärformate wie Excel- oder Word-Dateien nicht sinnvoll: Dort kann Git nur “die Datei ist anders” sagen. Für die Datenarbeit heisst das, Code und Konfiguration in Textform zu halten, und Notebooks vor dem Commit von ihren Ausgaben zu befreien.

git diff zeigt die neue Zeile +median(werte) im Zusammenhang der umgebenden Zeilen; die Kopfzeile @@ -1,3 +1,4 @@ nennt die betroffenen Zeilennummern vorher und nachher.

Frage und Datenlage

Auf einem Branch wird die Analyse logarithmiert, auf der Hauptlinie gleichzeitig auf den Median umgestellt. Beide ändern dieselbe Zeile. Was passiert beim Zusammenführen?

Rechnung

git("add -A")
git("commit -q -m 'Median ergaenzen'")
git("switch -q -c experiment")
schreibe("analyse.R", c("werte <- c(1, 2, 3, 4)", "mean(log(werte))"))
git("add -A")
git("commit -q -m 'Log-Transformation testen'")
git("switch -q main")
schreibe("analyse.R", c("werte <- c(1, 2, 3, 4)", "median(werte)"))
git("add -A")
git("commit -q -m 'Median statt Mittel'")
git("log --oneline --graph --all")
* 6e3adda Log-Transformation testen
| * 5cc65e1 Median statt Mittel
|/  
* 3aeea74 Median ergaenzen
* 49d5da7 Standardabweichung ergaenzen
* e3003f2 Mittelwert berechnen
git("merge experiment -m 'Experiment uebernehmen'")
Auto-merging analyse.R
CONFLICT (content): Merge conflict in analyse.R
Automatic merge failed; fix conflicts and then commit the result.
git("status --short")
UU analyse.R

Die Datei enthält jetzt beide Fassungen mit Markierungen:

zeige("analyse.R")
werte <- c(1, 2, 3, 4)
<<<<<<< HEAD
median(werte)
=======
mean(log(werte))
>>>>>>> experiment
git("diff --name-only --diff-filter=U")
analyse.R

Aufgelöst wird von Hand, danach wie ein gewöhnlicher Commit:

schreibe("analyse.R", c("werte <- c(1, 2, 3, 4)", "median(werte)",
                        "mean(log(werte))"))
git("add analyse.R")
git("commit -q -m 'Konflikt aufgeloest: beide Kennzahlen'")
git("log --oneline --graph")
*   c6cbc77 Konflikt aufgeloest: beide Kennzahlen
|\  
| * 6e3adda Log-Transformation testen
* | 5cc65e1 Median statt Mittel
|/  
* 3aeea74 Median ergaenzen
* 49d5da7 Standardabweichung ergaenzen
* e3003f2 Mittelwert berechnen

Output Zeile für Zeile

Schritt Ausgabe Erklärung
Graph vor dem Merge zwei Linien, die sich verzweigen Beide Commits haben denselben Vorgänger.
git merge Auto-merging analyse.R, CONFLICT (content), Automatic merge failed Git kann nicht entscheiden, welche Fassung gilt.
git status --short UU analyse.R U steht für unmerged, auf beiden Seiten geändert.
Dateiinhalt <<<<<<< HEAD, die eigene Fassung, =======, die fremde, >>>>>>> experiment Die Markierungen zeigen genau die strittige Stelle; alles andere hat Git selbst zusammengeführt.
--diff-filter=U analyse.R die Liste der Konfliktdateien, nützlich bei vielen Dateien
nach dem Auflösen Graph mit zusammengeführter Linie Der Merge-Commit hat zwei Vorgänger.

Interpretation und Ergebnissatz

Ein Konflikt ist kein Fehler, sondern eine Frage: Welche der beiden Änderungen gilt, oder gelten beide? Git stellt sie und beantwortet sie nicht selbst. Wichtig ist, die Markierungen vollständig zu entfernen; bleibt eine Zeile mit <<<<<<< stehen, ist der Code syntaktisch kaputt, und der Commit läuft trotzdem durch.

Das Zusammenführen meldet CONFLICT (content): Merge conflict in analyse.R, und git status zeigt UU analyse.R. Nach dem Auflösen von Hand und einem gewöhnlichen Commit führt der Graph beide Linien wieder zusammen.

Frage und Datenlage

Drei Situationen: eine Änderung im Arbeitsverzeichnis verwerfen, eine Vormerkung zurücknehmen, und einen bereits gemachten Commit aufheben.

Rechnung

schreibe("analyse.R", c("werte <- c(1, 2, 3, 4)", "unsinn()"))
git("status --short")
 M analyse.R
git("restore analyse.R")       # Aenderung verwerfen
git("status --short")
zeige("analyse.R")
werte <- c(1, 2, 3, 4)
median(werte)
mean(log(werte))
schreibe("notizen.txt", "doch fertig")
git("add notizen.txt")
git("status --short")          # vorgemerkt
M  notizen.txt
git("restore --staged notizen.txt")
git("status --short")          # Aenderung bleibt, Vormerkung weg
 M notizen.txt
schreibe("fehler.txt", "sollte nicht bleiben")
git("add -A")
git("commit -q -m 'Versehentlicher Commit'")
git("log --oneline")
7ebf7e9 Versehentlicher Commit
c6cbc77 Konflikt aufgeloest: beide Kennzahlen
5cc65e1 Median statt Mittel
6e3adda Log-Transformation testen
3aeea74 Median ergaenzen
49d5da7 Standardabweichung ergaenzen
e3003f2 Mittelwert berechnen
git("revert --no-edit HEAD")
[main 527c6e5] Revert "Versehentlicher Commit"
 Date: Sat Sep 12 13:46:49 2026 +0000
 2 files changed, 1 insertion(+), 2 deletions(-)
 delete mode 100644 fehler.txt
git("log --oneline")
527c6e5 Revert "Versehentlicher Commit"
7ebf7e9 Versehentlicher Commit
c6cbc77 Konflikt aufgeloest: beide Kennzahlen
5cc65e1 Median statt Mittel
6e3adda Log-Transformation testen
3aeea74 Median ergaenzen
49d5da7 Standardabweichung ergaenzen
e3003f2 Mittelwert berechnen

Output Zeile für Zeile

Schritt Ausgabe Erklärung
Datei kaputt geändert M analyse.R Das M steht rechts: geändert, nicht vorgemerkt.
nach restore keine Zeile mehr Die Änderung ist weg, die Datei steht wieder auf dem letzten Commit. Nicht rückgängig zu machen.
nach add notizen.txt M notizen.txt Die Datei ist seit Beispiel 4 verfolgt, also M statt A, und das M steht links: vorgemerkt.
nach restore --staged M notizen.txt Dasselbe M, jetzt rechts: Die Vormerkung ist zurückgenommen, der neue Inhalt bleibt erhalten.
nach dem versehentlichen Commit der Commit steht oben im Verlauf
nach revert eine Zeile mehr, oben Revert "Versehentlicher Commit" Der alte Commit bleibt in der Historie, ein neuer hebt ihn auf.

Interpretation und Ergebnissatz

Die drei Befehle greifen an drei verschiedenen Orten an, und die Wahl hängt davon ab, wo die Änderung gerade liegt. restore ohne --staged verwirft unwiderruflich; alles andere lässt sich wiederherstellen. Für geteilte Commits ist revert richtig, weil es nichts umschreibt: Wer den alten Stand schon geholt hat, bekommt einfach den neuen Commit dazu.

git restore setzt die Datei auf den letzten Commit zurück, git restore --staged nimmt nur die Vormerkung zurück, und git revert fügt einen neuen Commit hinzu, der die Änderung aufhebt, statt die Historie zu ändern.

Frage und Datenlage

Eine Rohdatei und eine Datei mit Zugangsdaten liegen im Projekt. Wie hält man sie draussen, und was tun, wenn eine davon schon committet ist?

Rechnung

schreibe(".gitignore", c(".venv/", "renv/library/", "__pycache__/",
                         "data/roh/", "*.Rhistory", ".RData"))
schreibe("data/roh/gross.csv", "viele Zeilen")
git("add -A")
git("commit -q -m 'Ignorierliste'")
git("status --short")          # leer: die Rohdatei wird ignoriert
schreibe(".env", "PGPASSWORD=geheim")
git("add -f .env")             # -f erzwingt es trotz Ignorierliste
git("commit -q -m 'Versehentlich Zugangsdaten'")
git("ls-files")
.env
.gitignore
analyse.R
notizen.txt
git("rm -q --cached .env")
git("commit -q -m 'Zugangsdaten aus der Verfolgung nehmen'")
git("ls-files")
.gitignore
analyse.R
notizen.txt

Output Zeile für Zeile

Schritt Ausgabe Erklärung
status nach der Ignorierliste leer Obwohl data/roh/gross.csv existiert, meldet Git nichts: Die Datei ist ignoriert.
ls-files nach dem Fehlgriff .env steht in der Liste -f hat die Ignorierliste übergangen; so passiert es in der Praxis.
nach git rm --cached .env .env fehlt in der Liste Die Datei bleibt auf der Platte, wird aber nicht mehr verfolgt.
gehört hinein gehört nicht hinein
Code und Skripte Zugangsdaten und Passwörter
Konfiguration ohne Geheimnisse virtuelle Umgebungen
Abhängigkeitslisten grosse Rohdaten
Dokumentation generierte Ausgaben

Interpretation und Ergebnissatz

git rm --cached nimmt die Datei aus der Verfolgung, nicht aus der Historie. Das Passwort steht weiterhin im alten Commit und lässt sich dort lesen. Es muss deshalb als kompromittiert gelten und gewechselt werden; das Bereinigen der Historie ist aufwendig und hilft nicht, wenn jemand den Stand bereits geholt hat. Deshalb steht .gitignore am Anfang eines Projekts und nicht am Ende.

Die ignorierte Rohdatei taucht in git status gar nicht auf. Eine mit -f erzwungene Datei steht dagegen in ls-files und verschwindet erst durch git rm --cached aus der Verfolgung, bleibt aber in der Historie und auf der Platte.

Verständnisfragen

Warum zeigt git status eine Datei als ?? an, obwohl sie im Ordner liegt?

Git verfolgt Dateien nicht automatisch
Richtig. Eine neue Datei ist zunächst unbekannt und wird erst durch git add aufgenommen. Das ist Absicht, sonst landeten Zwischenergebnisse und Zugangsdaten ungewollt im Repository.
Die Datei steht in .gitignore
Dann erschiene sie gar nicht in der Ausgabe, wie die Rohdatei in Beispiel 6.
Der letzte Commit fehlt noch
Der Zustand hängt daran, dass die Datei nie vorgemerkt wurde.

Was ist der Unterschied zwischen git add und git commit?

add merkt Änderungen vor, commit schreibt das Vorgemerkte fest
Richtig. Der Zwischenschritt erlaubt es, aus vielen Änderungen gezielt eine zusammengehörige Auswahl festzuhalten, wie in Beispiel 2.
add speichert lokal, commit überträgt auf den Server
Beides bleibt lokal. Übertragen wird erst mit push.
add gilt für neue Dateien, commit für geänderte
add wird für beide gebraucht.

git status zeigt UU analyse.R. Was ist zu tun?

Die Datei öffnen, zwischen den Markierungen entscheiden und den Commit abschliessen
Richtig. UU heisst, dass beide Seiten dieselbe Stelle geändert haben. Nach dem Auflösen folgt git add und ein gewöhnlicher Commit.
Den Merge abbrechen, Konflikte sind ein Fehler
Sie sind normal. git merge --abort ist nur dann richtig, wenn man den Merge überhaupt nicht wollte.
Die Datei löschen und neu schreiben
Das verwirft beide Änderungen.

Ein Commit wurde schon gepusht und soll rückgängig gemacht werden. Welcher Befehl ist richtig?

git revert, weil es einen neuen Commit anlegt und die Historie nicht umschreibt
Richtig. Wer den alten Stand bereits geholt hat, bekommt einfach den neuen Commit dazu.
git reset --hard, das ist gründlicher
Es schreibt die Historie um; beim nächsten Push bräuchte es --force, und alle anderen bekommen Probleme.
git restore
Das wirkt auf Dateien im Arbeitsverzeichnis, nicht auf Commits.

Eine Datei mit Zugangsdaten wurde versehentlich committet und mit git rm --cached wieder entfernt. Ist das Problem damit gelöst?

Ja, die Datei ist nicht mehr im Repository
Sie ist nicht mehr verfolgt, steht aber weiterhin im alten Commit.
Nein, das Passwort steht in der Historie und muss als kompromittiert gelten
Richtig. Es gehört gewechselt. Das Bereinigen der Historie ist aufwendig und hilft nicht, wenn jemand den Stand schon geholt hat.
Nein, die Datei muss auch von der Platte gelöscht werden
Lokal darf sie bleiben, sie ist ja ignoriert.

Warum erzeugt ein Jupyter-Notebook auch ohne inhaltliche Änderung einen grossen Diff?

Es ist JSON und enthält Ausgaben, Bilder und Ausführungszähler
Richtig. Schon ein erneuter Durchlauf ändert die Zähler. Wer die Ausgaben vor dem Commit leert, bekommt eine lesbare Historie.
Git kann JSON nicht vergleichen
Es vergleicht JSON wie jeden anderen Text, genau darin liegt das Problem.
Notebooks werden immer als Binärdateien behandelt
Sie sind Text; unlesbar ist nur der Diff.

Verlinkte Ressourcen