# How to Use Git Reflog to Recover Lost Commits

By the end of this guide you will be able to recover commits that disappeared after a hard reset, a botched rebase, an amend, a deleted branch, or a dropped stash. You will also know how the reflog stores history, how long entries survive, how to verify a recovery, and which mistakes destroy recoverable data permanently.

You need a terminal, Git version 2.x or later installed (run `git, version` to check), and a repository you can experiment in. Every example below uses a repository named `rnaseq-pipeline` with a `main` branch. All commands work identically on Linux, macOS, and Windows PowerShell, with one quoting note covered later.

For anyone doing git for scientists work, this skill matters more than in most fields. A dropped commit can represent a week of pipeline tuning, a curated variant list, or the exact code state that produced a figure in a manuscript. Git keeps almost everything you commit, even after commands that appear to delete it. The reflog is the index that lets you find it.

## What the Reflog Is and Why Commits Get Lost

![Timeline of reflog entry lifetime from the local move through 30 day unreachable expiry, 90 day reachable expiry and garbage collection.](/article-images/illustrations/git-reflog-recover-lost-commits-research-reflog-expiry.webp)

A commit in Git is a snapshot with a unique identifier called a SHA, a 40-character hexadecimal string such as `a1b2c3d...`. Branches are simply movable pointers to a commit. When you run a command that moves a branch pointer backward or rewrites history, Git does not delete the old commits. They stay in your local object database. What changes is which commits are "reachable" from a branch name.

The reflog (short for reference log) is a local, append-only record of every time HEAD or a branch reference moved in your repository. HEAD is the pointer to the commit you currently have checked out. Every checkout, commit, reset, rebase, merge, and pull appends a line to this log. The official Git documentation describes the reflog as a record of where the tips of your references have been, and that description is exactly what you exploit during recovery. Pro Git, the canonical open-source book on Git, calls the reflog one of the best-kept secrets in Git because it lets you unwind almost any local mistake.

The reflog lives in plain-text files inside the hidden `.git` directory:

- `.git/logs/HEAD` records every movement of HEAD
- `.git/logs/refs/heads/main` records every movement of the `main` branch tip
- equivalent files exist for every other branch

You never edit these files by hand. You query them with `git reflog`.

Two properties of the reflog shape everything else in this article:

1. **It is local.** The reflog is never pushed, pulled, or cloned. Each clone has its own. A commit you lost on your machine has no reflog entry on a collaborator's machine, and vice versa. GitHub for researchers workflows that rely on shared repositories still benefit, because the reflog on your own machine covers your own mistakes, which are the most common kind.
2. **It expires.** Entries are cleaned up by garbage collection. By default, entries reachable from a current reference are kept for 90 days and entries pointing at unreachable commits are kept for 30 days. These defaults come from the `gc.reflogExpire` and `gc.reflogExpireUnreachable` configuration settings. Until garbage collection runs, the commits themselves remain in the object database.

This is why recovery is a race against time only in a weak sense. In practice, if you notice a loss within days, the commits are almost always still there.

## Reading the Reflog

![Decision path asking whether the reflog still lists the lost commit, then whether the branch still exists, ending in a recovery command.](/article-images/illustrations/git-reflog-recover-lost-commits-research-recovery-triage.webp)

Before recovering anything, learn the addressing syntax. The general forms are:

```
HEAD@{n}        the value HEAD had n moves ago
HEAD@{<time>}   the value HEAD had at that time, e.g. HEAD@{2.hours.ago}
main@{n}        the value the branch main had n moves ago
@{n}            shorthand for the current branch's reflog
```

`HEAD@{0}` is where you are now. `HEAD@{1}` is where you were one move before. Note the distinction from `HEAD~1`, which means the parent commit of your current commit. `HEAD@{1}` and `HEAD~1` usually differ, and confusing them is a classic source of further damage.

Run the basic command:

```
git reflog
```

Expected output in a working repository:

```
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~
4f5e6d7 HEAD@{1}: commit: add fastq quality filter step
8c9d0e1 HEAD@{2}: commit: trim adapters with cutadapt
b3c4d5e HEAD@{3}: commit (initial): scaffold Snakemake pipeline
```

