Commit History Rewrite
Schreibt die Commit-History eines bestehenden Repositorys um, sodass sie den Conventional Commits entspricht. Analysiert Commits, klassifiziert sie nach Typ, identifiziert Squash-Kandidaten und führt das Rewriting mit vollständigen Sicherheitsvorkehrungen durch.
Überblick
History ist Dokumentation. Jede Commit-Nachricht auf main ist eine dauerhafte Aufzeichnung, die zukünftige Entwickler (und Agenten) nutzen, um zu verstehen, was sich geändert hat und warum. Nachrichten wie „fix“, „update“, „wip“ zerstören diese Dokumentation. Release-Please parst Commit-Nachrichten, um Versionssprünge zu bestimmen – nicht konforme Nachrichten führen zu falschen oder gar keinen Releases.
Code ist eine Verbindlichkeit. Hyrums Gesetz gilt auch für die Git-History: Wenn genügend Werkzeuge von Commit-Nachrichten abhängen (Changelogs, Release-Automatisierung, git bisect, git blame), wird das Format dieser Nachrichten zu einem Vertrag. Das Bereinigen der History vor der Einführung von Automatisierung verhindert, dass die alten Nachrichten jedes nachgelagerte Werkzeug verschmutzen.
Diese Fertigkeit schreibt die Nachrichten um, nicht den Code. Der Dateibaum muss vorher und nachher identisch sein – nur die Commit-Metadaten ändern sich.
Wann verwenden
- Einführung von Conventional Commits in einem Repository mit bestehender, nicht konformer History
- Commit-Log ist mit WIP, fixup, merge oder bedeutungslosen Nachrichten verunreinigt
- Vorbereitung eines Repositorys für release-please (das für seine Funktion Conventional Commits benötigt)
- Bereinigung vor der Veröffentlichung als Open Source oder der Übergabe eines Repositorys
Wann NICHT verwenden
- Repository wurde bereits mit Conventional Commits veröffentlicht (nichts umzuschreiben)
- Geteilte Branches mit aktiven Mitarbeitern, die nicht benachrichtigt wurden
- Es müssen nur die letzten Commits korrigiert werden (
git rebase -i HEAD~Ndirekt verwenden) - Repository ist ein Fork, dessen Upstream das Commit-Format vorgibt
Prozess
Schritt 1: Repository bewerten
# Gesamtzahl der Commits zählen
git rev-list --count HEAD
# Zusammenfassung der Commit-Nachrichten anzeigen
git log --oneline --no-merges
# Nach Mitarbeitern suchen (Vielfalt der Autoren)
git shortlog -sne --no-merges
# Dichte der Merge-Commits ermitteln
git log --merges --oneline | wc -l
Entscheidungspunkt: Wenn die Anzahl der Commits < 50 ist, interaktives Rebase verwenden. Wenn >= 50, git-filter-repo verwenden.
Schritt 2: Vorhandene Commits klassifizieren
Jeden Commit anhand von zwei Signalen analysieren: dem Nachrichtentext und den geänderten Dateien. Wenn die Nachricht mehrdeutig ist (z.B. „update stuff“), ist der Diff die Wahrheitsquelle.
Signal 1: Nachrichteninhalt
| Signal in der Nachricht | Zugewiesener Typ |
|---|---|
| „add“, „new“, „implement“, „create“, „introduce“, „support“ | feat |
| „fix“, „bug“, „patch“, „resolve“, „correct“, „handle error“ | fix |
| „refactor“, „restructure“, „reorganize“, „simplify“, „extract“, „move“ | refactor |
| „update dep“, „bump“, „upgrade“, „version“, „config“, „setup“ | chore |
| „test“, „spec“, „coverage“, „assert“ | test |
| „doc“, „readme“, „comment“, „typo in doc“ | docs |
| „ci“, „workflow“, „pipeline“, „action“, „deploy“ | ci |
Signal 2: Geänderte Dateien (bei mehrdeutiger Nachricht)
git show --stat --format="" <hash>
| Dateimuster | Zugewiesener Typ |
|---|---|
Nur *.md, docs/** | docs |
Nur *test*, *spec*, __tests__/** | test |
Nur .github/workflows/**, .gitlab-ci.yml, Jenkinsfile | ci |
Nur package.json, *.lock, requirements.txt, Cargo.toml | chore |
Nur Konfigurationsdateien (.eslintrc, tsconfig.json, .prettierrc) | chore |
Squash-Kandidaten
| Signal | Aktion |
|---|---|
| Nachricht ist „WIP“, „wip“, „temp“, „save“, „checkpoint“, einzelnes Wort oder leer | In den nächstgelegenen sinnvollen Commit squashen |
| Nachricht ist „fixup“, „fix typo“, „oops“, „lint“, „formatting“ | In den vorherigen squashen |
| Merge-Commits (aus Non-Squash-Merges) | Squash-Kandidat – prüfen, ob der Inhalt bereits auf main ist |
Bei mehrdeutigen Commits den vollständigen Diff anzeigen (git show <hash>), um den Typ anhand des Inhalts zu bestimmen.
Schritt 3: Qualitativ hochwertige Nachrichten schreiben
Umgeschriebene Nachrichten müssen den Commit-Nachrichten-Standards folgen:
<typ>[optionaler scope]: <kurze imperative Beschreibung>
[optionaler Body, der das WARUM erklärt, nicht das WAS]
Gute Umschreibungen:
„initial setup“ → „chore: initial project setup“
„added login“ → „feat: add user login“
„fix bug“ → „fix: resolve login redirect loop“
„Update README.md“ → „docs: update README with setup instructions“
„updated deps“ → „chore(deps): update dependencies“
Schlechte Umschreibungen (das nicht tun):
„stuff“ → „chore: stuff“ ← immer noch bedeutungslos
„changes“ → „feat: changes“ ← keine Beschreibung
„fix“ → „fix: fix“ ← redundant
Wenn die ursprüngliche Nachricht bedeutungslos ist, den Diff lesen, um eine angemessene Beschreibung zu schreiben. Wenn der Diff zu umfangreich oder mehrdeutig ist, den Benutzer fragen, worum es bei dem Commit ging.
Schritt 4: Umschreibungsplan präsentieren
Vor jeder destruktiven Aktion dem Benutzer einen vollständigen Plan zeigen:
UMSCHREIBUNGSPLAN
=================
Gesamtanzahl Commits: 47
Umzuschreibende Commits: 32
Zu squashende Commits: 8
Bereits konforme Commits: 7
Erhaltene Autoren: 3 (Alice <alice@co>, Bob <bob@co>, CI Bot <bot@co>)
VORGESCHLAGENE ÄNDERUNGEN:
abc1234 „initial setup“ → „chore: initial project setup“
def5678 „added login“ → „feat: add user login“
ghi9012 „wip“ → [SQUASHEN in def5678]
jkl3456 „fix bug“ → „fix: resolve login redirect loop“
mno7890 „Update README.md“ → „docs: update README with setup instructions“
pqr1234 „feat: add search“ → [BEHALTEN – bereits konform]
...
SICHERHEIT:
✓ Backup-Branch: backup/pre-rewrite-<Zeitstempel>
⚠ Force Push nach dem Umschreiben erforderlich
⚠ Alle Mitarbeiter müssen nach dem Push neu klonen oder zurücksetzen
UNVERÄNDERT:
- Dateibaum (keine Inhaltsänderungen)
- Autorenname, E-Mail und Datum bei jedem Commit
- Commit-Reihenfolge und Elternbeziehungen (außer bei gesquashten Commits)
Auf explizite Benutzerfreigabe warten, bevor fortgefahren wird. Nicht auf „klingt gut“ reagieren – „ja“ oder eine gleichwertige Bestätigung verlangen.
Schritt 5: Backup erstellen
# Einen Backup-Branch am aktuellen HEAD erstellen
git branch "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"
# Prüfen, ob das Backup existiert
git branch -l 'backup/*'
# Optional: Backup zur zusätzlichen Sicherheit auf das Remote-Push schieben
git push origin "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"
Schritt 6: Umschreiben ausführen
Option A: Interaktives Rebase (< 50 Commits)
# Von der Wurzel aus rebasen
git rebase -i --root
Im Editor:
- Squash-Kandidaten mit
squashoderfixupmarkieren - Nachrichten-Umschreibungen mit
rewordmarkieren - Commit-Reihenfolge beibehalten
Autorenschaft erhalten: Interaktives Rebase erhält standardmäßig Autorennamen, E-Mail und Datum. NICHT --reset-author verwenden. NICHT --committer-date-is-author-date verwenden, es sei denn, es wurde ausdrücklich darum gebeten.
Option B: git-filter-repo (>= 50 Commits)
Ein Python-Callback-Skript (rewrite-messages.py) erstellen, das alte Nachrichten auf neue abbildet:
import re
REWRITES = {
b'alter Hash-Präfix': b'neue Nachricht',
# ... aus dem Plan generiert
}
def rewrite(commit, metadata):
# Anhand der ursprünglichen Nachricht oder des Hashs nachschlagen
if commit.original_id in REWRITES:
commit.message = REWRITES[commit.original_id]
Anwenden mit:
git filter-repo --commit-callback "$(cat rewrite-messages.py)" --force
Autorenschaft erhalten: git-filter-repo erhält standardmäßig die Autorenschaft. NICHT --mailmap oder Autoren-Umschreibungs-Flags verwenden, es sei denn, der Benutzer fordert dies ausdrücklich an.
Commits squashen
Für Squash-Kandidaten in den nächstgelegenen sinnvollen Commit zusammenführen. Der resultierende Commit:
- Verwendet die Conventional-Commit-Nachricht des sinnvollen Commits
- Erhält den ursprünglichen Autor des sinnvollen Commits (nicht des Squash-Ziels)
- Kombiniert alle Änderungen aus dem gesquashten Bereich
Schritt 7: Überprüfen
Alle Überprüfungen durchführen. Jede Prüfung muss bestanden werden, bevor mit dem Push fortgefahren wird.
# 1. Umgeschriebenes Log überprüfen
git log --oneline --no-merges
# 2. ALLE Nachrichten auf Übereinstimmung mit Conventional Commits validieren
FAILURES=$(git log --format="%s" --no-merges | grep -cvE "^(feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+")
if [[ "$FAILURES" -gt 0 ]]; then
echo "FEHLGESCHLAGEN: $FAILURES nicht konforme Commits gefunden:"
git log --format="%h %s" --no-merges | grep -vE "^[a-f0-9]+ (feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+"
exit 1
fi
echo "BESTANDEN: Alle Commits sind konform"
# 3. NULL Inhaltsänderungen überprüfen (kritisch – beweist, dass nur Nachrichten geändert wurden)
DIFF=$(git diff "backup/pre-rewrite-"*..HEAD --stat)
if [[ -n "$DIFF" ]]; then
echo "FEHLGESCHLAGEN: Inhaltsunterschiede erkannt – Rewriting hat Code geändert, nicht nur Nachrichten"
echo "$DIFF"
exit 1
fi
echo "BESTANDEN: Dateibaum identisch"
# 4. Autorenschaft erhalten überprüfen
echo "=== Autoren vorher ==="
git -C . log backup/pre-rewrite-* --format="%an <%ae>" --no-merges | sort -u
echo "=== Autoren nachher ==="
git log --format="%an <%ae>" --no-merges | sort -u
# Diese beiden Listen müssen identisch sein
Wenn eine Prüfung fehlschlägt, NICHT pushen. Auf das Backup zurücksetzen und untersuchen:
git reset --hard backup/pre-rewrite-*
Schritt 8: Pushen (mit Bestätigung)
Dies ist destruktiv für Remote-Mitarbeiter. Mit dem Benutzer bestätigen:
⚠ FORCE PUSH WARNUNG
Branch: main
Remote: origin
Aktion: git push --force-with-lease origin main
Nach diesem Push müssen alle Mitarbeiter Folgendes ausführen:
git fetch origin
git reset --hard origin/main
Geben Sie „ja“ ein, um fortzufahren:
git push --force-with-lease origin main
--force-with-lease (nicht --force) verwenden, um das Überschreiben gleichzeitiger Remote-Änderungen zu verhindern. Wenn --force-with-lease abgelehnt wird, hat nach dem Umschreiben jemand auf den Branch gepusht – vor einem erneuten Versuch untersuchen.
Bereinigung nach dem Umschreiben
Nach erfolgreichem Push und Benachrichtigung der Mitarbeiter:
# Prüfen, ob Remote mit lokal übereinstimmt
git log --oneline -5
git log --oneline -5 origin/main
# Diese müssen identisch sein
# Backup-Branch mindestens 2 Wochen behalten, dann löschen
# git branch -D backup/pre-rewrite-*
# git push origin --delete backup/pre-rewrite-*
Rationalisierungen
| Rationalisierung | Realität |
|---|---|
| „Die alte History spielt keine Rolle“ | Release-Please parst Commit-Nachrichten, um Versionen zu bestimmen. Nicht konforme History führt zu falschen oder keinen Releases. git bisect, git blame und git log --grep sind alle auf aussagekräftige Nachrichten angewiesen. |
| „Ich mache ab diesem Commit einfach einen Neuanfang“ | Man verliert Blame, Autorenschaft und Kontext. Ein --orphan-Branch zerstört die Entwicklungsgeschichte. Das Rewriting erhält alles außer schlechten Nachrichten. |
| „Es ist zu riskant, umzuschreiben“ | Der Backup-Branch macht dies vollständig reversibel. Der Diff zeigt null Inhaltsänderungen. Wenn etwas schiefgeht: git reset --hard backup/pre-rewrite-*. |
| „Ich schreibe später um, wenn es wichtig ist“ | History-Umschreibungen werden mit der Zeit schwieriger, je mehr Leute davon klonen und Branches davon abzweigen. Jetzt erledigen, bevor weitere nachgelagerte Abhängigkeiten entstehen. |
| „Force Pushing ist gefährlich“ | --force-with-lease schützt davor, die Arbeit anderer zu überschreiben. In Kombination mit dem Backup-Branch ist dies sicher. |
| „Die Nachrichten sind nah genug dran“ | „Nah genug“ bedeutet, dass Release-Please sie nicht parsen wird. Conventional Commits sind ein strenges Format – entweder eine Nachricht ist konform oder nicht. |
| „Ich kann Releases einfach manuell taggen“ | Manuelle Tags treiben ab, werden vergessen und entbehren der Changelog-Integration. Der Sinn liegt in der Automatisierung – aber Automatisierung benötigt parsebare Eingaben. |
Rote Flaggen
- Überspringen der Erstellung des Backup-Branches
- Verwendung von
--forceanstelle von--force-with-lease - Umschreiben eines Branches, an dem andere aktiv arbeiten, ohne Benachrichtigung
- Inhaltsunterschiede, die im Diff von Schritt 7 auftauchen (bedeutet, dass das Rewriting Code geändert hat, nicht nur Nachrichten)
- Änderung der Autorenanzahl nach dem Rewriting (bedeutet, dass die Autorenschaft nicht erhalten wurde)
- Fortfahren ohne Benutzerfreigabe des Umschreibungsplans
- Umgeschriebene Nachrichten, die immer noch bedeutungslos sind (z.B.
"chore: stuff") - Squashen von Commits über verschiedene Autoren hinweg ohne Diskussion
- Backup-Branch nach dem Umschreiben nicht mindestens 2 Wochen behalten