Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXIV · RevertFoundations

Revert versus reset — additive undo versus history rewriting

Advanced⏱ ~20 mingit

What you'll learn

  • Explain the structural difference between `git revert` and `git reset` in terms of how each moves the branch tip
  • Identify why `git reset` rewrites the OID of every commit that came after the target
  • Choose the correct `--soft`, `--mixed`, or `--hard` variant of `git reset` for a local-only undo
  • Recognise that `git revert` is the only safe undo on a shared branch
  • Map the choice between revert and reset onto the merge-vs-rebase choice from Part XII

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

git revert and git reset both undo a change, but they do it in opposite directions on the branch. Revert moves the branch tip forward by adding a new commit that inverts the change. Reset moves the branch tip backward by re-pointing the current branch at an earlier commit. Forward means additive — every existing OID is preserved. Backward means subtractive — every commit between the new tip and the old tip is rewritten, and the OIDs of those rewrites no longer match what anyone else has fetched.

That structural difference is the entire lesson. The rest is mechanics.

What reset actually does

git reset <target> moves the current branch so that it points at <target>. The commits that used to be on the branch but are no longer reachable from the new tip are not deleted immediately; they become unreachable, kept alive by the reflog, and eventually garbage-collected once nothing references them. To anyone with a clone of the old branch, those commits are still there in their repository — but they are no longer reachable from the branch they thought they were on, and they must reconcile their working state against the new history.

TARGET=abc1234
git reset $TARGET
git log --format='%H %s' -3
# tip is now $TARGET; the commits that used to be after it are
# no longer reachable from the branch

The three variants of git reset differ only in what they do to the working tree and index after moving the branch tip:

flowchart LR
    A["HEAD"] --> B["target commit"]
    B --> C["working tree"]
    B --> D["index (staging area)"]
    A -- "--soft" --> E["keep tree and index as they were"]
    A -- "--mixed (default)" --> F["keep tree, reset index to target"]
    A -- "--hard" --> G["reset tree and index to target (destructive)"]
  • git reset --soft <target> moves the branch tip and changes nothing else. The working tree and index still contain the changes that were committed after <target>, so the changes are staged as if you had run git add on them. Use this when you want to “uncommit” but keep the work.
  • git reset --mixed <target> (the default) moves the branch tip and resets the index to match <target>, but leaves the working tree alone. The changes after <target> become unstaged modifications. Use this when you want to undo a commit but keep the files in the working tree as uncommitted edits.
  • git reset --hard <target> moves the branch tip and rewrites the working tree and index to match <target>. The changes after <target> are gone from the working tree. Use this only when you are certain you want to discard those changes permanently on your local machine.

What revert actually does

git revert <commit> does not move the branch tip backward. It computes the inverse of the changes introduced by <commit> and records that inverse as a new commit on top of the current branch. The original commit stays in the history; its OID is unchanged; every commit that came after it is also unchanged.

COMMIT=abc1234
git revert $COMMIT
git log --format='%H %s' -3
# tip is now a new commit whose subject is "Revert \"<original>\""
# and whose body is "This reverts commit $COMMIT."

The branch tip moves forward by one. The history gains a commit that says, in plain language, “the change from &lt;commit&gt; is no longer in the tree at HEAD.” No existing OID is rewritten.

The contrast in one diagram

The same starting history, with one undo via reset and one undo via revert, produces two completely different histories:

gitGraph
    commit id: "a1"
    commit id: "b_bad"
    commit id: "c1"
    commit id: "d1"
    branch undo-via-reset
    checkout undo-via-reset
    commit id: "a1_rebased"
    commit id: "c1_rebased"
    commit id: "d1_rebased"
    checkout main
    branch undo-via-revert
    commit id: "Revert B" tag: "new HEAD"

In the reset branch, commits C and D have new OIDs because their parent changed. In the revert branch, C and D are untouched, and B is still in the history next to a commit that says “Revert B”.