Read each line right to left. `HEAD@{1}: commit: add fastq quality filter step` means that one move ago, HEAD pointed at a commit whose message was "add fastq quality filter step". The word after the colon tells you what action caused the move: `commit`, `reset`, `checkout`, `rebase`, `merge`, `pull`, `clone`, `amend`, or `rebase (finish)`.

For timestamps, use:

```
git reflog --date=iso
```

Expected output adds a date column:

```
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~ 2026-11-10 14:32:07 -0500
4f5e6d7 HEAD@{1}: commit: add fastq quality filter step 2026-11-10 14:30:51 -0500
```

The `-date=iso` flag prints full dates with time zones, which helps when you are trying to locate work from a specific afternoon.

An alternative view with more detail is:

```
git log -g --oneline
```

This walks the reflog as if it were history and shows commit messages alongside. Add `-date=iso` there too when timestamps matter.

## Step-by-Step Recovery: Hard Reset

This is the most common recovery and the core of any git reflog recover lost commits research step by step procedure. The scenario: you had three commits of pipeline work, ran `git reset, hard HEAD~2`, and two commits vanished from `git log`.

### Step 1. Stop and inspect

Do not run `git gc`, `git prune`, or any cleanup command. Run:

```
git reflog
```

Expected output:

```
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
4f5e6d7 HEAD@{1}: commit: differential expression plots
8c9d0e1 HEAD@{2}: commit: DESeq2 contrast table
b3c4d5e HEAD@{3}: commit (initial): scaffold Snakemake pipeline
```

The entry right below the reset is the commit tip you lost. Here it is `4f5e6d7` at `HEAD@{1}`.

### Step 2. Inspect the candidate commit before moving anything

Never recover blind. Confirm the commit contains what you expect:

```
git show --stat HEAD@{1}
```

Expected output:

```
commit 4f5e6d7...
Author: You <you@lab.edu>
Date:   Mon Nov 10 14:30:51 2026 -0500

    differential expression plots

    results/plots/volcano.Rmd    | 120 ++++++
    results/plots/volcano.png    | Bin 0 -> 88412 bytes
    2 files changed, 120 insertions(+)
```

If `HEAD@{1}` is not the right commit, walk further down with `HEAD@{2}`, `HEAD@{3}`, or use `git log -g, oneline` to see the whole logged history at once.

### Step 3. Recover onto a new branch (the safe option)

```
git checkout -b recovered-de-plots 4f5e6d7
```

Expected output:

```
Switched to a new branch 'recovered-de-plots'
```

This puts the lost commits on a new branch without touching `main`. You can review, diff, and merge at your leisure. This is the recommended default for anyone doing careful work, because it is non-destructive no matter how wrong your guess was.

### Step 4. Or recover in place with reset

If you are certain and want `main` back exactly as it was:

```
git reset --hard HEAD@{1}
```

Expected output:

```
HEAD is now at 4f5e6d7 differential expression plots
```

`git reset, hard <target>` moves the current branch pointer to the target and makes your working tree and index match it. Because the target here is the commit tip from before the bad reset, this exactly undoes the reset.

Verify:

```
git log --oneline -3
git status
```

Expected output:

```
4f5e6d7 (HEAD -> main) differential expression plots
8c9d0e1 DESeq2 contrast table
b3c4d5e scaffold Snakemake pipeline

nothing to commit, working tree clean
```

Two of many practical git reflog recover lost commits research tips fit here. First, always recover to a new branch first and reset only after you have confirmed the content. Second, note the SHA explicitly, for example `git branch backup-de 4f5e6d7`, before experimenting further. SHAs never change, while `HEAD@{n}` positions shift every time you run another command that moves HEAD.

## Step-by-Step Recovery: Deleted Branch

You deleted a feature branch with `git branch -D drop-filter-step` before merging it. Deleting a branch deletes its pointer, not its commits.

```
git reflog
```

Expected output (abridged):

