Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXV · ResetSafety

Reset safety and recovery — when not to use --hard, reflog-based recovery, --merge and --keep

Advanced⏱ ~24 mingit

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

Not yet marked complete on this device.

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:

  1. 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.
  2. 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 --hard reset on a pushed branch must be followed by --force or --force-with-lease, which propagates the rewrite to every collaborator.
  • Any branch with in-flight pull requests. A --hard reset 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 --hard reset 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:

  • --merge for “rewind the branch, keep my uncommitted edits”. The engineer has a clean working tree except for some unrelated experiments that should survive the rewind.
  • --keep for “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.
  • --hard for “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:

CommandEffectSafe on shared branch?Touches working tree?
git reset <commit>Moves HEAD back to <commit>NoPer mode
git revert <commit>Adds a new commit that inverts <commit>YesOnly if conflicts
git restore <path>Replaces working tree file with index versionYes (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

  1. Verify with git status before --hard. A clean working tree means --hard will discard nothing important. An unclean working tree means --hard will destroy uncommitted edits; the right move is to commit or stash first.
  2. Record the reflog OID after every --hard reset. Even a deliberate --hard can turn out to have been wrong; the reflog OID is the recovery anchor.
  3. Prefer --merge or --keep over --hard when in doubt. Both modes refuse to clobber local edits, which is exactly the behaviour an engineer wants when the working tree is not clean.
  4. Reach for git revert on shared branches. The commit is preserved in history; the inverse is a new commit; the branch tip moves forward. No existing OID is rewritten.
  5. Reach for git restore for 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 --hard and git rebase --abort share the “undo by rewinding” pattern; both are unsafe on pushed branches.
  • Linux for Production Sysadmins — Part XXVII (Backup and Recovery) — the analogue is mv to a holding directory versus rm -rf: the safer tool keeps a recovery path.

Quiz

Knowledge check · 4 questions

  1. 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?

  2. Q2. `git reset --keep <commit>` refuses to run if any working tree file would be overwritten by the reset.

  3. Q3. Name the three production undo commands for Git and the one rule that distinguishes when each is appropriate.

  4. 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.