| English | 中文 | Interactive demo | Runnable lab |
Before reset --hard, rebase, amend, branch deletion, or a forced remote update, determine whether a recovery clue exists. Git recovery comes from objects, references, and the local reflog, not from the fact that a command was just run.
This chapter establishes recovery capability before explaining dangerous rewrites. It separates “the branch name disappeared” from “the object is no longer recoverable.”
refs/heads/main -> C1
refs/heads/recovered -> C2
HEAD reflog earlier value -> C3
no ref, no reflog, object pruned -> cannot resolve C4
git add or git commit has no content for Git objects to recover; use editor, filesystem, or other backups.The official documentation describes reflogs as records of reference-tip updates in the local repository and supports revision syntax such as HEAD@{2} for earlier HEAD values. git-reflog
The lab uses this object relationship:
main -> C1 baseline
C2 keep recoverable commit
no ref reaches it
HEAD reflog still names C2
C3 discard after expiry
no ref reaches it
HEAD reflog still names C3
After running git branch recovered C2:
refs/heads/recovered -> C2
C2 again has a direct reference, so GC treats it as protected. For C3, explicitly expire reflogs and then prune:
no ref -> no reflog -> object pruned
The object ID may no longer resolve through git cat-file. Recovery therefore has time and retention conditions; it is not a permanent-backup strategy.
git log main walks ancestors from main. If main was reset from C2 to C1, then C2 is no longer part of main’s history.
That does not prove C2 has disappeared from the repository’s disk. By default, git fsck uses the index, refs, and reflogs as heads for its reachability trace. Adding --no-reflogs helps reveal an old object that remains only through a reflog clue. git-fsck
git fsck --no-reflogs --unreachable
Expected output includes a line such as:
unreachable commit <old-commit-id>
This is a check result, not a promise about how long the object remains recoverable. The object database, reflog expiration, and GC status can all change the conclusion.
Inspect local HEAD and branch movement:
git reflog show --oneline HEAD
git reflog show --oneline main
The output can include commits, resets, and checkouts. HEAD@{1} means the previous HEAD reflog value; main@{1} means main’s previous value. gitrevisions defines these forms as the local ref’s value at an earlier time or ordinal. gitrevisions
Read local retention overrides:
git config --get gc.reflogExpire
git config --get gc.reflogExpireUnreachable
Without a local override, Git’s current documentation gives defaults of gc.reflogExpire=90 days and gc.reflogExpireUnreachable=30 days. The first applies to general entries; the second removes entries unreachable from the current branch tip earlier. Repository configuration, organization templates, manual expiration, and maintenance can all alter actual retention. git-reflog expiration options
Reflog is useful for short-lived incident recovery. It does not replace a remote repository, bundle, backup, or formal release artifact. Each clone has its own reflog; HEAD@{1} in repository A does not automatically exist in repository B.
Before a local rewrite, run:
git status --short
git branch safety/before-rewrite HEAD
git show --no-patch --oneline safety/before-rewrite
git reflog show -n 5 HEAD
These commands have three purposes:
git status --short exposes uncommitted state first. Choose an appropriate way to preserve edits that matter.safety/before-rewrite gives the current commit an explicit reference instead of relying only on reflog.git reflog records the local recovery clues available before the operation, so they can be checked afterward.If history was pushed or other people may have built on it, also inspect remote, collaborator, and release impact. A shared undo often needs a new git revert commit. Git has no automatic recovery for database writes, deployments, messages, or other external side effects.
The commit form of git reset changes the commit named by HEAD and records the prior branch tip in ORIG_HEAD. --hard also overwrites the working tree and index; official documentation says it may overwrite untracked files. git-reset
The lab script creates a marked temporary repository, sets a local identity, and disables signing and hooks. In that temporary repository, setup.sh makes C2, runs one reset --hard, removes ORIG_HEAD, then creates and deletes the temporary branch that carried C3. None of those actions modify the host repository.
From the repository root:
lab_path=$(bash labs/git-mental-model/05-recovery/setup.sh)
git -C "$lab_path" show-ref --heads
git -C "$lab_path" reflog show --oneline HEAD
git -C "$lab_path" fsck --no-reflogs --unreachable
Expected meaning:
main points to the baseline commit
HEAD reflog contains "keep recoverable commit"
HEAD reflog contains "discard after expiry"
fsck without reflogs reports both commits as unreachable
Object IDs and reflog ordinal numbers vary. First identify the object ID beside keep recoverable commit and call it <recoverable-id>; do not infer it from its format.
git -C "$lab_path" branch recovered <recoverable-id>
git -C "$lab_path" show --no-patch --oneline recovered
git -C "$lab_path" rev-parse refs/heads/recovered
Expected:
keep recoverable commit
<recoverable-id>
The recovery action only creates a branch reference. It does not rewrite commit contents. You can then inspect changes, decide on merge or rebase, or keep the branch as a recovery point.
Step 2 wrote recovered into the temporary repository. The verifier needs its initial state, where no ref reaches either commit, so remove that lab and create a fresh one first:
bash labs/git-mental-model/05-recovery/cleanup.sh "$lab_path"
lab_path=$(bash labs/git-mental-model/05-recovery/setup.sh)
Then run:
bash labs/git-mental-model/05-recovery/verify.sh "$lab_path"
Expected structure:
ok: reset moved main to <baseline-id> while reflog retained <recoverable-id>
ok: recovered branch now names <recoverable-id>
ok: after explicit reflog expiry and gc --prune=now, <expired-id> no longer resolves
The first assertion confirms that no ref reaches C2 or C3, both remain in the HEAD reflog, and both are reported by git fsck --no-reflogs --unreachable. The second creates recovered. The third intentionally removes reflog protection from C3 and prunes it immediately.
The verifier’s final assertion uses the following two commands to remove recovery clues and immediately prune unprotected objects. Run them only against the $lab_path that this chapter’s setup.sh just created. Do not copy them into an active repository, a shared repository, or a repository with another Git process writing to it. To reproduce the entire sequence manually, run setup.sh again, inspect the reflog first, then run these commands:
git -C "$lab_path" reflog expire --expire=now --expire-unreachable=now --all
git -C "$lab_path" gc --prune=now
Git documentation says that gc --prune=now prunes unnecessary loose objects of every age and raises corruption risk when another process is writing to the repository concurrently. Ordinary git gc uses a default prune threshold of two weeks ago, which gc.pruneExpire can override. git-gc
bash labs/git-mental-model/05-recovery/cleanup.sh "$lab_path"
The cleanup script checks the lab marker and refuses the repository root, the user home directory, and the filesystem root.
Check in this order:
git show-ref --heads --tags
git reflog show --oneline HEAD
git fsck --no-reflogs --unreachable
git cat-file -t <candidate-object-id>
These commands answer, respectively: whether an explicit reference exists, whether HEAD recorded the object, whether it is unreachable when reflogs are removed from the trace heads, and whether the object remains readable. Only when the clues are absent or the object is already pruned can you conclude that Git’s object database has no recovery clue.
reset --hard can overwrite the working tree, index, and untracked files. Inspect git status --short and actual file contents first. If work includes an edit never recorded through git add or git commit, the Git object database has no corresponding recovery evidence; pause the dangerous operation and use editor history, filesystem snapshots, or other backups.
Reflog belongs to the local repository and its entries expire. Important recovery points need an explicit branch or tag and, under the team’s policy, a push, backup, or verifiable release artifact.
Deleting the branch first removes one reference. If other refs, reflogs, or retention mechanisms still name the object and it has not been pruned, it may still be found.
It is a local, expiring record of reference updates. Defaults can also be changed through configuration and commands.
An ID only resolves while the object still exists. With no refs, reflogs, or other retention, GC may already have removed it.
Git works from objects and references it actually recorded. An edit never recorded through git add or git commit needs a recovery source outside Git.
Unreachable means the object cannot be found from the starting points used by that check. The object can still be complete in the object database and recoverable by creating a new branch.
An agent creates a reviewable local commit, then runs git reset --hard HEAD~1. It reports, “the commit is lost.” It did not push and did not say whether other working-tree edits exist.
Complete these checks before selecting a recovery action:
git status --short to find uncommitted edits that need protection.git reflog show --oneline HEAD to find the pre-reset object ID.git cat-file -t <id> and git show --stat <id> to confirm the type and contents.git branch recovery/agent-reset <id> so the recovered object receives an explicit reference again.If the agent’s important edit was never recorded through git add or git commit, reflog and the object graph cannot recover it. State the missing Git evidence and inspect editor history or backups within the authorized scope.