Gute Commit Messages in Git schreiben

1. Übersicht

Commit Messages sind das, was du beim Debuggen, Code-Review oder beim Schreiben eines Changelogs am häufigsten erneut liest — noch häufiger als den Code, den du gerade geschrieben hast. Dieser Artikel hilft dir, die Standard-Commit-Message-Struktur zu beherrschen, Conventional Commits anzuwenden und Messages automatisch mit commitlint + husky zu überprüfen. Nach dem Lesen wirst du Commits klar und konsistent schreiben können, und dein Team kann aus der Git-Historie automatisch ein Changelog generieren.

2. Warum Commit Messages wichtig sind

Eine gute Commit Message beantwortet drei Fragen: Was hat sich geändert, warum hat es sich geändert und welche Auswirkungen hat es. Ohne sie musst du git show für jeden Commit ausführen, um die Absicht des Autors zu erraten — das kostet ein Vielfaches der Zeit, die es gekostet hätte, die Message ordentlich zu schreiben.

Vergleiche zwei Messages für dieselbe Änderung:

# Schlecht
fix bug

# Gut
fix(payment): NullPointerException behandeln, wenn PaymentService.charge() ein Order ohne Customer erhält

Die schlechte Message verrät nicht, welcher Bug oder wo. Die gute Message zeigt das Modul (payment), die Aktion (fix) und die genaue Ursache – fünf Sekunden Lesen und man versteht es.

3. Standardstruktur für Commit Messages

Git empfiehlt eine Struktur aus 3 Teilen: subject (Betreff), body (Inhalt), footer (Fußzeile).

<type>(<scope>): <subject>

<body>

<footer>
  • subject: Kurzzusammenfassung, ≤ 50 Zeichen, im Präsens geschrieben (“add” nicht “added”).
  • body: erkläre das Warum der Änderung, nicht das Was (das Diff zeigt bereits, was sich geändert hat).
  • footer: Metadaten — Breaking Changes, zugehörige Issues, Co-Autor.

Vollständiges Beispiel:

fix(order): Double-Charging bei fehlgeschlagenen Zahlungen verhindern

PaymentService.charge() fehlt ein Idempotency Key. Bei einem Network-Timeout
führt ein Client-Retry dazu, dass das System das API Gateway zweimal für
dieselbe Order aufruft.

Idempotency Key = order.getId() + "-" + attemptCount zum Request-Header
hinzugefügt, damit das Gateway automatisch duplizierte Anfragen eliminiert.

Closes #482

3.1. Subject-Zeile

Der Subject ist die wichtigste Zeile – sie erscheint auf git log --oneline, im PR-Titel und im Changelog. Regeln:

# Falsch – mit Großbuchstaben, mit Punkt am Ende, Präteritum
Fixed the bug in order service.

# Richtig – Kleinbuchstaben, kein Punkt am Ende, Präsens
fix: Bestand wird bei Zahlung über E-Wallet nicht abgezogen

Hinweis: Die Grenze von 50 Zeichen ist keine harte Regel, aber über 72 Zeichen werden auf vielen Terminals und in der GitHub UI abgeschnitten. Versuche, prägnant zu sein.

3.2. Body-Abschnitt

Der Body dient zur Erklärung des Kontexts – das, was ein Code-Diff nicht vermitteln kann: warum diese Lösung gewählt wurde, warum andere Lösungen ausgeschlossen sind, ob es Nebenwirkungen gibt.

refactor(cart): CartService von synchron zu asynchron konvertieren

Bisher rief CartService.checkout() InventoryService und ShippingService
nacheinander auf, die Gesamtlatenz betrug ~800ms. Durch die Konvertierung
zu CompletableFuture und parallele Ausführung beider Calls wird die
Latenz auf ~450ms reduziert.

Die öffentliche API bleibt unverändert; nur die interne Implementierung
wurde geändert.

Trenne Subject und Body durch eine leere Zeile – Git und alle Commit-Parsing-Tools verlassen sich auf diese Konvention.

4. Conventional Commits

Conventional Commits ist der heute am weitesten verbreitete Standard und definiert feste type-Werte, damit Tools automatisch klassifizieren und Changelogs generieren können.

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Häufig verwendete type-Werte:

Type Bedeutung
feat neue Funktion hinzufügen
fix Bug beheben
docs nur Dokumentationsänderungen
style Code-Formatierung, keine Logik-Änderungen
refactor Code-Struktur ändern, kein Verhaltensänderung
test Tests hinzufügen/ändern
chore Nebensächlichkeiten (Build, Abhängigkeiten, Config)
perf Performance-Verbesserung

Beispiel mit scope als Modulname:

feat(auth): Google OAuth2 Login-Unterstützung hinzufügen

GoogleOAuth2Provider implementiert OAuth2LoginProvider.
Client-ID und Client-Secret in application.properties hinzufügen.

4.1. Breaking Change

Wenn eine Änderung die Rückwärtskompatibilität bricht, markiere sie mit ! nach type/scope oder mit BREAKING CHANGE: in der Fußzeile.

feat(api)!: GET /orders/{id} Response zu OrderDto geändert

BREAKING CHANGE: Feld "customerName" wurde durch ein
"customer"-Objekt mit id, name und email ersetzt. Clients
müssen ihren Parser aktualisieren.

Semantic-release und CI/CD-Tools orientieren sich an der Zeile BREAKING CHANGE:, um die Major-Version automatisch zu erhöhen.

