Sooner or later every Git user needs to undo something — a typo in a commit message, a bug that slipped in, or a change pushed to the wrong branch. Git gives you three different commands for it: `git revert`, `git reset`, and `git restore`. They sound similar, but each one undoes something different and the consequences vary.

This tutorial walks through what each command does, when to reach for it, and a few real-world recipes so you can pick the right one in the moment.

## The three "undo" commands at a glance

| Command | What it touches | Safe to use after pushing? |
|---|---|---|
| `git revert` | Creates a **new commit** that reverses an earlier one. History is preserved. | ✅ Yes |
| `git reset` | Moves the branch pointer **back in history**. Older commits are dropped from the branch. | ⚠️ Only if you haven't pushed (or you understand `--force-with-lease`) |
| `git restore` | Restores files in your **working tree or staging area** without touching commits. | ✅ Yes (file-level only) |

A useful way to remember the difference: `revert` is the public-friendly undo, `reset` is the private rewrite, and `restore` is for files rather than commits.

## `git revert` — undo a commit safely

`git revert` is the right tool when the commit you want to undo has already been pushed, or when you're working on a branch other people pull from. It doesn't delete the original commit — it appends a brand new one that reverses the changes. That keeps history honest and avoids rewriting anything other developers may have based their work on.

Find the commit you want to undo first. The hash is the long string next to each entry in [the commit history](/git/viewing-previous-commits):

```bash
git log --oneline
```

```
8fd3350 Add the correct link to Brie
5eba8ab Remove Cheddar
d240853 Initial commit
```

To undo the `Remove Cheddar` commit, pass its hash (or the first few characters) to `git revert`:

```bash
git revert 5eba8ab
```

Git opens your editor with a prefilled commit message — usually `Revert "Remove Cheddar"` — which you can edit or accept. Once you save and close, Git creates a new commit that re-adds whatever the original commit removed.

`git log --oneline` now shows the new commit on top, with the original commit still intact below it:

```
e74b9a1 Revert "Remove Cheddar"
8fd3350 Add the correct link to Brie
5eba8ab Remove Cheddar
d240853 Initial commit
```

Push the revert like any other commit and the change reaches your teammates:

```bash
git push
```

### Reverting a range of commits

To revert several commits at once, pass a range:

```bash
git revert HEAD~3..HEAD
```

Git walks the range in reverse order and creates one revert commit for each, which keeps the per-commit history clean. If you'd prefer a single combined revert commit instead, add `--no-commit` and commit manually after Git has finished applying the inverses:

```bash
git revert --no-commit HEAD~3..HEAD
git commit -m "Revert last 3 commits"
```

### Reverting a merge commit

Reverting a merge needs an extra hint, because Git has to know which side of the merge to keep. The `-m 1` flag tells it to keep the first parent — typically the branch you were merging *into*:

```bash
git revert -m 1 <merge-hash>
```

**Note:** Reverting a merge does not delete the merged branch's commits — they're still in history. If you later want to merge that branch again, you'll have to revert the revert (`git revert <revert-hash>`), or rebase the feature branch.

## `git reset` — move the branch pointer back

`git reset` is the private undo. It moves your current branch's pointer back to an earlier commit, effectively pretending the more recent commits never happened on this branch. Anyone who has already pulled those commits will still have them, which is why `reset` is reserved for work that hasn't been shared yet.

It has three modes, controlled by a flag:

| Mode | Moves HEAD | Touches the index (staging) | Touches the working tree | Use when |
|---|---|---|---|---|
| `--soft` | ✓ | ✗ | ✗ | You want to redo the commit but keep the staged changes |
| `--mixed` (default) | ✓ | ✓ (unstages) | ✗ | You want the changes back as edits, before staging |
| `--hard` | ✓ | ✓ | ✓ (discards) | You want the changes gone completely |

### Soft reset: redo the last commit

Imagine you just committed but the message is wrong, or you forgot to include a file:

```bash
git reset --soft HEAD~1
```

The commit is gone, but every change it contained is still staged and ready to be committed again. Stage the missing file (or fix whatever needs fixing) and run `git commit` to produce a clean replacement. For nothing more than fixing the message, `git commit --amend` is even quicker.

### Mixed reset: pull changes back into the working tree

`git reset HEAD~1` (with no flag) is the default mixed reset. It removes the commit and unstages the changes — they're still in your working tree as uncommitted edits, ready for you to re-stage selectively.

This is the right choice when you committed too eagerly and want to split the changes into two or more commits.

### Hard reset: throw the changes away

`git reset --hard` is destructive. It moves HEAD, clears the staging area, and overwrites your working tree to match the target commit. Anything that hasn't been committed (or stashed) is lost.

```bash
git reset --hard HEAD~2
```

