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

by Arif Ikhsanudin, Backend Developer

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

When You Spend More Time Debugging Than Coding

You sit down to write a few lines of code and suddenly realize you’ve spent the last three hours chasing a bug. Why does debugging sometimes feel like the real work?

Read more

Your Code Just Crashed the Client’s Server—Now What?

Panic sets in, emails start flying, and your stomach drops. A crash happened, but it’s not the end of the world—you can handle this.

Read more

Load Balancing Is Not Just Distributing Traffic. Here Is What It Really Does.

Load balancers do a lot more than split requests across servers. Understanding their full role — health checking, session management, TLS termination, connection handling — changes how you design around them.

Read more

Why Cheap Freelancing Can Damage Your Career

Ever wonder why some freelancers seem stuck forever in low-paying gigs? Cheap freelancing isn’t just about money—it can quietly sabotage your career.

Read more