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 HEAD auf 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 restore verworfene Änderungen sind endgültig verloren. Git hat sie nie gespeichert, daher kann auch reflog sie nicht retten. Wenn du dir nicht sicher bist, führe git stash aus, anstatt restore zu 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 --hard lö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 du git clean -fd verwenden, 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:

  • restore funktioniert mit Dateien.
  • reset funktioniert mit Branches, wird lokal verwendet.
  • revert funktioniert mit gemeinsamer Historie.

7. Häufige Fehler

  • reset mit revert auf gemeinsamen Branches verwechseln. Das ist der häufigste Fehler. Hat man bereits gepusht, verwende revert.
  • reset --hard ausführen, während noch ungecommittete Änderungen vorhanden sind. Schau dir vor der Ausführung git status an. Lagere Änderungen mit git stash vorher ein.
  • Denken, reset --hard würde das Verzeichnis bereinigen. Unverfolgte Dateien bleiben erhalten. Verwende git clean -n zur Vorschau und dann git clean -fd zum Löschen.
  • Vergessen, dass HEAD~1 und HEAD^ bei Merge-Commits unterschiedlich sind. HEAD^2 ist der zweite Parent, HEAD~2 ist der Großvater (Parent des Parents). Bei linearer Historie sind HEAD~1 und HEAD^ gleich.
  • git checkout <file> aus Gewohnheit verwenden. Der Befehl funktioniert noch, aber checkout hat zu viele Aufgaben und es ist leicht, versehentlich in einen anderen Branch zu wechseln. In modernem Git sind restore und switch klarer.

8. Fazit

In diesem Artikel hast du drei Möglichkeiten zum Rückgängigmachen in Git kennengelernt:

  • git restore stellt Dateiinhalte im Working Tree oder in der Staging Area wieder her, ohne die Historie zu beeinflussen.
  • git reset verschiebt 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 revert erstellt 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.

gitversion-controlresetrevertrestore