Es gibt diese Situationen, in denen du an einer größeren Aufgabe sitzt und irgendwann feststellst, dass dein Git-Status langsam unangenehm wird. Hier eine geänderte Klasse, dort noch eine Anpassung an der Oberfläche, eine Konfiguration ist dazugekommen und irgendwo liegen vielleicht schon die ersten Tests. Eigentlich wäre längst ein guter Zeitpunkt für einen Commit. Eigentlich.
Denn gleichzeitig sitzt da dieser Gedanke im Hinterkopf: Wenn ich jetzt committe, sind meine Änderungen weg.
Natürlich sind sie nicht wirklich weg. Sie sind danach sogar wesentlich sicherer aufgehoben als vorher. Aber der Working Tree ist wieder sauber. IntelliJ zeigt nicht mehr überall die geänderten Dateien an. git diff liefert nichts mehr. Und damit verschwindet etwas, das du während der Arbeit vielleicht ganz selbstverständlich benutzt hast: deine Übersicht darüber, was du für diese Aufgabe bereits angefasst hast.
Ich nenne das für mich inzwischen "Commit-Fear".
Das ist kein offizieller Begriff aus der Softwareentwicklung. Aber das Verhalten dahinter dürfte vielen Entwicklern ziemlich bekannt vorkommen. Und ich stecke gerade selbst wieder genau in diesem Dilemma.
Ich arbeite an einer größeren Änderung, habe inzwischen viel zu viel verändert und weiß eigentlich sehr genau, dass ein Commit längst sinnvoll wäre. Gleichzeitig möchte ich möglichst nichts anfassen, was für die Aufgabe nicht notwendig ist. Solange alle Änderungen noch uncommittet vor mir liegen, sehe ich sehr deutlich, welche Dateien zu meiner aktuellen Baustelle gehören.
Genau das macht den Commit plötzlich schwieriger, als er eigentlich sein sollte.
Warum sich ein Commit plötzlich wie Informationsverlust anfühlt
Technisch betrachtet ist die Sache ziemlich eindeutig. Ein Commit speichert einen definierten Stand deiner Änderungen in der Git-Historie. Nichts verschwindet. Im Gegenteil: Du schaffst einen festen Punkt, zu dem du jederzeit zurückkehren kannst.
Das Problem ist also nicht Git. Das Problem ist, wie wir den Working Tree während der Entwicklung benutzen. Wenn du mehrere Stunden oder vielleicht sogar mehrere Tage an einer größeren Aufgabe arbeitest, entsteht ganz automatisch eine Art visuelle Aufgabenliste. IntelliJ markiert geänderte Dateien. Git zeigt dir, was neu, verändert oder gelöscht wurde. Mit einem Blick auf git status bekommst du einen ziemlich guten Eindruck davon, wo du überall gearbeitet hast.
modified: UserService.java
modified: UserRepository.java
modified: user.xhtml
modified: messages.properties
Diese Liste beantwortet allerdings zwei vollkommen unterschiedliche Fragen gleichzeitig. Sie zeigt dir einerseits, was seit dem letzten Commit verändert wurde. Du interpretierst sie aber möglicherweise zusätzlich als Antwort auf die Frage, welche Teile deiner aktuellen Aufgabe bereits bearbeitet wurden. Und genau dort beginnt das Problem: Ein Commit räumt die erste Information auf. Der Working Tree ist danach sauber. Gleichzeitig verschwindet dadurch aber auch die zweite Information aus deinem unmittelbaren Blickfeld. Das kann sich so anfühlen, als würdest du deinen aktuellen Arbeitskontext verlieren. Gerade wenn du bewusst darauf achtest, bei einer Änderung möglichst wenig unnötig anzufassen, ist dieser Effekt noch stärker. Die Liste der geänderten Dateien ist dann gleichzeitig eine Abgrenzung deiner Baustelle. Du weißt beispielsweise: Diese zwölf Dateien gehören zu meiner aktuellen Änderung. Alles andere lasse ich möglichst in Ruhe. Nach einem Commit fehlt diese optische Grenze plötzlich. Das führt schnell zu einem paradoxen Verhalten: Du vermeidest einen Mechanismus, der deine Arbeit eigentlich absichern und strukturieren soll, weil dir genau dieser Mechanismus kurzfristig die Orientierung nimmt.
Das eigentliche Problem sind nicht zu viele Commits
Wenn du in dieser Situation steckst, liegt die naheliegende Lösung zunächst darin, einfach weiterzuarbeiten und später alles gemeinsam zu committen. Das funktioniert eine Zeit lang erstaunlich gut. Bis aus fünf geänderten Dateien zwanzig werden. Dann kommt vielleicht noch eine kleine Korrektur dazu, die mit der eigentlichen Aufgabe nur am Rand zu tun hat. Während du diese Stelle ohnehin geöffnet hast, räumst du noch eine Kleinigkeit auf. Irgendwo fällt dir ein fehlender Test auf. An einer anderen Stelle musst du eine Methode anpassen, weil deine ursprüngliche Änderung weitere Auswirkungen hat. Und irgendwann hast du einen riesigen Diff vor dir. Das ist genau der Punkt, an dem die vermeintliche Übersicht ins Gegenteil kippt. Ein großer uncommitteter Arbeitsstand sagt dir zwar noch immer, welche Dateien verändert wurden. Er sagt dir aber nicht mehr besonders gut, warum sie verändert wurden. Dabei ist genau das später die wesentlich wichtigere Information.
Ein guter Commit sollte deshalb nicht die komplette Aufgabe repräsentieren. Er sollte einen logisch nachvollziehbaren Schritt innerhalb dieser Aufgabe festhalten. Angenommen, du baust ein bestehendes Feature um. Dann könnten sinnvolle Commits beispielsweise nacheinander das Datenmodell, die Persistenz, die Oberfläche und anschließend Tests oder kleinere Korrekturen enthalten. Die komplette Aufgabe ist nach dem ersten Commit natürlich noch nicht fertig. Das muss sie auch nicht sein. Ein Commit bedeutet nicht: "Die Aufgabe ist abgeschlossen." Er bedeutet eher: "Dieser Teil der Änderung ist jetzt in einem nachvollziehbaren Zustand."
Genau diese Unterscheidung musste ich mir selbst immer wieder bewusst machen. Denn wenn du einen Commit gedanklich automatisch mit dem Abschluss einer Aufgabe verbindest, entsteht zwangsläufig Widerstand gegen Zwischencommits. Dabei ist eine saubere Commit-Historie gerade bei größeren Änderungen wesentlich angenehmer als ein einzelner riesiger Commit mit einer Nachricht wie:
Implement new feature
Wenn später etwas nicht funktioniert, hilft dir dieser Commit kaum dabei herauszufinden, an welcher Stelle das Problem entstanden ist. Mehrere logisch getrennte Commits können das sehr wohl.
Wie du aus der Commit-Fear wieder herauskommst
Der wichtigste Schritt ist eigentlich ziemlich unspektakulär: Hör auf, deinen Git-Status als Aufgabenverwaltung zu benutzen. Das klingt zunächst offensichtlicher, als es in der Praxis ist. Wenn dein gesamter Überblick darüber, was bei einer größeren Aufgabe noch zu erledigen ist, davon abhängt, welche Dateien IntelliJ als geändert markiert, dann fehlt dir nach jedem Commit zwangsläufig ein Teil deiner Orientierung. Du brauchst deshalb eine zweite Ebene für den fachlichen Arbeitsstand. Das kann ein Ticket sein. Es kann eine kleine Markdown-Datei sein. Es kann eine Notiz in deinem Projekt sein oder schlicht eine kurze Liste von Punkten, die für die aktuelle Aufgabe noch offen sind.
Zum Beispiel:
Kategorieverwaltung
[x] Datenmodell
[x] Persistenz
[ ] Oberfläche
[ ] Validierung
[ ] Tests
Damit trennst du zwei Dinge voneinander, die Git niemals wirklich gemeinsam verwalten sollte. Git beantwortet die Frage, welche Änderungen du vorgenommen hast. Deine Aufgabenverwaltung beantwortet die Frage, was du noch tun musst. Wenn diese Trennung funktioniert, verliert ein Commit plötzlich einen großen Teil seines Schreckens. Du kannst das Datenmodell fertigstellen und committen, ohne befürchten zu müssen, anschließend nicht mehr zu wissen, dass die Validierung noch fehlt. Eine zweite wichtige Gewohnheit ist, Commits nach fachlich oder technisch abgeschlossenen Teilschritten zu setzen und nicht nach Zeit. Du musst nicht jede halbe Stunde committen. Ebenso wenig musst du zwingend am Ende eines Arbeitstages committen. Der sinnvollere Zeitpunkt ist häufig dann erreicht, wenn du sagen kannst: Dieser Teil funktioniert jetzt in sich und lässt sich verständlich beschreiben.
Wenn deine Commit-Nachricht schwierig zu formulieren ist, kann das übrigens ein Hinweis darauf sein, dass du gerade zu viele unterschiedliche Änderungen zusammenfassen möchtest.
Add category persistence
ist ziemlich eindeutig.
Implement categories and fix UI and some other stuff
ist meistens ein Warnsignal.
Auch der Blick auf den Diff vor dem Commit hilft enorm. Nicht nur zur Kontrolle, ob versehentlich Debug-Ausgaben, temporäre Änderungen oder Dateien im Commit landen, die dort nichts verloren haben. Du zwingst dich dabei gleichzeitig dazu, die Änderung noch einmal als Einheit zu betrachten. Wenn du dabei feststellst, dass eigentlich drei voneinander unabhängige Dinge im Diff liegen, kannst du sie mit Git problemlos getrennt stagen und committen. Du musst also nicht einmal warten, bis dein kompletter Working Tree einen perfekten Zwischenstand erreicht hat. Genau dafür gibt es das Staging.
Fazit
Meine Commit-Fear entsteht nicht deshalb, weil ich Git nicht vertraue. Sie entsteht eher daraus, dass ich meinen uncommitteten Arbeitsstand als zusätzliche Gedächtnisstütze benutze. Solange eine Datei als geändert markiert ist, weiß ich: Da war ich dran. Das gehört zu meiner aktuellen Aufgabe. Das sollte ich noch einmal anschauen. Das funktioniert bei kleinen Änderungen ziemlich gut. Bei größeren Aufgaben wird es irgendwann gefährlich. Denn aus der hilfreichen Übersicht entsteht ein immer größer werdender Berg an Änderungen, den du irgendwann selbst kaum noch auseinanderhalten kannst. Und genau dann wird der Commit, den du die ganze Zeit aufgeschoben hast, erst recht unangenehm.
Die Lösung besteht deshalb nicht darin, einfach häufiger auf den Commit-Button zu drücken. Du musst zuerst akzeptieren, dass ein Commit kein Abschluss einer Aufgabe ist. Er ist ein Zwischenstand. Wenn du gleichzeitig an anderer Stelle festhältst, was für die eigentliche Aufgabe noch fehlt, kannst du diese Zwischenstände wesentlich entspannter setzen.
Und genau das ist vermutlich auch das, was ich bei meiner aktuellen Baustelle jetzt tun sollte: den Diff noch einmal sauber durchgehen, die bereits abgeschlossenen Teile sinnvoll voneinander trennen und endlich committen.
Denn irgendwann schützt dich der große uncommittete Diff nicht mehr davor, den Überblick zu verlieren.
Er ist der Grund dafür.
