Zum Inhalt springen

8. Versionsverwaltung mit Git

Zu Zen-Modus wechseln

Wer länger an einem Projekt arbeitet, kennt das Problem der Versionsstände: project.py, project_old.py, project_final.py, project_final_v2.py, project_final_v2_FIXED.py. Niemand weiß mehr, welche Datei welchen Stand enthält, was sich zwischen den Ständen geändert hat und welche Fassung eigentlich funktioniert hat. In Teamarbeit verschärft sich das Problem: Zwei Personen ändern dieselbe Datei, und beim Zusammenkopieren geht Arbeit verloren.

Versionsverwaltung löst genau das. Ein Versionsverwaltungssystem protokolliert die Geschichte eines Projekts als Folge von nachvollziehbaren Ständen, erlaubt die Rückkehr zu jedem früheren Stand und führt die Arbeit mehrerer Personen kontrolliert zusammen. Das mit Abstand am weitesten verbreitete System ist Git, entwickelt 2005 von Linus Torvalds für die Arbeit am Linux-Kernel. Git ist heute in der Softwareentwicklung so grundlegend wie ein Editor; auch außerhalb der Programmierung wird es eingesetzt, etwa für Dokumentation, Konfiguration oder wissenschaftliche Arbeiten.

Bevor Kommandos ins Spiel kommen, die Begriffe. Sie gelten für jedes Versionsverwaltungssystem und sind ausdrücklich Prüfungsstoff.

  • Repository: die Datenbank mit der gesamten Geschichte eines Projekts. Bei Git liegt sie in einem versteckten Ordner .git direkt im Projektverzeichnis; das Projekt trägt seine Geschichte also vollständig mit sich.
  • Commit: ein dauerhaft gespeicherter Schnappschuss des gesamten Projekts zu einem Zeitpunkt, zusammen mit Autor, Datum, einer Beschreibung und einem Verweis auf den Vorgänger-Commit. Jeder Commit hat eine eindeutige ID (einen 40-stelligen Hash wie a3f8e21..., von dem im Alltag die ersten sieben Zeichen genügen).
  • Historie: die Kette der Commits, jeder mit Verweis auf seinen Vorgänger. Sie beantwortet die Fragen: Was wurde geändert? Wann? Von wem? Und, bei guten Beschreibungen: Warum?
  • Branch: eine benannte, unabhängige Entwicklungslinie. Branches erlauben es, an einer neuen Funktion zu arbeiten, ohne den funktionierenden Hauptstand anzutasten.
  • Merge: das Zusammenführen zweier Entwicklungslinien zu einem gemeinsamen Stand.
  • Remote: ein entferntes Repository auf einem Server, über das Teammitglieder ihre Stände austauschen.

Git unterscheidet drei Bereiche, zwischen denen Änderungen wandern. Dieses Modell zu verstehen erspart die meiste Verwirrung im Umgang mit den Kommandos:

flowchart LR
    wd["Working Directory<br>(die Dateien, die Sie editieren)"]
    st["Staging Area<br>(die Auswahl für den nächsten Commit)"]
    repo["Repository<br>(die gespeicherte Historie)"]
    wd -- "git add" --> st
    st -- "git commit" --> repo
    repo -- "git restore / checkout" --> wd
  • Das Working Directory sind die gewöhnlichen Projektdateien, an denen gearbeitet wird.
  • Die Staging Area ist ein Zwischenschritt: Mit git add wird ausgewählt, welche Änderungen in den nächsten Commit aufgenommen werden. Das erlaubt es, von fünf geänderten Dateien nur die drei zusammengehörigen zu committen und den Rest für einen späteren, thematisch eigenen Commit aufzuheben.
  • Das Repository enthält die committeten, dauerhaften Stände.

Git wird von git-scm.com installiert (unter Linux über den Paketmanager, unter Windows als “Git for Windows” inklusive Git Bash). Einmalig werden Name und E-Mail-Adresse hinterlegt; beide landen in jedem Commit:

Terminal-Fenster
git config --global user.name "Anna Muster"
git config --global user.email "[email protected]"

Ein neues Repository entsteht mit git init im Projektordner. Danach besteht die tägliche Arbeit aus einem Kreislauf von vier Kommandos:

Terminal-Fenster
git status # what has changed?
git add timecode.py # stage the change
git commit -m "Add input validation for negative frame numbers"
git log --oneline # show the history

Ein Beispieldurchlauf mit den zugehörigen Ausgaben:

Terminal-Fenster
$ git status
On branch main
Changes not staged for commit:
modified: timecode.py
Untracked files:
notes.txt
$ git add timecode.py
$ git commit -m "Add input validation for negative frame numbers"
[main 4f2a9c1] Add input validation for negative frame numbers
1 file changed, 4 insertions(+)
$ git log --oneline
4f2a9c1 Add input validation for negative frame numbers
b81d3e0 Extract timecode conversion into a function
a3f8e21 Initial version of the timecode tool

git status ist das wichtigste Kommando von allen: Es zeigt jederzeit, welche Dateien geändert, welche gestaged und welche unbekannt sind, und schlägt sogar die passenden nächsten Kommandos vor. Bei Unsicherheit gilt immer: zuerst git status.