5. Automatische Überprüfung von Commit Messages

Konventionen nützen nur, wenn das Team sie konsequent befolgt. Verwende commitlint, um falsch formatierte Commits sofort lokal zu blockieren, bevor du sie pushst.

<!-- Nicht für Node-Projekte anwendbar; nur ein Beispiel für eine Abhängigkeit, wenn Sie einen Maven-Wrapper für husky-git-hooks verwenden -->

Tatsächlich läuft commitlint auf Node und wird über npm installiert:

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

Erstelle eine Konfigurationsdatei:

// commitlint.config.js
module.exports = { extends: ['@commitlint/config-conventional'] };

Binde den commit-msg Hook über husky ein:

npx husky init
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg

Versuche jetzt, einen falsch formatierten Commit zu erstellen:

git commit -m "fixed bug"
⧗   input: fixed bug
✖   subject may not be empty [subject-empty]
✖   type must be one of [feat, fix, docs, style, refactor, test, chore, perf]
✖   found 2 problems, 0 warnings
husky - commit-msg script failed (code 1)

Der Commit wird sofort blockiert und zwingt dich, ihn vor der Erstellung in das richtige Format zu ändern.

5.1. Verwendung mit Java/Spring Boot-Projekten

Wenn dein Projekt Maven ist, kannst du husky + commitlint parallel verwenden (sie benötigen nur Node Runtime und beeinflussen nicht den Java-Build). Oder wenn du es rein in Java haben möchtest, schreib ein einfaches Validierungsskript und rufe es über core.hooksPath auf:

#!/bin/bash
# .githooks/commit-msg
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf)(\([a-z-]+\))?: .{1,50}"
MSG=$(cat "$1")

if ! [[ "$MSG" =~ $PATTERN ]]; then
  echo "Commit Message folgt nicht dem Conventional Commits Format" >&2
  exit 1
fi
git config core.hooksPath .githooks
chmod +x .githooks/commit-msg

5.2. Tests für das Validierungsskript schreiben

Wenn du ein eigenes Validierungsskript pflegst (ohne commitlint), solltest du Tests für diesen Regex haben. Angenommen, du packst die Parse-Logik in eine Java-Klasse, um sie in der CI wiederzuverwenden:

public class CommitMessageValidator {

    private static final Pattern PATTERN =
        Pattern.compile("^(feat|fix|docs|style|refactor|test|chore|perf)" +
                         "(\\([a-z0-9-]+\\))?: .{1,50}");

    public boolean isValid(String message) {
        String subject = message.lines().findFirst().orElse("");
        return PATTERN.matcher(subject).find();
    }
}
class CommitMessageValidatorTest {

    private final CommitMessageValidator validator = new CommitMessageValidator();

    @Test
    void shouldAcceptConventionalFormat() {
        boolean result = validator.isValid("fix(order): Double-Charging bei Retry verhindern");

        assertThat(result).isTrue();
    }

    @Test
    void shouldRejectMessageWithoutType() {
        boolean result = validator.isValid("fixed the bug");

        assertThat(result).isFalse();
    }

    @Test
    void shouldRejectUnknownType() {
        boolean result = validator.isValid("update: etwas hinzugefügt");

        assertThat(result).isFalse();
    }
}

Diese Tests stellen sicher, dass die Validierungslogik nicht regrediert, wenn du den Regex später änderst – etwa wenn du einen neuen type hinzufügst und vergisst, den Test zu aktualisieren.

6. Häufige Fehler

Zu große Commits mit allgemeinen Messages. „update code” für einen Commit, der 15 verschiedene Dateien ändert, ist ein Zeichen, dass du in mehrere kleinere Commits aufteilen solltest, jeweils mit klarem, atomarem Zweck.

Mehrere Arten von Änderungen in einem Commit vermischen. Wenn du sowohl einen fix als auch ein unabhängiges refactor im selben Commit machst, erschwert das git revert und git bisect – das Revertieren eines Fixes könnte das Refactor mitreißen.

Den Body repetitiv nach dem Subject schreiben. Der Body sollte Kontext ergänzen, nicht die erste Zeile mit längeren Sätzen wiederholen.

# Wiederholung, bedeutungslos
fix(order): Fehler bei der Gesamtsummenerrechnung behoben

Fehler bei der Gesamtsummenerrechnung im OrderService behoben.

Emoji/Sonderzeichen statt Inhalten verwenden. Emojis sind nicht falsch, aber wenn du sie statt echter Beschreibung verwendest, wird das automatische Changelog leer sein.

Hinweis: Conventional Commits passen zu Teams mit automatischer Changelog-Generierung oder Semantic Versioning in der CI/CD. Für kleine Teams mit wenigen Commits kann strikte Formatierung unnötige Reibung erzeugen – priorisiere Klarheit vor strikter maschineller Konformität.

7. Fazit

Gute Commit Messages müssen nicht lang sein, nur richtig strukturiert: prägnanter Subject im Präsens, Body erklärt das Warum, Footer dokumentiert nötige Metadaten. Die Anwendung von Conventional Commits hilft dem Team, Git-Historien schneller zu lesen, und ermöglicht Tools, Changelog und Versionierung zu automatisieren. Richte commitlint oder einen benutzerdefinierten Git Hook ein, um Konventionen in automatische Erzwingungen umzuwandeln und deine Abhängigkeit vom Bewusstsein einzelner Teamkollegen zu vermeiden.

gitconventional-commitsbest-practicescommitlintdevops