Git, CI/CD & GitOpsXV · ResetModes
Hard reset — the destructive mode and when it is acceptable
What you'll learn
- Predict the state of HEAD, the index, and the working tree after `git reset --hard <commit>`
- Identify the three things --hard destroys: commits, staged entries, and working tree bytes
- Recognise the only acceptable use case: local-only mistakes on unpushed branches
- Use `git reflog` to recover the previous HEAD after a mistaken --hard
- Distinguish --hard from `git restore` for the working-tree-only discard case
Prerequisites
Practice
- breakfixAccidental hard reset
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 --hard <commit> is the only reset mode that
overwrites the working tree. It moves HEAD, resets the index,
and rewrites every file in the working directory to match
<commit>’s tree. Uncommitted edits are destroyed. Untracked
files (anything Git has never been told about) are left alone,
but everything tracked is replaced byte-for-byte with the
version at <commit>. This is the destructive mode, and the
production rule is unambiguous: --hard is acceptable only on
local-only unpushed branches. On any branch that has been
pushed, the safe undo is git revert, not git reset --hard.
What —hard does, precisely
A --hard reset performs three operations:
- The current branch pointer is rewritten to point at
<commit>. - The index is rewritten so every entry matches
<commit>’s tree. - Every file in the working tree that differs from
<commit>’s tree is overwritten with the version from<commit>’s tree.
COMMIT=abc1234
git reset --hard $COMMIT
git status
# On branch main
# nothing to commit, working tree clean
git log --oneline -5
# tip is $COMMIT; the intervening commits are gone from history
After the command, the three trees are in lockstep with
<commit>: HEAD points at it, the index matches its tree, and
the working tree matches its tree. There are no unstaged
changes, no staged changes, and no reachable commits between
<commit> and the previous tip.
What —hard destroys
Three categories of work are destroyed by --hard:
- Uncommitted edits. Any modification, addition, or
deletion in the working tree that is not yet committed is
overwritten. The bytes are replaced with the version from
<commit>’s tree. Untracked files (paths Git has never been told about) survive because they were never in the index to begin with. - Staged changes. Any file in the index that does not match
<commit>’s tree is unstaged by being overwritten in the index. The previous index entries are gone. - Commits between the previous tip and
<commit>. Any commit strictly between the previous branch tip and<commit>is now unreachable from any branch. The objects persist in.git/objects/, but they are eligible for garbage collection once the reflog entry that records them expires.
The first category is the one that loses work the engineer
cares about right now. The second category is the one that
makes --hard feel “thorough”. The third category is the one
that makes --hard destructive on a shared branch.
flowchart LR
A["previous tip"] --> B["undone commit"]
B --> C["commit"]
C --> D["target commit (--hard target)"]
D --> E["parent of target"]
style B fill:#fdd
style C fill:#fdd
style A fill:#fdd
The red-shaded commits are no longer reachable from any branch
after --hard. They are not deleted; they are orphaned.
The only acceptable use case
git reset --hard is acceptable on:
- A local-only feature branch that has never been pushed. The engineer is the only one with the branch, no one else has a remote-tracking ref pointing at the doomed commits, and the reflog retains the orphaned commits for 90 days.
- A throwaway experiment. A branch created to try something, where the engineer has decided the experiment was wrong.
git reset --hard is unacceptable on:
main,master,release/*, or any default branch. Even if the engineer is the only one pushing, 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 a--forcepush, which propagates the rewrite to every collaborator’s clone. - Any branch with in-flight pull requests. A
--hardreset on the source branch of a pull request rewrites the commits the PR is based on, which silently invalidates the PR.
Recovery via reflog
The safety net for a mistaken --hard is the reflog. The
reflog records every change to HEAD (and to every branch ref)
in .git/logs/. After a git reset --hard <commit>, the
reflog still contains an entry for the previous HEAD, marked as
the moment the reset happened:
git reset --hard HEAD~3
# oh no, that was wrong
git reflog
# a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
# 7e8f9a0 HEAD@{1}: commit: terraform apply plan v3
# 1f2e3d4 HEAD@{2}: commit: bump ansible collection version
git reset --hard 7e8f9a0
# recovered
The reflog entry survives for the default 90-day window
(configurable via gc.reflogExpire and gc.reflogExpireUnreachable).
The recovery is a single git reset --hard <orphan-oid>. The
orphan OID is the SHA printed by git reflog.
The recovery is not guaranteed. Two things can break it:
- Garbage collection has run and pruned the unreachable
objects.
git gcexpires reflog entries per the configured policy. A--hardreset followed bygit gcand the passage of the expiration window can make the orphan commits unrecoverable. - The clone is fresh. A new clone does not inherit the reflog from the original clone; the reflog is local to each repository.
—hard versus git restore —hard
Git 2.23 added git restore to give the working-tree-only
discard operation a clear name. The two commands are not the
same:
git reset --hard HEAD~1 # rewind HEAD, reset index, rewrite WT
git restore . # rewrite WT to match index (no HEAD move)
git restore --source=HEAD~1 . # rewrite WT to match HEAD~1 (no HEAD move)
git reset --hard moves HEAD and overwrites the working
tree. git restore overwrites the working tree but does not
move HEAD. For the case “I want to discard my working tree
edits but keep the current commit”, git restore is the right
tool. For the case “I want to discard my working tree edits
and rewind my commits”, git reset --hard is the right tool.
Production discipline
- Never
--hardon a pushed branch. Usegit revertfor the commit-level undo andgit restorefor the working-tree-only undo.--hardis reserved for local-only mistakes. - 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. - Commit the reflog recovery plan. When a
--hardreset is necessary, the first follow-up isgit reflogto record the pre-reset OID somewhere durable (a comment in the next commit, a ticket, a chat message). The 90-day window is not long in a long-running infrastructure repository.
Cross-course references
- Git, CI/CD & GitOps — Part XIV (Revert) — the additive alternative on shared branches; revert undoes commits without rewriting history or touching the working tree.
- Git, CI/CD & GitOps — Part VI (Resetting and restore) —
the working-tree-only discard via
git restore, the safer sibling of--hard. - Git, CI/CD & GitOps — Part XI (Rebase) —
--hardand interactive rebase share the history-rewrite property; both are unsafe on pushed branches. - Linux for Production Sysadmins — Part XXVII (Backup and
Recovery) — the analogue is
rm -rfversusmvto a holding directory: the destructive tool is faster in the moment and more expensive in every other moment.
Quiz
Knowledge check · 4 questions
Q1. An engineer has just pushed a commit to a shared `main` branch and realises the change was wrong. The team needs the change undone. What is the production-safe command?
Q2. `git reset --hard <commit>` overwrites the working tree bytes for every tracked file whose tree entry differs between `<commit>` and the current working tree.
Q3. What is the safety-net command for recovering the previous HEAD after a mistaken `git reset --hard`, and what is its default retention window?
Q4. Recover from a mistaken `git reset --hard` on a feature branch that lost two hours of working tree edits, using the reflog.
An engineer ran `git reset --hard HEAD~1` thinking the most recent commit was the one to undo, but the actual most recent commit was the one containing the two hours of work. The working tree was clean before the reset, so the changes from the undone commit are gone from all three trees. The engineer needs the previous HEAD back.
Passing score: 75%. Answers are checked in this browser.