That command throws away the last two commits and any uncommitted edits. Useful when you've made a mess on a private branch and want a clean slate — dangerous if you forget that uncommitted work disappears too. If you're unsure, [stash your changes](https://git-scm.com/docs/git-stash) first.

### Recovering from a bad reset

`git reset --hard` looks final, but Git keeps a safety net called the reflog. Every move of HEAD is logged, so you can almost always get back to where you were:

```bash
git reflog
```

```
e74b9a1 HEAD@{0}: reset: moving to HEAD~2
8fd3350 HEAD@{1}: commit: Add the correct link to Brie
5eba8ab HEAD@{2}: commit: Remove Cheddar
```

To restore the branch to the state before the reset, point it back at the relevant entry:

```bash
git reset --hard HEAD@{1}
```

The reflog stays around for about 90 days by default, which is enough rope to recover from most accidents.

## `git restore` — undo file-level changes

`git restore` was introduced to separate file-level operations from `git checkout`, which had grown to mean too many things. It doesn't move HEAD or change history — it works only on the files in your working tree and the staging area.

### Discard unstaged changes in a file

If you've edited a file and want to throw the edits away:

```bash
git restore path/to/file
```

The file is rewritten to match the version in the last commit. Unstaged changes are gone, so use it intentionally.

### Unstage a file

If you accidentally `git add`ed something, unstage it without touching the file's contents:

```bash
git restore --staged path/to/file
```

The file goes back to being an unstaged modification, ready to be edited or staged again.

### Restore a single file from a previous commit

To bring a single file back to the way it looked in an earlier commit, point `--source` at that commit:

```bash
git restore --source 8fd3350 path/to/file
```

The file is rewritten to the version from that commit, but Git doesn't make a new commit for you — the change shows up as an unstaged modification you can review, stage, and commit yourself.

This is the cleanest way to undo a single file without affecting the rest of the project, and it's safe to use after pushing because no commits move.

## Practical recipes

A short reference for the situations that come up most often:

**"I just committed and want to change the message" —** `git commit --amend`. Edits the most recent commit in place. Don't use this on commits you've already pushed.

**"I committed and want the changes back as edits, before staging" —** `git reset HEAD~1` (mixed reset). The commit is gone, the files are back to unstaged.

**"I want to throw away my last 2 commits completely" —** `git reset --hard HEAD~2`. Destructive. Make sure those commits aren't on a shared branch.

**"I pushed a bad commit and need to undo it on the remote" —** `git revert <hash>` then `git push`. Safest option — adds a new commit instead of rewriting history.

**"I want to discard everything I've changed since the last commit" —** `git restore .` for unstaged edits, or `git reset --hard HEAD` if you've staged some too.

**"I want to bring back one file from an earlier commit" —** `git restore --source <hash> path/to/file`. Other files are untouched.

**"I accidentally `git add`ed a file" —** `git restore --staged path/to/file` (or the older `git reset HEAD path/to/file`).

These all work the same way against [GitHub](/blog/how-to-set-up-git-pull-deployments-with-deployhq), GitLab, and Bitbucket — undo is a local operation, and pushing the result is what propagates it to your remote.

## When you have to rewrite pushed history

Sometimes you really do need to use `git reset` on a branch that has already been pushed — for example, when a commit contains a secret that has to disappear from history entirely. The standard `git push` will refuse, because it would overwrite commits on the remote. A force-push is the only way:

```bash
git push --force-with-lease
```

`--force-with-lease` is the safer cousin of `--force`. It refuses to overwrite the remote if someone else has pushed commits since you last fetched, which prevents you from accidentally wiping out a teammate's work.

Even with that safeguard, force-pushing on a [shared branch](/git/branching-and-merging) breaks anyone who has the old history checked out. They'll have to reset their local branch to match the new remote, and any work they had based on the rewritten commits will need to be rebased. As a rule of thumb: rewrite freely on your own branches, never on `main`, and on shared feature branches only after agreeing with everyone who has pulled.

## revert, reset, restore — quick decision table

| What you want to do | Command |
|---|---|
| Undo a pushed commit safely | `git revert <hash>` |
| Undo a pushed merge commit | `git revert -m 1 <merge-hash>` |
| Redo the last commit (keep staged changes) | `git reset --soft HEAD~1` |
| Bring last commit's changes back as unstaged edits | `git reset HEAD~1` |
| Throw the last N commits away entirely | `git reset --hard HEAD~N` |
| Recover from a bad `--hard` reset | `git reflog` then `git reset --hard HEAD@{N}` |
| Discard unstaged changes to a file | `git restore <file>` |
| Unstage a file | `git restore --staged <file>` |
| Bring one file back from a previous commit | `git restore --source <hash> <file>` |
| Fix the most recent commit's message | `git commit --amend` |

## Next steps

- [Viewing the commit history](/git/viewing-previous-commits) — find the right hash before reverting
- [Committing file changes](/git/committing-file-changes) — the commands behind every undo
- [Publishing local changes](/git/publishing-local-changes) — push your revert to the remote

---

`git revert` undoes a commit at the source. When the commit has already been deployed, you'll often want to undo it in production as well — [DeployHQ's one-click rollback](https://www.deployhq.com/features/one-click-rollback) restores a previous build on your servers without re-running the whole pipeline, so a source-side revert and a production-side rollback take seconds rather than a deploy cycle. [Try DeployHQ free](https://www.deployhq.com/signup).