```
a1b2c3d (HEAD -> main) HEAD@{0}: checkout: moving from drop-filter-step to main
7e8f9a0 HEAD@{1}: commit: drop low-count features pre-filter
9a0b1c2 HEAD@{2}: commit: add read-count normalization
...
```

`HEAD@{0}` shows the checkout back to `main`, which is the move you made right before or after deleting the branch. The commit tip of the deleted branch is the last commit you had checked out on it, here `7e8f9a0` at `HEAD@{1}`.

Alternatively, and often faster, query the branch's own reflog directly:

```
git reflog show drop-filter-step
```

If the branch was deleted recently this still works, because the log file `.git/logs/refs/heads/drop-filter-step` is removed on deletion, so in practice the HEAD reflog is the reliable route after a deletion. The per-branch form works while the branch exists.

Recover:

```
git branch drop-filter-step 7e8f9a0
```

Expected output:

```
fatal: a branch named 'drop-filter-step' already exists
```

You would only see that error if the branch still existed. When the branch is truly gone, the command produces no output and simply recreates it. Verify:

```
git log --oneline -2 drop-filter-step
```

Expected output:

```
7e8f9a0 drop low-count features pre-filter
9a0b1c2 add read-count normalization
```

## Step-by-Step Recovery: Botched Rebase or Merge

An interactive rebase that went wrong, a merge with conflicts resolved badly, or a `git pull, rebase` that mangled your commits all leave the pre-rebase state in the reflog. Git also records the pre-rebase tip in a special reference called `ORIG_HEAD`, which is set automatically before operations that rewrite history, including rebase, reset, and merge.

Quick route using `ORIG_HEAD`:

```
git reset --hard ORIG_HEAD
```

This restores the exact state from before the rebase, assuming you have not run another history-rewriting operation since.

General route using the reflog, which works even after intervening commands:

```
git reflog
```

Expected output:

```
c4d5e6f (HEAD -> main) HEAD@{0}: rebase (finish): returning to refs/heads/main
d5e6f7a HEAD@{1}: rebase (pick): add [bootstrapping](/blog/data-analysis/what-is-bootstrapping-resampling-examples) script
e6f7a8b HEAD@{2}: rebase (start): checkout main
f7a8b9c HEAD@{3}: commit: add bootstrapping script
...
```

`rebase (start)` marks where the rewrite began. `HEAD@{3}` here, the commit just before the start entry, is your original pre-rebase tip. Confirm it:

```
git log --oneline HEAD@{3} -3
```

Then recover to a branch:

```
git checkout -b pre-rebase-backup HEAD@{3}
```

The same approach covers a bad merge. Look for the `merge` entry in the reflog and take the position before it.

## Step-by-Step Recovery: Amended Commit

`git commit, amend` replaces the previous commit with a new one. The original commit leaves `git log` but stays in the reflog.

```
git reflog
```

Expected output:

```
8d9e0f1 (HEAD -> main) HEAD@{0}: commit (amend): fix adapter trimming parameters
9e0f1a2 HEAD@{1}: commit: fix adapter trimming parameters
```

`HEAD@{1}` is the pre-amend commit. If the amend lost changes you wanted, restore it:

```
git reset --hard HEAD@{1}
```

Or cherry-pick the old commit onto your current state if you want to keep both:

```
git cherry-pick 9e0f1a2
```

## Step-by-Step Recovery: Dropped Stash

Stash entries live on their own reflog, attached to the reference `refs/stash`. If you ran `git stash drop` or `git stash pop` and lost work:

```
git reflog stash
```

Expected output:

```
stash@{0}: WIP on main: 4f5e6d7 differential expression plots
stash@{1}: WIP on main: 8c9d0e1 DESeq2 contrast table
```

If the entry you need still appears, apply it:

```
git stash apply stash@{1}
```

If it was dropped and no longer appears, find it through the object database:

```
git fsck --unreachable --no-reflogs
```

Expected output (abridged):

```
unreachable commit 1f2a3b4...
unreachable tree 2a3b4c5...
```

Each `unreachable commit` line is a candidate. Identify the right one:

```
git show --stat 1f2a3b4
```

Then recover:

```
git stash apply 1f2a3b4
```

