Git Reset vs Git Revert: Picking the Wrong One Can Ruin Your Day

by Eric Hanson, Backend Developer at Clean Systems Consulting

The Confusion That Causes Incidents

A developer pushes a bad commit to main. They want to undo it. They google "git undo commit," find git reset --hard HEAD~1, run it, and then git push --force origin main. Three other developers who have pulled since the original push now have diverged histories. The next time any of them pushes, Git rejects it. They pull to reconcile, and the bad commit comes back. The developer force-pushes again. The cycle repeats.

This scenario plays out on teams regularly because git reset and git revert look like they do the same thing — both "undo" changes — but they operate on entirely different models.

What git reset Does

git reset moves the branch pointer (HEAD) to a different commit. It changes where your current branch points.

# Before reset:
# A --- B --- C --- D  ← HEAD (main)

git reset HEAD~2

# After reset:
# A --- B  ← HEAD (main)
# (C and D still exist in object store, just unreachable from main)

The --soft, --mixed (default), and --hard flags control what happens to your working tree and index during the move:

# --soft: move HEAD, keep C and D's changes staged
git reset --soft HEAD~2

# --mixed (default): move HEAD, keep changes in working tree (unstaged)
git reset HEAD~2

# --hard: move HEAD, discard C and D's changes entirely
git reset --hard HEAD~2

Critical: in all three cases, the branch pointer moved. Commits C and D are no longer part of the branch history, even though they still exist in Git's object store until garbage collection runs.

This is why pushing after git reset requires --force. The remote branch still points to D. Your local branch now points to B. Git sees a non-fast-forward divergence and refuses to push without force.

What git revert Does

git revert creates a new commit that inverts the changes from a specific commit. It does not move any pointers. It appends to history.

# Before revert:
# A --- B --- C --- D  ← HEAD (main)

git revert C --no-edit

# After revert:
# A --- B --- C --- D --- C'  ← HEAD (main)
# where C' contains the inverse of C's changes

The branch now has five commits. C is still there. C' undoes C's effect. The history is honest about what happened.

Pushing after git revert is a normal fast-forward push. No force required. Anyone who pulls gets C' and their history converges correctly.

The Decision Rule

Use git reset when:

  • The commit(s) you're undoing haven't been pushed yet
  • You're on a personal branch that nobody else has pulled
  • You explicitly need to rewrite history (squashing commits before a PR)

Use git revert when:

  • The commit has been pushed to a shared branch
  • Other people may have pulled since the commit
  • You need an auditable record that a change was made and then undone

This is not a matter of preference. Using git reset on a shared branch and force-pushing is a decision that affects every other developer who has pulled from that branch. It should require team awareness and coordination, not a reflex.

The Danger of --hard

git reset --hard is the riskiest form because it discards working tree changes in addition to moving the pointer.

# This discards any uncommitted work in your working tree
git reset --hard HEAD~1

If you had uncommitted changes (staged or unstaged) when you ran this, they're gone. Not in reflog (reflog tracks commits, not working tree state). Not recoverable through normal means.

Before running --hard, always check what you have:

git status          # check for uncommitted changes
git stash           # stash them if you want to keep them
git reset --hard HEAD~1  # now it's safe

Reverting a Range of Commits

For undoing multiple sequential commits:

# Revert commits D, C, B (in reverse order to avoid conflicts)
git revert HEAD~3..HEAD --no-edit

This creates three revert commits. If the commits are related and you'd prefer one revert commit:

git revert HEAD~3..HEAD --no-commit
# stages all the inversions without committing
git commit -m "revert: undo last three commits (accidental push to main)"

Reverting a Merge Commit

Merge commits have two parents, so git revert needs to know which parent to treat as the mainline:

# -m 1 = revert to parent 1 (the branch that was merged INTO)
git revert -m 1 <merge-commit-sha>

One non-obvious consequence: if you revert a merge commit and later want to re-apply that branch's changes, you need to revert the revert first. The revert told Git "this branch's changes are not wanted" — simply re-merging the branch won't bring the changes back because Git considers them already merged.

The Force-With-Lease Safety Net

If you do need to force push (on a personal branch, or after coordination with the team), use --force-with-lease instead of --force:

# Safe: fails if someone else pushed after you last fetched
git push --force-with-lease origin feature/my-branch

# Dangerous: overwrites regardless of what the remote has
git push --force origin feature/my-branch

--force-with-lease checks whether the remote is at the SHA you last fetched. If someone else pushed in the interim, it fails. This prevents the scenario where your force push overwrites someone else's work that arrived after your last fetch.

The practical rule is simple: revert on shared branches, reset on private ones. Knowing the difference saves you from explaining to your team why their local history is broken.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

The Client Who Seems Difficult Is Often Just Unclear. Here Is How to Handle It.

Most contractor-client friction is not a personality problem — it is an information problem. Understanding that changes how you respond to it.

Read more

Why Some Projects Are Impossible to Save

“Can we still fix this?” Sometimes the real answer is: it was never set up to succeed.

Read more

Git Reflog: The Safety Net Most Developers Don't Know They Have

Reflog is a local log of every position HEAD has ever been in. It is the reason that almost nothing in Git is permanently irreversible — and most developers have never opened it.

Read more

The Backend Decisions I've Regretted — and What I Do Differently Now

Every experienced developer carries a graveyard of decisions that looked reasonable at the time and cost real money later. Here are mine, and the habits I built to stop repeating them.

Read more