Die Commit-Historie als Lernpfad
Dieses Repository ist so committet, dass git log selbst ein lesbares TDD-Transkript ist.
Diese Seite erklärt die Konvention und wie du sie zum Nacherleben eines Use-Case nutzt.
Commit-Konvention
| Präfix | Bedeutung | TDD-Phase |
|---|---|---|
test(<scope>): [RED] ... |
neuer, absichtlich fehlschlagender Test — sonst keine Produktionsänderung | Red |
feat(<scope>): [GREEN] ... |
gerade genug Produktionscode, um den zuletzt hinzugefügten Test grün zu machen | Green |
refactor(<scope>): ... |
Strukturverbesserung ohne Verhaltensänderung, Tests bleiben grün | Refactor |
docs(<scope>): ... |
Dokumentationsänderung (ADR, Doku-Seite, README) | — |
build(<scope>): ... |
Build-/Abhängigkeitsänderung (POM, Plugin-Konfiguration) | — |
chore(<scope>): ... |
Modul-/Verzeichnis-Gerüst, ohne eigenes Verhalten | — |
fix(<scope>): ... |
Korrektur eines eigenen vorherigen Fehlers (z. B. in der POM-Konfiguration) | — |
<scope> ist meist der Modulname (common-domain, accounts-service, ...), bei
modulübergreifenden Änderungen der grobe Bereich.
Zwei Granularitätsstufen in diesem Projekt
Die ersten Use-Cases (common-domain, common-events, customer-service bis einschließlich
"Kunde registrieren") committen pro Klasse: ein RED-Commit pro neuem Testfall, ein
GREEN-Commit pro dafür nötiger Produktionsklasse. Das zeigt die Methode im Detail, erzeugt aber
sehr viele Commits.
Ab "Identität verifizieren" (siehe TDD-Katalog) gilt eine gröbere Regel, die sich in der Praxis besser bewährt hat: ein Commit pro Phasen-Übergang (Rot → Gelb/Refactor → Grün), auch wenn dabei mehrere Dateien gleichzeitig entstehen (z. B. ein Domain-Test, ein Application-Test und ein Web-Slice-Test für denselben Anwendungsfall zusammen als ein RED-Commit, die zugehörigen drei Produktionsklassen zusammen als ein GREEN-Commit). Der didaktische Kern - "erst ein fehlschlagender Test, dann die minimale Implementierung" - bleibt erhalten; nur die Commit-Anzahl sinkt. Das ist selbst eine reale TDD-Praxis-Lektion: Commit-Granularität ist eine bewusste Abwägung zwischen maximaler Nachvollziehbarkeit und praktikabler Historie, keine feste Regel - und darf sich, wie hier, sogar innerhalb eines Projekts ändern, wenn sich zeigt, dass die ursprüngliche Granularität zu fein war.
Wie man einen Use-Case Schritt für Schritt nachvollzieht
Jede Zeile im TDD-Katalog verweist auf den ersten und letzten Commit-Hash eines Use-Case. Um die komplette Entwicklung nachzuvollziehen:
# Alle Commits zwischen Start- und End-Hash eines Use-Case, aeltester zuerst
git log --oneline --reverse <start-hash>^..<end-hash>
# Nur die Aenderungen an einer bestimmten Testdatei ueber die Zeit verfolgen
git log --oneline --follow -- accounts-service/src/test/java/.../OpenAccountUseCaseTest.java
# Eine einzelne Commit-Aenderung ansehen (z.B. den GREEN-Schritt, der einen Test zum
# Laufen brachte)
git show <hash>
# Den Diff zwischen einem RED- und dem zugehoerigen GREEN-Commit ansehen -
# zeigt exakt, wie viel (bzw. wie wenig) Produktionscode fuer ein "Gruen" noetig war
git diff <red-hash> <green-hash>
Übungsvorschlag für Einsteiger:innen: Nimm den allerersten Use-Case dieses Projekts —
Money-Value-Object in common-domain — und lies dir die Commits von
chore: Reactor-Skeleton ... bis refactor(common-domain): Waehrungspruefung ... extrahiert in
chronologischer Reihenfolge durch (git log --oneline --reverse). Du wirst sehen, wie aus vier
kleinen Red-Green-Paaren plus einem Refactor-Schritt ein vollständiges, invariantengeschütztes
Value Object entsteht — ganz ohne dass an irgendeiner Stelle mehr Code geschrieben wurde, als der
jeweils aktuelle Test verlangte.
Exkurs: feingranulare Commits nachträglich zu Phasen-Commits verdichten
Dieses Projekt hat die feingranularen Commits der ersten Use-Cases bewusst stehen lassen (siehe oben) - aber die Frage "wie würde man das nachträglich verdichten?" ist selbst lehrreich genug für eine eigene Notiz, auch ohne es hier tatsächlich auszuführen.
Zwei Standardwerkzeuge dafür, beide git-nativ:
1. git reset --soft + neu committen (einfachste Variante für "die letzten N Commits zu einem
verschmelzen"):
# Beispiel: die neun Money-Commits dieses Projekts (RED/GREEN x4 + 1 Refactor,
# e63d924..82d72f9) zu drei Phasen-Commits verdichten.
# 1. Zurueck auf den Stand VOR dem ersten Money-Commit, Arbeitsverzeichnis bleibt unveraendert
git reset --soft e63d924~1
# 2. Jetzt liegen alle Aenderungen der neun Commits gemeinsam in der Staging Area.
# Selektiv wieder committen, z.B. nur die Testdateien fuer den RED-Commit:
git reset HEAD . # erstmal alles unstagen
git add **/MoneyTest.java
git commit -m "test(common-domain): [RED] Money - Erzeugung, Validierung, Addition"
# 3. Dann die Produktionsklassen fuer GREEN:
git add **/Money.java **/DomainException.java **/InvariantViolationException.java
git commit -m "feat(common-domain): [GREEN] Money-Value-Object mit Validierung und Addition"
# 4. Der Refactor-Commit war inhaltlich bereits sauber getrennt - so uebernehmen
# (Diff pruefen, dann committen)
2. git rebase -i (interaktives Rebase mit squash/fixup) - praktischer, wenn die
Commits nicht direkt hintereinander sauber nach RED/GREEN/REFACTOR sortiert sind:
git rebase -i e63d924~1
# Im Editor: erster Commit jeder Phase bleibt "pick", alle folgenden Commits derselben Phase
# werden auf "squash" (verschmilzt Commit-Historie UND fragt nach neuer Nachricht) oder
# "fixup" (verschmilzt still, behaelt die Nachricht des "pick"-Commits) gesetzt, z.B.:
# pick e63d924 test(common-domain): [RED] Money.of erzeugt Geldbetrag ...
# fixup 3e4a8c8 feat(common-domain): [GREEN] Money als Record ...
# pick fbceaaa test(common-domain): [RED] Money.of lehnt negativen Betrag ab
# fixup e89c607 feat(common-domain): [GREEN] Money.of validiert Betrag ...
# ...
Wichtige Randbedingungen, bevor man das tatsächlich tut:
- Beide Varianten schreiben Commit-Hashes um. Absolut unproblematisch auf einem lokalen,
ungeteilten Branch (wie in diesem Lernprojekt) - gefährlich, sobald der Branch schon von
jemand anderem gepullt/darauf aufgebaut wurde (dann bräuchte es eine abgestimmte
Force-Push-Aktion, siehe Git-Grundlagen zu
--force-with-lease). - Der TDD-Katalog referenziert konkrete Commit-Hashes - nach einem Squash müssten diese Verweise aktualisiert werden.
- Man verliert die feinste Nachvollziehbarkeit (z. B. "genau dieser eine Halbsatz Code hat diesen einen Test gruen gemacht") zugunsten einer kürzeren, leichter lesbaren Historie. Das ist exakt der Trade-off aus dem Abschnitt "Zwei Granularitätsstufen in diesem Projekt" oben - nur eben nachträglich statt von Anfang an gewählt.
In diesem Projekt bleibt die bestehende Historie unverändert (siehe Baufortschritt) - dieser Exkurs dokumentiert das Vorgehen als Lektion, nicht als durchgeführte Aktion.
Warum diese Disziplin (und ihre Grenzen)
Der Nutzen dieser feinkörnigen Historie ist didaktisch: Sie macht den TDD-Prozess selbst zu einem prüfbaren Artefakt, nicht nur zu einer Behauptung im Projekt-README. Für ein reales Produktionsprojekt außerhalb eines Lernkontexts wäre diese Granularität an Commits meist zu fein für den täglichen Arbeitsfluss (typischerweise squasht man vor dem Merge in einen Feature-Branch) — hier bleibt sie bewusst erhalten, weil das Nachvollziehen der Methode selbst das Lernziel ist, nicht nur das fertige Produkt.