If that fails because the SHA is a merge-style stash commit, use `git stash store` to re-register it first, or apply it directly as a commit with `git cherry-pick` on a clean working tree. The `fsck` route is the fallback for anything the reflog no longer shows, and it works because `fsck` walks the raw object database rather than any log.

## Command Reference

| Command | What it does | When to use it |
|, - |, - |, - |
| `git reflog` | List HEAD movements, newest first | First command after any suspected loss |
| `git reflog, date=iso` | Same list with absolute timestamps | Locate work from a specific time |
| `git log -g, oneline` | Walk the reflog with commit messages | Survey many candidate commits quickly |
| `git reflog show <branch>` | List movements of a branch tip | Recover a branch that still exists |
| `git reflog stash` | List stash reflog entries | Recover dropped or popped stashes |
| `git show, stat HEAD@{n}` | Preview a candidate commit's contents | Confirm before recovering |
| `git checkout -b <name> HEAD@{n}` | Put a lost commit on a new branch | Safest recovery, review before merging |
| `git branch <name> <sha>` | Recreate a deleted branch pointer | Recover deleted branches |
| `git reset, hard HEAD@{n}` | Move current branch to a reflog entry | Undo reset, rebase, amend in place |
| `git reset, hard ORIG_HEAD` | Move to the pre-rewrite tip | Undo the most recent rebase, merge, or reset |
| `git cherry-pick <sha>` | Copy a lost commit onto current branch | Keep current state and add lost work |
| `git fsck, unreachable` | Find commits no reference points at | Reflog is empty or expired |
| `git reflog expire, expire=90.days, all` | Force reflog entry expiration | Only when you intentionally want to purge history |

## Verifying the Recovery

A recovery is not complete until you have checked three things.

1. **The commit content is intact.** Run `git show, stat <sha>` on the recovered tip and confirm the file list and line counts match your memory or notes. For a pipeline, open the changed files and confirm the code is the version you expected.
2. **The history is coherent.** Run `git log, oneline -10` and `git status`. The log should show your expected sequence of commits and the working tree should be clean, or contain only changes you made after the recovery.
3. **The recovered code actually runs.** For research code, this is the decisive test. Re-run the affected step of your pipeline on a small test input and compare output to any saved result. A recovered commit is only useful if the analysis reproduces.

If you recovered onto a new branch and are satisfied, merge or fast-forward your main branch:

```
git checkout main
git merge --ff-only recovered-de-plots
```

The `-ff-only` flag refuses anything but a fast-forward, which prevents an accidental merge commit from sneaking divergence into your main line. If it fails, review the divergence deliberately rather than forcing it.

## Common Mistakes and Limitations

These are the errors that turn a recoverable situation into a permanent loss, along with the exact failure modes you will see.

**Mistake 1: running garbage collection in a panic.** `git gc`, `git prune`, and `git repack -ad` can drop unreachable objects before you have identified them. If you suspect lost work, your first command is `git reflog`, never a cleanup command. Note that `git gc` runs automatically under some conditions in modern Git, which is one reason to investigate losses within days rather than weeks.

**Mistake 2: expecting the reflog to hold uncommitted changes.** The reflog indexes commits, not working-tree edits. If you never committed, staged, or stashed the changes, there is no reflog entry to recover. Editors with local history, IDE checkpointing, and periodic `git commit` on a scratch branch are the actual protections against this. Commit early and commit often is not a stylistic preference, it is what makes reflog recovery possible at all.

**Mistake 3: confusing `HEAD@{n}` with `HEAD~n`.** `HEAD~2` walks parent commits from your current position. `HEAD@{2}` reads where HEAD itself was two moves ago. Recovering with the wrong one silently moves you to an unrelated commit. Always `git show, stat` the candidate before resetting to it.

**Mistake 4: unquoted braces in some shells.** In shells with brace expansion, such as zsh under certain settings, `HEAD@{1}` can be misparsed. If you see `fatal: ambiguous argument 'HEAD@{1}': unknown revision or path not in the working tree` when the entry plainly exists in the reflog, quote it:

