Recover Lost Git Work with git reflog

A branch you reset or replaced by mistake is not gone, git reflog is the local record that gets it back. This guide reads the HEAD reflog, verifies a candidate commit before touching anything, and recovers it safely onto a new branch. The examples use Git 2.43.0, matching the installed git-reflog(1) manual. Allow about fifteen minutes.

1. Check the installed Git and repository

Run these from the repository where the branch or commit seems to have vanished:

$ git --version
git version 2.43.0
$ git rev-parse --show-toplevel
/path/to/your/repository

If the second command says the directory is not a Git repository, change into it first. Reflogs are local records: they are not stored on a remote, and a fresh clone will not carry the reflog history from the original checkout.

Checkpoint: confirm the version and repository root before interpreting any reflog entry. Git versions add subcommands and change help text over time; the commands here are present in Git 2.43.0.

2. Read the HEAD reflog first

With no subcommand, git reflog shows the reflog for HEAD. Ask for ISO dates and keep the output short while you scan it:

$ git reflog --date=iso -n 12
f2a91c7 HEAD@{2026-09-24 10:14:02 +0100}: commit: update parser
91b7e20 HEAD@{2026-09-24 10:09:44 +0100}: checkout: moving from feature/parser to main
6c3d8aa HEAD@{2026-09-24 10:07:11 +0100}: commit: save parser experiment

The object ID at the start of each line is what HEAD held at that point. The selector HEAD@{2} means two reflog moves ago, not necessarily two calendar days ago. The message after the colon names the operation that moved the reference.

Do not treat the newest line as proof your work is safe. A reflog entry names an object in this local repository, it does not tell you whether that object is the branch tip you actually meant to recover, inspect the candidate before changing anything.

3. Inspect a candidate commit without moving a ref

Replace COMMIT_ID with the object ID you found. These commands only inspect it:

$ git show --stat --oneline --decorate COMMIT_ID
6c3d8aa (HEAD@{2}) commit: save parser experiment
 parser.go | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
$ git show --format=fuller --no-ext-diff COMMIT_ID -- path/to/file

Use the first command to check subject, branch decorations and changed files, and the second to inspect the actual patch for a file. If the candidate is not yours, go back to the reflog and try another entry, it is a timeline of reference values, not a ranked list of likely answers.

You can also resolve a relative selector directly:

$ git show --stat --oneline 'HEAD@{2}'
$ git rev-parse 'HEAD@{2026-09-24 10:07:11}'

Quote selectors containing braces or spaces so the shell does not reinterpret them. A date selector is evaluated against this repository's local reflog timestamps and can fail if the time falls outside the recorded range.

4. Confirm the candidate is usable

Before creating a branch, verify Git can resolve the object and that it has the expected commit shape:

$ git rev-parse --verify 'COMMIT_ID^{commit}'
6c3d8aa4f5c1d2e3f4a5b6c7d8e9f00112233445
$ git merge-base --is-ancestor 'COMMIT_ID' HEAD
$ printf 'ancestor check status: %s\n' "$?"
ancestor check status: 1

To check whether the candidate is already reachable from any local branch:

$ git branch --contains COMMIT_ID
  feature/parser

Branch names are repository-specific. An empty result does not make the commit unusable, it only means no local branch currently contains it.

5. Recover it on a new branch

Once the commit is verified, create a clearly named branch pointing at it:

$ git switch -c recover-parser-work COMMIT_ID
Switched to a new branch 'recover-parser-work'
$ git log -1 --oneline --decorate
6c3d8aa (HEAD -> recover-parser-work) commit: save parser experiment

This changes the current checkout and creates a local branch, but it does not delete the original branch or rewrite shared history. The new branch protects the commit from getting harder to find while you decide whether to merge, cherry-pick or copy the work elsewhere.

Checkpoint: confirm HEAD now points to the intended commit and the working tree holds what you expected:

$ git status --short --branch
## recover-parser-work
$ git diff 'COMMIT_ID^' HEAD --stat
 parser.go | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

Wrong branch? Undo this particular checkout safely by switching to a known branch, then delete only the newly created branch after checking its name:

$ git switch main
$ git branch -D recover-parser-work

Warning: git branch -D force-deletes the branch reference. It does not immediately erase the commit, but it removes the convenient name you just made. Do not run it until you have recorded the correct object ID or are certain the branch is disposable.

6. Check whether the reflog still has the entry

After recovery, inspect both the branch and HEAD reflogs. A branch reflog is useful because it follows that branch name, the HEAD reflog also records checkouts:

$ git reflog show --date=iso recover-parser-work -n 3
6c3d8aa recover-parser-work@{2026-09-24 10:20:00 +0100}: branch: Created from COMMIT_ID
$ git reflog exists recover-parser-work
$ printf 'reflog check status: %s\n' "$?"
reflog check status: 0

git reflog exists returns zero when the named ref has a reflog and non-zero when it does not. A missing reflog is not the same as a missing commit, the object may still be reachable from a branch, tag or another reference.

7. Leave expiry and deletion alone until the recovery is settled

Git normally manages reflog expiry through garbage collection. In this Git version, reachable entries default to 90 days and unreachable entries to 30 days, unless configuration changes those values. That is why an old, abandoned commit can eventually become difficult or impossible to recover from this repository.

Warning: do not use git reflog expire --expire=all or git reflog delete as routine cleanup while investigating a loss, they remove evidence. To test an expiry policy, use the documented dry-run form first:

$ git reflog expire --dry-run --verbose --expire=all --expire-unreachable=all HEAD

Dry-run output shows entries that would be pruned and changes nothing. The pruning options affect your recovery window, so make a backup or a recovery branch and record the repository state before any real expiry operation. Reflog maintenance normally belongs to git gc, not an ad hoc repair command.

Done means