Git rückgängig machen: reset, revert und restore
1. Überblick
Jeder Git-Nutzer muss früher oder später etwas rückgängig machen: eine Datei versehentlich geändert, zu viel mit git add hinzugefügt oder etwas committed, das nicht hätte committed werden dürfen. Git hat dafür drei Hauptbefehle: restore, reset und revert. Die Namen klingen ähnlich, aber jeder Befehl wirkt auf einen anderen Bereich.
Dieser Artikel zeigt jeden Befehl anhand eines Beispiel-Repositories. Nach dem Lesen weißt du, welchen Befehl du in welcher Situation einsetzt. Du erfährst auch, wie du Commits rettest, wenn du versehentlich reset --hard benutzt hast.
2. Ein Beispiel-Repository vorbereiten
Zunächst solltest du dich an die drei Git-Bereiche erinnern:
- Working Tree: die Dateien, die du gerade auf der Festplatte bearbeitest.
- Staging Area (Index): das, was in den nächsten Commit geht.
- Commit-Historie: die gespeicherten Commits, wobei
HEADauf den aktuellen Commit zeigt.
Wir erstellen ein kleines Repository mit vier Commits:
mkdir order-service && cd order-service
git init -b main
echo "class Order {}" > Order.java
git add Order.java && git commit -m "Order-Klasse hinzugefügt"
echo "class Customer {}" > Customer.java
git add Customer.java && git commit -m "Customer-Klasse hinzugefügt"
echo "class Order { int total; }" > Order.java
git commit -am "total zu Order hinzugefügt"
echo "class PaymentService {}" > PaymentService.java
git add PaymentService.java && git commit -m "PaymentService hinzugefügt"
git log --oneline
Der Befehl git commit -am staged alle verfolgten Dateien und committed sie sofort. Die Ausgabe von git log sieht ungefähr so aus (die Hashes auf deinem Computer unterscheiden sich):
d4e5f60 (HEAD -> main) PaymentService hinzugefügt
c3d4e5f total zu Order hinzugefügt
b2c3d4e Customer-Klasse hinzugefügt
a1b2c3d Order-Klasse hinzugefügt
3. git restore: Änderungen rückgängig machen, die noch nicht committed sind
git restore gibt es seit Git 2.23. Dieser Befehl funktioniert nur mit Dateien. Er verschiebt HEAD nie oder ändert die Commit-Historie. In älteren Git-Versionen übernahm git checkout -- <file> diese Rolle.
3.1. Änderungen im Working Tree verwerfen
Angenommen, du hast Order.java bearbeitet und hast dich umentschieden:
echo "class Order { int total; String note; }" > Order.java
git status --short
git restore Order.java
cat Order.java
M Order.java
class Order { int total; }
git status --short zeigt M an, was bedeutet, dass die Datei geändert, aber nicht staged wurde. git restore Order.java kopiert den Inhalt aus der Staging Area zurück ins Working Tree. Da noch nichts gestaged wurde, stimmt dieser Inhalt mit HEAD überein.
Hinweis: Mit
git restoreverworfene Änderungen sind endgültig verloren. Git hat sie nie gespeichert, daher kann auchreflogsie nicht retten. Wenn du dir nicht sicher bist, führegit stashaus, anstattrestorezu verwenden.
3.2. Eine Datei aus der Staging Area entfernen
Du hast versehentlich eine Datei mit git add hinzugefügt, die du noch nicht committen möchtest:
echo "class Order { int total; String note; }" > Order.java
git add Order.java
git restore --staged Order.java
git status --short
M Order.java
Die Option --staged kopiert den Inhalt von HEAD in die Staging Area. Die Datei wird somit „unstaged”, während der gerade bearbeitete Inhalt im Working Tree bleibt. Der ältere Befehl dafür ist git reset HEAD Order.java.
Um sowohl Staging Area als auch Working Tree auf einmal zu verwerfen, verwendest du:
git restore --staged --worktree Order.java
3.3. Eine Datei aus einem älteren Commit abrufen
Die Option --source ermöglicht es dir, den Inhalt einer Datei aus jedem beliebigen Commit abzurufen:
git restore --source=HEAD~2 Order.java
cat Order.java
git status --short
class Order {}
M Order.java
HEAD~2 ist der Commit „Customer-Klasse hinzugefügt”. In diesem Commit hatte Order.java noch kein total. Der Befehl schreibt standardmäßig nur ins Working Tree, daher ist die Datei jetzt M (nicht gestaged). Du kannst die Änderung überprüfen und dann selbst git add und commit ausführen, wenn du sie beibehalten möchtest. Ansonsten führe git restore Order.java aus, um zur Ausgangsversion zurückzukehren.
4. git reset: Verzweigungszeiger verschieben
git reset <commit> verschiebt den aktuellen Branch (und HEAD) zum angegebenen Commit. Die drei Modi --soft, --mixed und --hard unterscheiden sich darin, ob Staging Area und Working Tree überschrieben werden:
| Modus | Branch/HEAD | Staging Area | Working Tree |
|---|---|---|---|
--soft |
verschiebt zum Ziel-Commit | unverändert lassen | unverändert lassen |
--mixed (Standardwert) |
verschiebt zum Ziel-Commit | zurücksetzen auf Ziel-Commit | unverändert lassen |
--hard |
verschiebt zum Ziel-Commit | zurücksetzen auf Ziel-Commit | zurücksetzen auf Ziel-Commit |
Alle folgenden Beispiele starten aus einem sauberen Zustand beim Commit d4e5f60.
4.1. –soft: Commit entfernen, staged Änderungen behalten
git reset --soft HEAD~1
git log --oneline -1
git status --short
c3d4e5f (HEAD -> main) total zu Order hinzugefügt
A PaymentService.java
Der Commit „PaymentService hinzugefügt” verschwindet aus dem Branch. Die Datei PaymentService.java bleibt erhalten und hat Status A (staged). Dies ist praktisch, wenn du die Commit-Nachricht ändern oder mehrere Commits zusammenfassen möchtest:
git commit -m "PaymentService für Zahlungsabwicklung hinzugefügt"
4.2. –mixed: Commit entfernen und unstagen
git reset HEAD~1
git status --short
?? PaymentService.java
Dies ist der Standardmodus. Der Branch geht um einen Commit zurück, die Staging Area wird zurückgesetzt. PaymentService.java ist eine neue Datei, daher ist sie jetzt nicht verfolgbar (??). Du verwendest --mixed, wenn du einen Commit aufteilen möchtest: stage einzelne Teile und committe sie als mehrere kleinere Commits.
4.3. –hard: Alles verwerfen
git add PaymentService.java && git commit -m "PaymentService hinzugefügt"
git reset --hard HEAD~1
ls
HEAD is now at c3d4e5f total zu Order hinzugefügt
Customer.java Order.java
Commit, Staging Area und Working Tree aller verfolgten Dateien werden auf c3d4e5f zurückgesetzt. PaymentService.java verschwindet.
Hinweis:
reset --hardlöscht auch ungecommittete Änderungen an verfolgten Dateien. Diese Änderungen können nicht gerettet werden. Andererseits löscht es unverfolgte Dateien nicht. Um unverfolgte Dateien zu löschen, musst dugit clean -fdverwenden, und dieser Befehl ist genauso gefährlich.
4.4. Commits mit reflog retten
Commits, die zurückgesetzt wurden, sind nicht sofort verloren. Git zeichnet jede HEAD-Verschiebung in reflog auf:
git reflog -3
c3d4e5f HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: commit: PaymentService hinzugefügt
c3d4e5f HEAD@{2}: commit: PaymentService für Zahlungsabwicklung hinzugefügt
Du musst nur auf den Commit zurücksetzen, den du wiederherstellen möchtest:
git reset --hard 9f8e7d6
git log --oneline -1
9f8e7d6 (HEAD -> main) PaymentService hinzugefügt
Reflog existiert nur auf deinem Computer. Standardmäßig laufen Einträge, auf die nicht mehr verwiesen wird, nach 30 Tagen ab und werden vom Garbage Collector bereinigt. Daher solltest du so schnell wie möglich retten.
4.5. Wann man reset NICHT verwenden sollte
Du solltest keine Commits zurücksetzen, die du auf einen gemeinsam genutzten Branch gepusht hast. Nach dem Reset unterscheiden sich die lokale und die Remote-Historie, daher musst du git push --force verwenden. Deine Kollegen, die diese Commits gepullt haben, werden eine verwirrte Historie haben und könnten sie versehentlich erneut pushen.
Falls du unbedingt überschreiben musst, z. B. bei einem persönlichen Branch, verwende einen sichereren Befehl:
git push --force-with-lease
--force-with-lease lehnt den Push ab, wenn Remote neue Commits hat, die du noch nicht abgerufen hast. Dies verhindert, dass du die Arbeit anderer überschreibst.
5. git revert: Mit einem neuen Commit rückgängig machen
git revert löscht keine Historie. Dieser Befehl erstellt einen neuen Commit mit dem umgekehrten Inhalt des rückgängig zu machenden Commits. Die Historie wird nur länger, wird aber nicht umgeschrieben. Daher ist dies die sichere Methode für bereits gepushte Branches.
5.1. Einen Commit revertieren
git revert --no-edit HEAD
git log --oneline -3
ls
1a2b3c4 (HEAD -> main) Revert "PaymentService hinzugefügt"
9f8e7d6 PaymentService hinzugefügt
c3d4e5f total zu Order hinzugefügt
Customer.java Order.java
--no-edit verwendet die Standardnachricht, ohne den Editor zu öffnen. Der ursprüngliche Commit bleibt in der Historie. Der neue Commit löscht PaymentService.java. Der Body des neuen Commits enthält die Zeile This reverts commit 9f8e7d6..., die anderen hilft, den Ursprung zu finden.
Du kannst auch alte Commits in der Mitte der Historie revertieren, nicht nur HEAD:
git revert --no-edit c3d4e5f
cat Order.java
class Order {}
Wenn spätere Commits die gleichen Codezeilen geändert haben, meldet Git einen Konflikt. Du behebst den Konflikt, führst git add aus und dann git revert --continue. Um abzubrechen, verwendest du git revert --abort.
5.2. Mehrere Commits zu einem revertieren
Standardmäßig erzeugt jeder revertierte Commit seinen eigenen neuen Commit. Um sie zusammenzufassen, verwendest du --no-commit:
git revert --no-commit HEAD~3..HEAD~1
git commit -m "Änderungen an Customer und Order rückgängig machen"
Die Syntax A..B wählt Commits nach A bis einschließlich B. Git wendet jeden Commit umgekehrt auf die Staging Area an, dann machst du einen einzelnen Commit.
5.3. Einen Merge-Commit revertieren
Ein Merge-Commit hat zwei Parents. Daher muss Git wissen, welcher Branch die „Hauptlinie” bleiben soll:
git revert -m 1 <hash-merge-commit>
-m 1 wählt den ersten Parent aus, normalerweise der Branch, in den du merged hast (z. B. main). Alle Änderungen, die der andere Branch gebracht hat, werden rückgängig gemacht.
Hinweis: Nach dem Revertieren eines Merges wird Git alte Commits als „bereits gemerged” ansehen, wenn du denselben Branch erneut mergst. Diese Änderungen werden nicht zurückkommen. Um sie zurückzubringen, musst du den Revert-Commit selbst revertieren.
6. Welcher Befehl ist der richtige?
| Situation | Empfohlener Befehl |
|---|---|
| Unverfolgte Änderungen einer Datei verwerfen | git restore <file> |
| Eine Datei unstagen | git restore --staged <file> |
| Eine ältere Version einer Datei abrufen | git restore --source=<commit> <file> |
| Den letzten Commit reparieren/zusammenfassen, noch nicht gepusht | git reset --soft HEAD~1 |
| Einen Commit aufteilen, noch nicht gepusht | git reset HEAD~1 |
| Einen lokalen Commit völlig verwerfen | git reset --hard <commit> |
| Einen bereits gepushten Commit rückgängig machen | git revert <commit> |
Kurz zusammengefasst zur Erinnerung:
restorefunktioniert mit Dateien.resetfunktioniert mit Branches, wird lokal verwendet.revertfunktioniert mit gemeinsamer Historie.
7. Häufige Fehler
resetmitrevertauf gemeinsamen Branches verwechseln. Das ist der häufigste Fehler. Hat man bereits gepusht, verwenderevert.reset --hardausführen, während noch ungecommittete Änderungen vorhanden sind. Schau dir vor der Ausführunggit statusan. Lagere Änderungen mitgit stashvorher ein.- Denken,
reset --hardwürde das Verzeichnis bereinigen. Unverfolgte Dateien bleiben erhalten. Verwendegit clean -nzur Vorschau und danngit clean -fdzum Löschen. - Vergessen, dass
HEAD~1undHEAD^bei Merge-Commits unterschiedlich sind.HEAD^2ist der zweite Parent,HEAD~2ist der Großvater (Parent des Parents). Bei linearer Historie sindHEAD~1undHEAD^gleich. git checkout <file>aus Gewohnheit verwenden. Der Befehl funktioniert noch, abercheckouthat zu viele Aufgaben und es ist leicht, versehentlich in einen anderen Branch zu wechseln. In modernem Git sindrestoreundswitchklarer.
8. Fazit
In diesem Artikel hast du drei Möglichkeiten zum Rückgängigmachen in Git kennengelernt:
git restorestellt Dateiinhalte im Working Tree oder in der Staging Area wieder her, ohne die Historie zu beeinflussen.git resetverschiebt einen Branch zu einem anderen Commit, mit drei Modi--soft,--mixed,--hard. Dieser Befehl sollte nur für noch nicht gepushte Commits verwendet werden.git reverterstellt einen umgekehrten Commit und ist die sichere Wahl für gemeinsam genutzte Branches.
Wenn du einen Fehler machst, denk an git reflog. Die meisten verlorengegangenen Commits warten dort darauf, von dir abgerufen zu werden.