Zwei weitere Kommandos vervollständigen den Alltag:

  • git diff zeigt zeilengenau, was sich seit dem letzten Commit geändert hat; git diff --staged zeigt, was der nächste Commit enthalten wird. Vor jedem Commit einen Blick auf das Diff zu werfen ist eine Gewohnheit, die versehentliche Commits von Debug-Ausgaben und Experimenten verhindert.
  • git restore <file> verwirft nicht committete Änderungen einer Datei und stellt den letzten committeten Stand her. Achtung: Die verworfenen Änderungen sind damit tatsächlich weg.

Die Historie ist nur so nützlich wie ihre Commits. Zwei Regeln:

  1. Ein Commit, ein Thema. Ein Commit fasst eine zusammengehörige Änderung: eine Funktion, eine Fehlerbehebung, eine Umbenennung. Ein Commit “alles von heute” mit zwölf durchmischten Änderungen ist bei der Fehlersuche wertlos, denn er lässt sich weder verstehen noch gezielt zurücknehmen.
  2. Die Message beschreibt die Änderung. Bewährte Konvention: eine kurze Zeile im Imperativ, die den Satz “Dieser Commit wird …” vervollständigt. Add input validation for negative frame numbers erfüllt das; changes, fix oder asdf nicht. Wer in drei Monaten per git log nach der Ursache eines Fehlers sucht, liest ausschließlich diese Zeilen.

Nicht alles im Projektordner gehört unter Versionskontrolle. Automatisch erzeugte Dateien (__pycache__/, virtuelle Umgebungen, Renderergebnisse, Exportdateien) werden in einer Datei .gitignore im Projektwurzelverzeichnis aufgezählt und von Git fortan ignoriert:

__pycache__/
.venv/
*.mp4
exports/

Grundregel: Ins Repository gehört, was Menschen geschrieben haben (Quellcode, Konfiguration, Dokumentation); was daraus erzeugt werden kann, bleibt draußen. Gerade in Medienprojekten ist das relevant: Git ist für Textdateien gebaut. Große Binärdateien wie Videomaterial blähen das Repository dauerhaft auf, denn Git behält jede jemals committete Version. Rohmaterial und Renderergebnisse werden daher außerhalb verwaltet; versioniert wird das Projekt, nicht das Material.

Der Hauptbranch (üblicherweise main) soll jederzeit einen funktionierenden Stand enthalten. Neue Funktionen, Experimente und größere Umbauten finden auf eigenen Branches statt und kommen erst nach Fertigstellung zurück:

Terminal-Fenster
git branch feature-export # create a new branch
git switch feature-export # start working on it
# ... edit, add, commit as usual ...
git switch main # back to the main line
git merge feature-export # bring the finished work into main
git branch -d feature-export # delete the merged branch
gitGraph
    commit id: "a3f8e21"
    commit id: "b81d3e0"
    branch feature-export
    commit id: "c92f1a4"
    commit id: "d15e8b7"
    checkout main
    commit id: "e77c203"
    merge feature-export

Ein Branch ist bei Git nur ein beweglicher Zeiger auf einen Commit; das Anlegen kostet nichts und dauert keine Sekunde. Entsprechend niedrig soll die Hemmschwelle sein: Jede abgegrenzte Aufgabe bekommt ihren Branch. Der Wert zeigt sich spätestens dann, wenn mitten in einer halbfertigen Funktion ein dringender Fehler im Hauptstand zu beheben ist: git switch main, Fehler beheben, committen, zurück auf den Feature-Branch, weiterarbeiten. Ohne Branches müsste die halbfertige Arbeit erst irgendwie beiseitegeräumt werden.

git merge führt die Linien zusammen. Haben sich beide Seiten seit der Abzweigung geändert, erzeugt Git einen Merge-Commit mit zwei Vorgängern (wie im Diagramm oben); die Historie wird damit zu einem Graphen statt einer Kette.

Ändern zwei Branches dieselben Zeilen derselben Datei, kann Git nicht wissen, welche Fassung gelten soll, und meldet einen Konflikt:

Auto-merging timecode.py
CONFLICT (content): Merge conflict in timecode.py
Automatic merge failed; fix conflicts and then commit the result.

Wichtig vorweg: Es ist nichts kaputt und nichts verloren. Git hat lediglich die Entscheidung an den Menschen zurückgegeben und markiert die strittigen Stellen in der Datei:

def frames_to_timecode(frames, fps=25):
<<<<<<< HEAD
if frames < 0:
raise ValueError("frame number must be >= 0")
=======
if frames < 0:
frames = 0
>>>>>>> feature-export
total_seconds = frames // fps

