git add -p, git rebase -i und die Kunst der sauberen Commit-Historie
Monster-Commits mit gemischten Refactorings, Bugfixes und neuen Features machen Code-Reviews langsam, git bisect nutzlos und einzelne Änderungen unmöglich rückgängig zu machen. Mit git add -p, git commit -p und git rebase -i lässt sich jede Änderung in klar abgegrenzte, atomare Commits aufteilen, die einzeln nachvollziehbar, testbar und im Zweifel gezielt revertierbar bleiben.
Inhaltsverzeichnis
- 1. Was einen Commit atomar macht
- 2. Das Problem mit Monster-Commits
- 3. Gemischte Änderungen im Working Tree erkennen
- 4. git add -p / git add -i im Detail
- 5. Hunks weiter teilen mit s und manuell editieren mit e
- 6. git commit -p als Abkürzung
- 7. Nachträglich aufteilen: git reset -p und git rebase -i
- 8. Der Payoff: git bisect, git revert und git cherry-pick
- 9. Team-Workflow, Konventionen und Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was einen Commit atomar macht
Ein atomarer Commit enthält genau eine logische Änderung und nichts sonst. Nicht eine Zeile, nicht eine Datei, sondern eine in sich abgeschlossene Idee: ein Bugfix, eine neue Methode, eine Umbenennung. Der Test dafür ist einfach: Lässt sich der Commit in einem Satz ohne das Wort "und" beschreiben? Wenn die Antwort "Fixt den Rabatt-Rechner und räumt nebenbei die Imports auf" lautet, ist der Commit nicht atomar, sondern ein Kompromiss aus zwei unabhängigen Absichten.
Atomar bedeutet außerdem: Der Commit lässt sich isoliert bauen und testen. Nach einem git checkout <commit> sollte das Projekt kompilieren, die Test-Suite laufen und die Anwendung sich in einem konsistenten Zustand befinden, selbst wenn spätere Commits noch fehlen. Das ist der entscheidende Unterschied zu willkürlich kleinen Commits: Atomare Commits sind so klein wie möglich, aber nicht kleiner, als es die Konsistenz des Projekts erlaubt.
2. Das Problem mit Monster-Commits
Ein Monster-Commit vermischt Refactoring, neue Features und Bugfixes in einer einzigen Änderung mit hunderten betroffenen Zeilen über dutzende Dateien. Für den Reviewer ist ein solcher Commit praktisch unlesbar: Welche Zeilenänderung ist beabsichtigt, welche ist Nebenwirkung eines automatischen Refactorings? In der Praxis führt das dazu, dass Reviewer den Diff nur noch überfliegen und pauschal genehmigen, statt jede Änderung wirklich zu verstehen. Genau dort entstehen die Bugs, die später niemand mehr zuordnen kann.
git bisect wird bei Monster-Commits nutzlos: Die Binärsuche landet zwangsläufig bei dem 800-Zeilen-Commit, der drei unabhängige Änderungen enthält, und liefert keine Antwort darauf, welche der drei die Regression verursacht hat. Auch git revert und git cherry-pick scheitern an vermischten Änderungen: Ein Revert reißt ungewollt auch das mit, was eigentlich bleiben sollte, und ein Cherry-Pick auf einen Hotfix-Branch schleppt Refactorings mit, die dort niemand haben will.
3. Gemischte Änderungen im Working Tree erkennen
Monster-Commits entstehen selten absichtlich. Typischerweise beginnt die Arbeit an einem Bugfix, und beim Lesen des Codes fällt eine unschöne Variable, eine fehlende Typisierung oder ein veralteter Kommentar auf, und wird nach dem Motto "während ich schon hier bin" gleich mit erledigt. Nach zwei Stunden Arbeit zeigt git status plötzlich zwölf geänderte Dateien, obwohl der ursprüngliche Bugfix nur zwei betrifft.
Der Moment, in dem man das erkennen sollte, ist vor dem Commit, nicht danach. git diff --stat gibt einen schnellen Überblick über die Anzahl geänderter Dateien und Zeilen. Ein genauerer Blick mit git diff zeigt, ob innerhalb einer einzelnen Datei mehrere unabhängige Hunks stecken. Die Faustregel: Sobald der Commit-Message-Entwurf ein "und" oder eine Aufzählung enthält, steckt mehr als eine logische Änderung im Working Tree, und es lohnt sich, vor dem Commit aufzuteilen statt danach zu reparieren.
4. git add -p / git add -i im Detail: Hunks interaktiv stagen
git add -p (Kurzform für --patch) durchläuft jeden Hunk, also jeden zusammenhängenden Änderungsblock, einzeln und fragt, ob er in die Staging-Area soll. Die wichtigsten Tasten: y stagt den Hunk, n überspringt ihn, s versucht, den Hunk in kleinere Teile zu splitten, e öffnet den Editor für manuelle Anpassung, q beendet die Session sofort. So lässt sich aus einem Working Tree mit gemischten Änderungen gezielt nur der Teil stagen, der zum aktuellen Commit gehört; der Rest bleibt unstaged für den nächsten Commit.
git add -i öffnet stattdessen ein interaktives Menü mit Optionen wie status, update, patch und diff. Der Menüpunkt patch entspricht git add -p, bietet aber zusätzlich die Möglichkeit, vorher gezielt Dateien auszuwählen, für die der Patch-Modus laufen soll. Das ist praktisch, wenn nur zwei von zehn geänderten Dateien den aktuellen Commit betreffen.
$ git add -p
diff --git a/src/Discount/PercentageCalculator.php b/src/Discount/PercentageCalculator.php
@@ -12,7 +12,7 @@ class PercentageCalculator
- return $price * $percentage;
+ return round($price * $percentage / 100, 2);
Stage this hunk [y,n,q,a,d,s,e,?]? y
diff --git a/src/Discount/PercentageCalculator.php b/src/Discount/PercentageCalculator.php
@@ -30,6 +30,10 @@ class PercentageCalculator
+ // TODO: remove after Q3 migration
+ private function legacyFallback(): float
+ {
+ return 0.0;
+ }
Stage this hunk [y,n,q,a,d,s,e,?]? n
5. Hunks weiter teilen mit s und manuell editieren mit e
Manchmal enthält ein einzelner Hunk immer noch zwei unabhängige Änderungen, etwa weil beide dieselbe oder eine benachbarte Zeile betreffen. Die Taste s versucht dann, den Hunk in kleinere, unabhängig stagbare Teile zu zerlegen. Das gelingt jedoch nicht immer: Git kann einen Hunk nur splitten, wenn zwischen den Änderungen unveränderte Kontextzeilen liegen. Bei zwei Änderungen in direkt aufeinanderfolgenden Zeilen bricht s mit der Meldung ab, dass der Hunk nicht weiter geteilt werden kann.
Für genau diesen Fall gibt es e. Der Editor öffnet den Hunk als Patch-Text, in dem Zeilen mit + oder - markiert sind. Um eine Zeile aus dem zu stagenden Patch auszuschließen, wird eine +-Zeile einfach gelöscht, eine --Zeile hingegen in eine Kontextzeile ohne Präfix umgewandelt. Git prüft anschließend, ob der editierte Patch konsistent anwendbar ist. Bei Syntaxfehlern im editierten Patch bricht der Vorgang ohne Änderung ab, sodass nichts kaputtgehen kann.
# Manual edit mode (e): only keep the null-check, drop the log line
# Original hunk shown by git add -p -e:
@@ -45,6 +45,8 @@ class OrderValidator
if ($order === null) {
throw new InvalidArgumentException('Order must not be null');
}
+ $this->logger->debug('Validating order ' . $order->getId());
+ // Remove the debug line below by deleting the '+' line entirely
+ if (!$order->getItems()) {
# After editing: the debug-log '+' line is deleted,
# only the null-check hunk remains staged
6. git commit -p als Abkürzung: eine saubere Commit-Sequenz bauen
git commit -p kombiniert git add -p und git commit in einem Schritt: Git fragt Hunk für Hunk ab, öffnet danach direkt den Editor für die Commit-Message der gerade zusammengestellten Änderung und committet nur diesen Teil. Der Rest bleibt unstaged und unverändert im Working Tree für den nächsten Durchlauf. Für das Aufteilen eines gemischten Working Trees in mehrere atomare Commits ist das oft schneller als add -p gefolgt von einem separaten commit.
Die Praxis: git commit -p so oft wiederholen, bis git status einen sauberen Working Tree zeigt. Jeder Durchlauf bekommt eine eigene, präzise Commit-Message. Zur Kontrolle danach git log --oneline und git show <commit> für jeden einzelnen Commit. Wenn jeder Commit in einem Satz beschreibbar ist und für sich baut, war die Aufteilung erfolgreich.
# Working tree has three unrelated changes mixed together
$ git status --short
M src/Discount/PercentageCalculator.php
M src/Order/OrderValidator.php
M composer.json
# Build three atomic commits directly, one git commit -p per concern
$ git commit -p
# ... select only the rounding-fix hunk, then write the message:
# "fix: round percentage discount to 2 decimals"
$ git commit -p
# ... select only the null-check hunk:
# "fix: reject null order in OrderValidator"
$ git add composer.json && git commit -m "chore: bump phpunit to 10.5"
$ git log --oneline -3
a1b2c3d chore: bump phpunit to 10.5
e4f5g6h fix: reject null order in OrderValidator
i7j8k9l fix: round percentage discount to 2 decimals
7. Nachträglich aufteilen: git reset -p und git rebase -i
Wenn der Monster-Commit bereits existiert, ist er noch nicht verloren, solange er nicht in einen geteilten Branch gepusht wurde. git rebase -i HEAD~3 öffnet die letzten drei Commits zur Bearbeitung; der betroffene Commit wird von pick auf edit gesetzt. Sobald der Rebase dort anhält, setzt git reset HEAD^ den Commit zurück, ohne die Dateiänderungen zu verwerfen. Alles landet unstaged im Working Tree, bereit für git add -p und mehrere neue, atomare Commits.
git reset -p funktioniert ähnlich, aber direkter: Es entstagt Änderungen hunkweise aus der Staging-Area, ohne den gesamten Commit zurückzusetzen. Das ist nützlich, wenn zu viel auf einmal gestaged wurde, bevor committet wurde. Wichtig bei beiden Verfahren: Das Umschreiben von Historie mit rebase ist nur unproblematisch, solange die betroffenen Commits noch nicht gepusht oder von anderen Team-Mitgliedern ausgecheckt wurden. Nach dem Push nur mit --force-with-lease und expliziter Absprache im Team.
$ git rebase -i HEAD~3
# editor shows:
pick i7j8k9l fix: round percentage discount to 2 decimals
edit e4f5g6h fix: reject null order + bump phpunit in one commit
pick a1b2c3d docs: update changelog
# Rebase stops at the "edit" commit
$ git reset HEAD^
# changes are now unstaged, commit is undone, files intact
$ git add -p
# stage only the null-check hunk, then:
$ git commit -m "fix: reject null order in OrderValidator"
$ git add composer.json
$ git commit -m "chore: bump phpunit to 10.5"
$ git rebase --continue
8. Der Payoff: git bisect, git revert und git cherry-pick
Mit einer atomaren Historie liefert git bisect ein exaktes Ergebnis. Die Binärsuche halbiert bei jedem Schritt den Commit-Bereich, und wenn jeder Commit genau eine logische Änderung enthält, zeigt git bisect am Ende exakt die eine Zeile, die die Regression verursacht hat, und nicht einen 800-Zeilen-Commit, in dem noch drei weitere unabhängige Änderungen gesucht werden müssten. Mit git bisect run lässt sich dieser Prozess sogar vollständig automatisieren, solange ein Testskript den fehlerhaften Zustand zuverlässig erkennt.
Auch git revert und git cherry-pick profitieren direkt: Ein Revert eines atomaren Commits hebt genau eine Änderung sauber auf, ohne dass ungewollt Refactorings oder unabhängige Fixes mit verschwinden. Ein Cherry-Pick auf einen Hotfix- oder Release-Branch lässt sich gezielt auf den einen Fix beschränken, ohne dass Merge-Konflikte durch mitgeschlepptes, dort unerwünschtes Refactoring entstehen. Genau das ist der praktische Wert atomarer Commits: Jede dieser drei Operationen wird von einer Ratearbeit zu einer verlässlichen, mechanischen Operation.
$ git bisect start
$ git bisect bad HEAD
$ git bisect good v2.4.1
# Automate the search with a script that exits non-zero on failure
$ git bisect run vendor/bin/phpunit --filter=DiscountCalculatorTest
# ... bisect narrows down through the atomic commit history ...
i7j8k9l is the first bad commit
commit i7j8k9l
fix: round percentage discount to 2 decimals
# Revert exactly this one concern, nothing else
$ git revert i7j8k9l
# Or cherry-pick just this fix onto the release branch
$ git cherry-pick i7j8k9l
9. Team-Workflow, Konventionen und Vergleich
Atomare Commits entfalten ihren vollen Wert erst, wenn ein Team sie konsequent einhält. Eine gemeinsame Konvention für Commit-Messages, etwa nach dem Schema von Conventional Commits mit Präfixen wie fix:, feat: oder chore:, macht die Absicht jedes Commits sofort erkennbar, auch ohne den Diff zu öffnen. In Pull Requests lohnt sich außerdem die Entscheidung, ob per Merge-Commit, Rebase oder Squash gemergt wird: Squash-Merges verstecken die atomare Struktur im Ziel-Branch, bewahren sie aber innerhalb des Pull Requests für die Review-Phase, was für viele Teams ein guter Kompromiss ist.
Die folgende Tabelle stellt typische Monster-Commit-Muster den entsprechenden atomaren Alternativen gegenüber.
| Situation | Monster-Commit-Muster | Atomares Muster | Vorteil |
|---|---|---|---|
| Refactoring während Bugfix | Ein Commit mit Umbenennung und Fix | git add -p in zwei Commits trennen | Review und Revert bleiben unabhängig |
| Commit-Message | "various fixes and cleanup" | Ein Commit pro logischer Änderung | Historie ist ohne Diff lesbar |
| Fehlersuche | git bisect landet auf 800-Zeilen-Commit | git bisect landet exakt auf der Zeile | Keine manuelle Nachsuche nötig |
| Änderung zurücknehmen | git revert reißt ungewollte Änderungen mit | git revert hebt genau eine Änderung auf | Kein Kollateralschaden bei Reverts |
| Hotfix auf Release-Branch | cherry-pick schleppt Refactoring mit | cherry-pick überträgt nur den einen Fix | Wenige bis keine Merge-Konflikte |
In der Praxis lohnt sich ein kurzer CI-Check, der Commit-Messages gegen das Team-Format validiert, etwa mit commitlint, damit die Konvention nicht nur auf dem Papier existiert, sondern bei jedem Push automatisch durchgesetzt wird.
Mironsoft
Git-Workflows, Code-Review-Prozesse und CI/CD für Magento-Teams
Saubere Commit-Historie im Team etablieren?
Wir helfen Magento- und PHP-Teams dabei, Git-Workflows zu professionalisieren: von Commit-Konventionen über Code-Review-Guidelines bis zur CI/CD-Pipeline, die bei jedem Pull Request automatisch prüft.
Git-Workflow-Audit
Analyse eurer Commit-Historie, Branching-Strategie und Review-Prozesse
Team-Coaching
Praxisnahes Training zu git add -p, rebase -i und atomaren Commits
CI/CD-Integration
Automatisierte Checks für Commit-Konventionen in der Pipeline
10. Zusammenfassung
Atomare Commits statt Monster-Commits lösen ein wiederkehrendes Problem: Code-Reviews, die niemand mehr wirklich liest, git bisect, das keine brauchbare Antwort liefert, und Reverts, die mehr kaputt machen als sie reparieren. git add -p und git add -i teilen den Working Tree hunkweise in klar abgegrenzte Commits auf, git commit -p verkürzt den Weg dorthin. Wo der Monster-Commit schon existiert, holen git reset -p und git rebase -i die Kontrolle nachträglich zurück, solange die Historie noch nicht gepusht wurde.
Der Aufwand für saubere, atomare Commits zahlt sich nicht beim Schreiben aus, sondern bei jedem späteren Blick in die Historie: beim Review, bei der Fehlersuche mit bisect, beim Revert eines einzelnen Fixes und beim Cherry-Pick auf einen Release-Branch. Teams, die Commit-Konventionen und Review-Guidelines konsequent durchsetzen, verwandeln die Git-Historie von einem chronologischen Nebenprodukt in ein durchsuchbares, verlässliches Werkzeug.
Atomare Commits statt Monster-Commits, das Wichtigste auf einen Blick
Ein Commit, ein Zweck
git add -p und git commit -p stagen und committen nur die Hunks, die zusammengehören.
Nachträglich reparierbar
git reset -p und git rebase -i teilen bereits bestehende Commits, solange sie noch lokal sind.
Bisect, Revert, Cherry-Pick
Atomare Commits machen alle drei Operationen zu mechanischen, verlässlichen Schritten statt Ratearbeit.
Team-Konvention
Klare Commit-Message-Formate und CI-Checks setzen atomare Commits im ganzen Team durch.