```
git reset --hard 'HEAD@{1}'
```

Single quotes work in every POSIX shell, PowerShell, and cmd.

**Mistake 5: assuming the reflog exists everywhere.** A freshly cloned repository has a reflog containing essentially only the clone event, because reflog entries are written by local actions. Shallow clones (created with `-depth`) and bare repositories do not keep reflogs by default. If `git reflog` returns only one or two lines in a repository where you remember many commits, you are likely in a clone, and the reflog you need lives on the machine where the commits were made.

**Mistake 6: relying on the reflog for shared or remote losses.** If a collaborator force-pushed a shared branch, your local reflog contains your own history but the reflog on the machine where the old tip was created is the one that matters. Ask the person whose push rewrote history to check their own reflog. Similarly, commits lost on a remote, for example through a forced push on GitHub, are sometimes recoverable through that platform's support for events on the repository, but that is outside the reflog mechanism.

**Mistake 7: resetting hard over uncommitted changes.** `git reset, hard` destroys uncommitted modifications in the working tree as a side effect. Run `git status` first. If you have live edits you want to keep, stash them (`git stash push -m "wip before recovery"`) before any reset. This is the single most common way people lose data while trying to recover other data.

**Limitation: expiry is real.** Unreachable commits become eligible for deletion after the configured expiry windows pass and garbage collection runs. A recovery attempt months later may fail. When a reflog entry is gone, `git fsck, unreachable` is the last resort, and it too comes up empty after objects are pruned.

**Limitation: the reflog records movements, not intent.** It tells you where HEAD was, in order, with the command that moved it. It does not tell you which commit contained which logical change beyond the commit message. Annotate your commits with meaningful messages so that a future scan of `git log -g, oneline` is legible.

## Reflog Habits for Research Repositories

For git for scientists workflows, where repositories hold pipelines, notebooks, and results alongside manuscripts, a few low-cost habits make the reflog far more useful when you need it.

- **Commit checkpoints on scratch branches.** Before running a risky command, `git checkout -b wip-2026-11-10` and commit. Abandoned scratch branches cost nothing and give every experiment a named reflog anchor.
- **Use descriptive commit messages.** "fix" recovers nothing in a scan. "fix cutadapt -m minimum length for miRNA reads" lets you find the right reflog entry at a glance a month later.
- **Never reset hard on branches others build on.** History rewriting on shared branches shifts recovery from a local reflog problem to a coordination problem across every clone.
- **Push early.** A commit on GitHub is protected against local disasters that the reflog cannot cover, such as disk failure. The reflog covers your own history-rewriting mistakes. Pushing covers everything else.
- **Check the reflog after any surprising command output.** A quick `git reflog -5` after a rebase, merge, or pull tells you immediately whether history moved the way you thought it did, while the fix is still one step away.

## Frequently Asked Questions

### Does git reflog work after git gc?

Sometimes. Garbage collection expires reflog entries on a schedule, with defaults of 90 days for reachable entries and 30 days for unreachable ones. After `gc` runs, recent entries usually survive, but commits pruned from the object database are gone permanently. If `git reflog` shows nothing useful, try `git fsck, unreachable` immediately, before any further cleanup runs.

### How long do reflog entries last?

By default, entries reachable from your current references expire after 90 days and entries for unreachable commits expire after 30 days, controlled by the `gc.reflogExpire` and `gc.reflogExpireUnreachable` settings. Nothing is deleted on a timer by itself, only when garbage collection runs.

### Can I recover uncommitted changes with the reflog?

No. The reflog tracks movements of references between commits, so only committed (or stashed) work has entries. Uncommitted working-tree edits need protection from your editor's local history, IDE checkpoints, or a habit of committing to a scratch branch.

### Is the reflog shared with teammates or pushed to GitHub?

No. The reflog is strictly local to each clone and is never transmitted by push, pull, fetch, or clone. If a lost commit exists on a collaborator's machine, that collaborator's reflog is the one to search.

### How do I recover a deleted branch?