Zwischen <<<<<<< HEAD und ======= steht die Fassung des aktuellen Branches, darunter bis >>>>>>> die des gemergten Branches. Die Auflösung ist Handarbeit mit drei Schritten:

  1. Entscheiden. Die Datei öffnen und pro Konfliktstelle festlegen, was gelten soll: die eine Fassung, die andere oder eine Kombination. Das ist eine inhaltliche Entscheidung; im Beispiel oben stehen zwei unvereinbare Fehlerstrategien gegeneinander, und ein Werkzeug kann diese Entscheidung prinzipiell nicht treffen. VS Code zeigt Konflikte mit klickbaren Auswahlknöpfen an, die Entscheidung selbst bleibt beim Menschen.

  2. Marker entfernen. Alle Zeilen mit <<<<<<<, ======= und >>>>>>> müssen verschwinden; übrig bleibt die gewünschte, lauffähige Fassung.

  3. Testen, dann abschließen. Das Programm ausführen, dann git add timecode.py und git commit. Der Merge ist damit abgeschlossen.

Konflikte sind kein Zeichen für Fehler, sondern die logische Folge paralleler Arbeit. Sie bleiben klein, wenn Branches kurzlebig sind und regelmäßig gemergt wird; gefürchtet werden nur Konflikte nach wochenlanger getrennter Entwicklung.

Remote-Repositories: Zusammenarbeit über den Server

Abschnitt betitelt „Remote-Repositories: Zusammenarbeit über den Server“

Bisher lag alles lokal. Für Abgaben, Backups und Teamarbeit kommt ein Remote dazu: ein Repository auf einem Server, im Unterricht auf dem GitLab der Schule. Die Verbindung läuft über drei Kommandos:

  • git clone <url> holt ein bestehendes Repository vom Server, inklusive der gesamten Historie. Damit beginnt die Arbeit an jedem Übungsprojekt.
  • git push überträgt die eigenen neuen Commits zum Server.
  • git pull holt neue Commits vom Server in das lokale Repository.

Der Arbeitstag in Teamarbeit folgt damit einem festen Rhythmus: mit git pull den aktuellen Stand holen, lokal arbeiten und committen, mit git push die Ergebnisse teilen. Wer vor dem Push von einem Kollegen überholt wurde, muss zuerst pullen; haben beide dieselben Zeilen geändert, entsteht dabei ein gewöhnlicher Merge-Konflikt, der wie oben aufgelöst wird.

Für die Übungsabgaben gilt ab jetzt der Git-Workflow: Jede Übung ist ein Repository auf dem Schul-GitLab, abgegeben wird durch Push, und die Commit-Historie ist Teil der Abgabe. Eine Abgabe, die aus einem einzigen Commit “final” um 23:58 Uhr besteht, erzählt eine andere Geschichte als eine mit zwölf nachvollziehbaren Schritten über eine Woche.

Mit dem kompletten Werkzeugkasten lassen sich die eingangs genannten Probleme abhaken. Die Dateinamens-Versionierung (final_v2_FIXED) ersetzt die Historie: git log zeigt alle Stände, git diff a3f8e21 4f2a9c1 zeigt die Änderungen zwischen zwei beliebigen Ständen, und git switch --detach a3f8e21 stellt jeden früheren Stand vollständig wieder her. Verlorene Arbeit durch Zusammenkopieren ersetzt der Merge mit expliziter Konfliktauflösung. Und die Frage “Wer hat das geändert, und warum?” beantwortet git log <file> samt Commit-Messages, pro Datei und auf Wunsch pro Zeile (git blame).

Versionsverwaltung ist damit kein Zusatzwerkzeug für große Teams, sondern die Grundlage nachvollziehbaren Arbeitens, auch allein. Ab diesem Kapitel wird jedes Projekt dieses Schuljahres in einem Git-Repository geführt, einschließlich des Abschlussprojekts.

KommandoZweck
git initneues Repository im aktuellen Ordner anlegen
git statusZustand von Working Directory und Staging zeigen
git add <file>Änderung für den nächsten Commit vormerken
git commit -m "..."vorgemerkte Änderungen dauerhaft speichern
git log --onelineHistorie kompakt anzeigen
git diff / git diff --stagedÄnderungen zeilengenau anzeigen
git restore <file>lokale Änderungen verwerfen
git branch <name> / git switch <name>Branch anlegen / wechseln
git merge <name>Branch in den aktuellen Branch zusammenführen
git clone <url>Repository vom Server holen
git pull / git pushCommits vom/zum Server übertragen

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Erklären: die Konzepte der Versionsverwaltung (Repository, Commit, Historie, Branch, Merge, Remote) mit eigenen Worten erklären.
  • Erklären: das Drei-Bereiche-Modell von Git (Working Directory, Staging Area, Repository) und den Weg einer Änderung hindurch beschreiben.
  • Anwenden: den täglichen Zyklus aus status, add, commit, log und diff sicher anwenden und aussagekräftige Commit-Messages schreiben.
  • Anwenden: Branches anlegen, zwischen ihnen wechseln, mergen und Merge-Konflikte kontrolliert auflösen.
  • Anwenden: mit Remote-Repositories arbeiten (clone, push, pull) und den Abgabe-Workflow über das Schul-GitLab einhalten.
  • Beurteilen: entscheiden, welche Dateien unter Versionskontrolle gehören und welche per .gitignore ausgeschlossen werden.
  • Aufgabe 15 - Git I: Zeitmaschine
  • Aufgabe 16 - Git II: Abzweigungen