Git, CI/CD & GitOpsXV · ResetSafety
Reset safety and recovery — when not to use --hard, reflog-based recovery, --merge and --keep
What you'll learn
- Apply the production rule for when `git reset --hard` is acceptable and when it is forbidden
- Use `git reflog` to recover the previous HEAD after a mistaken --hard reset
- Distinguish --merge and --keep from --hard and identify the cases where they are safer
- Choose between reset, revert, and restore for the three production undo operations
- Recognise the reflog retention window and its implications for durable recovery
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 reset is the most dangerous of the three undo commands
because it can rewrite history, rewrite the index, and rewrite
the working tree — sometimes all three in a single invocation.
The previous lessons in this part cover each mode in detail;
this one consolidates the safety discipline. The production
rules are four: (1) --hard is forbidden on pushed branches,
(2) the recovery path is the reflog, (3) --merge and --keep
are the conditional alternatives when --hard would clobber
local edits, and (4) git revert is the additive alternative
for any commit that has been shared.
When —hard is acceptable
git reset --hard <commit> is acceptable in exactly two
scenarios:
- The branch has never been pushed. The engineer is the only one with the branch, no remote-tracking ref points at the doomed commits, and the reflog retains the orphaned commits for the 90-day default window.
- The branch is a throwaway experiment. The branch was created to try something, the engineer has decided the experiment was wrong, and the branch can be deleted.
git reset --hard <commit> is forbidden in every other
scenario. The forbidden cases include:
main,master,release/*, or any default branch. Even if the engineer is the only one with push access, the default branch is read by every collaborator and every CI run.- Any branch that has been pushed to a shared remote. A
--hardreset on a pushed branch must be followed by--forceor--force-with-lease, which propagates the rewrite to every collaborator. - Any branch with in-flight pull requests. A
--hardreset on a source branch rewrites the commits the PR is based on, silently invalidating the PR. - Any branch being read by a long-running CI pipeline. A
--hardreset on a branch that a CI pipeline has cached causes the pipeline to re-run against an unexpected tree.
Recovery via reflog
The safety net for a mistaken --hard reset is the reflog.
The reflog is a per-repository log of every change to HEAD
and every branch reference, written to .git/logs/. After a
git reset --hard <commit>, the reflog still contains an
entry for the previous HEAD:
git reset --hard HEAD~3
# oh no, that was the wrong one
git reflog
# a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
# 7e8f9a0 HEAD@{1}: commit: apply terraform v3 plan
# 1f2e3d4 HEAD@{2}: commit: bump ansible version
git reset --hard 7e8f9a0
# recovered
The reflog entry is the OID of the commit that was at HEAD
immediately before the reset. The recovery is a single
git reset --hard <orphan-oid>. The orphan OID is the SHA
printed by git reflog at the HEAD@{1} entry (one before
the reset entry).
The retention window for reflog entries is 90 days for reachable
commits and 30 days for unreachable commits by default. Both
windows are configurable via gc.reflogExpire and
gc.reflogExpireUnreachable. The retention shrinks if git gc
runs with a shorter expiration, which it does in some
auto-gc-enabled workflows.
git config gc.reflogExpire # default: 90 days
git config gc.reflogExpireUnreachable # default: 30 days
git reflog expire --expire=now # manually prune
git reflog expire --expire-unreachable=now
—merge and —keep as conditional alternatives
The two conditional modes are safer than --hard for the case
where the working tree holds uncommitted edits. They refuse to
run if the reset would clobber the edits, which is exactly the
behaviour an engineer wants when they are not sure whether the
working tree holds work that matters.
COMMIT=abc1234
git reset --merge $COMMIT
# resets HEAD + index; resets working tree ONLY if no local file would be clobbered
# refuses with error if any working tree file would be overwritten
git reset --keep $COMMIT
# resets HEAD + index + working tree; refuses if any local edit would be clobbered
--merge is the right tool when the engineer wants to rewind
the branch tip and the index but preserve any working tree
edits that are unrelated to the target commit. --keep is the
mirror image: it resets everything together but refuses if any
local edit would be overwritten.
The use cases in production:
--mergefor “rewind the branch, keep my uncommitted edits”. The engineer has a clean working tree except for some unrelated experiments that should survive the rewind.--keepfor “get the working tree onto a different commit without losing my edits”. The engineer has been editing locally and wants to switch to a different commit’s tree, but only if the local edits will survive.--hardfor “I want a clean slate, no questions asked”. The engineer is sure the working tree is disposable.
flowchart TB
A["git reset <commit>"] --> B{"working tree disposable?"}
B -- "yes" --> C["--hard (or --soft/--mixed)"]
B -- "no" --> D{"edits unrelated to <commit>?"}
D -- "yes" --> E["--merge (rewind, preserve edits)"]
D -- "no, edits ARE in target" --> F["--keep (refuse if clobber)"]
The decision tree is the same as for git stash versus git checkout: the safer the mode, the more conditions it imposes
on what the working tree looks like.
reset, revert, restore — the three production undos
The three undo commands have distinct production roles:
| Command | Effect | Safe on shared branch? | Touches working tree? |
|---|---|---|---|
git reset <commit> | Moves HEAD back to <commit> | No | Per mode |
git revert <commit> | Adds a new commit that inverts <commit> | Yes | Only if conflicts |
git restore <path> | Replaces working tree file with index version | Yes (local) | Yes (only) |
git reset is the subtractive undo: it moves the branch tip
backward and (with --hard) rewrites the working tree. It is
safe on local-only unpushed branches and unsafe on shared
branches.
git revert is the additive undo: it produces a new commit
whose content is the inverse of <commit> and appends it to
the current branch. It is safe on shared branches because no
existing commit’s OID is rewritten.
git restore is the surgical undo: it operates on the working
tree (or the index, with --staged) for specific paths, and it
never moves HEAD. It is safe on shared branches because the
branch tip is unchanged.
The three commands are not interchangeable. The production rule
is: use git restore for the working-tree-only discard, use
git revert for the shared-branch commit undo, use git reset
for the local-only branch rewind.
Production discipline
- Verify with
git statusbefore--hard. A clean working tree means--hardwill discard nothing important. An unclean working tree means--hardwill destroy uncommitted edits; the right move is to commit or stash first. - Record the reflog OID after every
--hardreset. Even a deliberate--hardcan turn out to have been wrong; the reflog OID is the recovery anchor. - Prefer
--mergeor--keepover--hardwhen in doubt. Both modes refuse to clobber local edits, which is exactly the behaviour an engineer wants when the working tree is not clean. - Reach for
git reverton shared branches. The commit is preserved in history; the inverse is a new commit; the branch tip moves forward. No existing OID is rewritten. - Reach for
git restorefor working-tree-only discards. The branch tip is unchanged, the index is unchanged, and the operation is path-scoped.
Cross-course references
- Git, CI/CD & GitOps — Part XIV (Revert) — the additive undo for shared branches; the contrast with reset is history-preserving versus history-rewriting.
- Git, CI/CD & GitOps — Part VI (Resetting and restore) —
the index and working-tree operations via
git restore. - Git, CI/CD & GitOps — Part XI (Rebase) —
git reset --hardandgit rebase --abortshare the “undo by rewinding” pattern; both are unsafe on pushed branches. - Linux for Production Sysadmins — Part XXVII (Backup and
Recovery) — the analogue is
mvto a holding directory versusrm -rf: the safer tool keeps a recovery path.
Quiz
Knowledge check · 4 questions
Q1. An engineer ran `git reset --hard HEAD~3` on a feature branch by mistake and lost three commits from the branch tip. The branch has not been pushed. What is the recovery command?
Q2. `git reset --keep <commit>` refuses to run if any working tree file would be overwritten by the reset.
Q3. Name the three production undo commands for Git and the one rule that distinguishes when each is appropriate.
Q4. Choose the right command — reset, revert, or restore — for three production undo scenarios on an infrastructure repository, and justify each choice.
An infrastructure team manages a Terraform monorepo on a default branch named `main`. Three undo scenarios need decisions. (A) An engineer committed a sensitive credential file to `main` by accident; the commit has been pushed and is now on every collaborator's clone. (B) An engineer has uncommitted edits to a local feature branch and wants to discard them and rewind the branch by one commit. (C) An engineer has edits to `terraform/main.tf` in the working tree that should never have been made; the file should be replaced with the version from HEAD.
Passing score: 75%. Answers are checked in this browser.