Git, CI/CD & GitOpsXIV · RevertFoundations
Revert versus reset — additive undo versus history rewriting
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
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 rungit addon 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 <commit> 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:
- 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.
- Use
--softor--mixedfor local undo, never--hardby reflex.--harddiscards work;--mixedand--softkeep it as unstaged or staged changes you can re-commit. - Use
--hardonly 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. - 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 resetis 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 revertis 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
Q1. An engineer wants to undo a commit on a shared branch and proposes `git reset --hard <commit>~1 && git push --force`. What is the primary problem with this approach?
Q2. `git revert <commit>` rewrites the OID of `<commit>`; `git reset <commit>` does not rewrite any OID.
Q3. Name the two tools that undo a commit, and state which one is safe on a shared branch.
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.