Find the branch's last commit tip in `git reflog`, typically the entry just before the `checkout: moving from <branch> to ...` line, then run `git branch <branch-name> <sha>`. This recreates the branch pointer exactly where it was, with all of its history.

### What is the difference between ORIG_HEAD and HEAD@{1}?

`ORIG_HEAD` is a single reference Git sets to the tip before the most recent history-rewriting operation, such as a rebase, merge, or reset. `HEAD@{1}` is the previous entry in the HEAD reflog regardless of what caused it. They often point to the same commit right after a rebase, but `ORIG_HEAD` is overwritten by the next rewriting operation while the reflog keeps a long record.

### Can the reflog help after a force push on a shared branch?

Yes, if the old commits were created on your machine. Your local reflog records the pre-push history of the branch, and you can restore it with `git branch <name> HEAD@{n}` or `git reset, hard HEAD@{n}`. If the force push came from someone else, the commits you lost were never on your machine, so their reflog on the machine that created them is where recovery starts.

### What if the reflog is empty or missing the commit I need?

Use `git fsck, unreachable, no-reflogs` to list commits that no reference points at, inspect each candidate with `git show, stat <sha>`, and recover with `git branch <name> <sha>` or `git cherry-pick <sha>`. This walks the raw object database and finds commits the reflog no longer mentions, as long as garbage collection has not pruned them.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does git reflog work after git gc?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Sometimes. Garbage collection expires reflog entries on a schedule, with defaults of 90 days for reachable entries and 30 days for unreachable ones. After gc runs, recent entries usually survive, but commits pruned from the object database are gone permanently. If git reflog shows nothing useful, try git fsck, unreachable immediately, before any further cleanup runs."
      }
    },
    {
      "@type": "Question",
      "name": "How long do reflog entries last?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "By default, entries reachable from your current references expire after 90 days and entries for unreachable commits expire after 30 days, controlled by the gc.reflogExpire and gc.reflogExpireUnreachable settings. Nothing is deleted on a timer by itself, only when garbage collection runs."
      }
    },
    {
      "@type": "Question",
      "name": "Can I recover uncommitted changes with the reflog?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. The reflog tracks movements of references between commits, so only committed or stashed work has entries. Uncommitted working-tree edits need protection from your editor's local history, IDE checkpoints, or a habit of committing to a scratch branch."
      }
    },
    {
      "@type": "Question",
      "name": "Is the reflog shared with teammates or pushed to GitHub?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. The reflog is strictly local to each clone and is never transmitted by push, pull, fetch, or clone. If a lost commit exists on a collaborator's machine, that collaborator's reflog is the one to search."
      }
    },
    {
      "@type": "Question",
      "name": "How do I recover a deleted branch?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Find the branch's last commit tip in git reflog, typically the entry just before the checkout line that moved you away from the branch, then run git branch <branch-name> <sha>. This recreates the branch pointer exactly where it was, with all of its history."
      }
    },
    {
      "@type": "Question",
      "name": "What is the difference between ORIG_HEAD and HEAD@{1}?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "ORIG_HEAD is a single reference Git sets to the tip before the most recent history-rewriting operation, such as a rebase, merge, or reset. HEAD@{1} is the previous entry in the HEAD reflog regardless of what caused it. They often point to the same commit right after a rebase, but ORIG_HEAD is overwritten by the next rewriting operation while the reflog keeps a long record."
      }
    },
    {
      "@type": "Question",
      "name": "Can the reflog help after a force push on a shared branch?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes, if the old commits were created on your machine. Your local reflog records the pre-push history of the branch, and you can restore it with git branch <name> HEAD@{n} or git reset, hard HEAD@{n}. If the force push came from someone else, the commits you lost were never on your machine, so their reflog on the machine that created them is where recovery starts."
      }
    },
    {
      "@type": "Question",
      "name": "What if the reflog is empty or missing the commit I need?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Use git fsck, unreachable, no-reflogs to list commits that no reference points at, inspect each candidate with git show, stat, and recover with git branch <name> <sha> or git cherry-pick <sha>. This walks the raw object database and finds commits the reflog no longer mentions, as long as garbage collection has not pruned them."
      }
    }
  ]
}
</script>