Why the choice matters on shared branches

A shared branch is a branch that more than one engineer pushes to. Every collaborator’s local repository has the OIDs of the commits on the branch, by way of git fetch. When you git reset a shared branch and force-push the new tip, you have produced a world where your local repository has commits C' and D' (with new OIDs), and every collaborator’s local repository still has the old C and D (with the original OIDs). The next git fetch from a collaborator’s machine does not move their branch tip — it just adds the new commits to their object database. Their branch tip still points at the old D. They must reconcile.

# On the reset-and-force-push side
git push --force origin main

# On the collaborator side
git fetch origin
git status
# Your branch and 'origin/main' have diverged,
# and have N and M different commits each, respectively.

The collaborator’s two options are: rebase their in-flight work on top of the new tip (which means rewriting their own commits’ OIDs), or throw away their in-flight work and start over from the new tip. Neither is what they wanted to do at the start of the day. This is the cost that git reset on a shared branch imposes on everyone except the person who ran it.

git revert imposes no such cost. The original commits stay in place, every collaborator’s local OIDs are still valid, the next git fetch brings in the revert commit, and the only thing that changes for the team is the tree at HEAD — which is exactly what the revert was supposed to change.

UnderTheHood: OIDs and reachability

A commit’s OID is the SHA of its content object, which includes the parent reference. When git reset moves a branch tip from D back to C, the OID of D does not change (its parent is still whatever it was before), but the OID of every commit whose parent was D does change, because their parent reference is now different. In a chain A -> B -> C -> D -> E, resetting from E to B keeps A, B, C, and D reachable (they are still in the object database) but the branch tip now points at B, so E is unreachable. If E is then amended or recommitted, the new E' has a different OID because its parent is now B.

Production discipline

The production discipline has four rules:

  1. Default to revert on shared branches. If a branch has ever been pushed and another person might have fetched it, revert is the only undo. Reset is for local-only history.
  2. Use --soft or --mixed for local undo, never --hard by reflex. --hard discards work; --mixed and --soft keep it as unstaged or staged changes you can re-commit.
  3. Use --hard only when the discarded work is known to be reproducible from somewhere else — a topic branch tip you are about to delete, a WIP commit whose contents you have copied out of band, or a published branch whose tip is genuinely irrecoverable to discard.
  4. Never reset and force-push a branch another person is working from. The cost is paid by everyone except you.

Cross-course references

  • Git, CI/CD & GitOps — Part XI (Rebase)git reset is the local-only companion to rebase; together they are the two history-rewriting tools.
  • Git, CI/CD & GitOps — Part XII (Trade-offs) — the merge-vs-rebase choice at the team level is the same shape as the revert-vs-reset choice at the per-commit level: additive history versus rewritten history.
  • Git, CI/CD & GitOps — Part XIII (Cherry-pick)git revert is structurally a cherry-pick of the inverse of a commit; the three-way merge mechanics are shared.
  • Linux for Production Sysadmins — Part XII (RepoSecurity) — the same “additive history on shared state” principle applies to apt repository operations: a bad package is replaced by pushing a new version, not by rewriting the repository.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer wants to undo a commit on a shared branch and proposes `git reset --hard &lt;commit&gt;~1 && git push --force`. What is the primary problem with this approach?

  2. Q2. `git revert &lt;commit&gt;` rewrites the OID of `&lt;commit&gt;`; `git reset &lt;commit&gt;` does not rewrite any OID.

  3. Q3. Name the two tools that undo a commit, and state which one is safe on a shared branch.

  4. Q4. Diagnose the recovery plan when an engineer has reset and force-pushed `main` and three other engineers have already fetched the new tip.

    Engineer A reset `main` to `HEAD~3` and force-pushed. Engineers B, C, and D had each fetched `main` earlier in the day and have local feature branches based on the old tip. The team has a GitOps controller reading `main` and a CI pipeline that requires signed commits.

Passing score: 75%. Answers are checked in this browser.