Git Course 0%

Recover files, branches, commits; detached HEAD

Advanced ≈ 14 min

What you will learn

  • How to restore a file from any commit, including one that deleted it
  • How to find and rescue commits nothing points at
  • What detached HEAD means and how to leave it safely

After this lesson you can

  • I can recover work from a repository that looks like it has lost it

Why this matters

Four things go missing in practice: a file, a branch, a commit, and a stash. Each has a reliable recovery, and none of them requires a colleague's help. Detached HEAD is in this lesson because it is the state people land in while trying to recover something, and the message that announces it frightens people more than it should.

A deleted file

Deleting a file is an ordinary change, recorded in an ordinary commit. Finding the deletion is the first step:

Terminal
$ git log --oneline -- docs/faq.md
d348c93 docs: remove the FAQ
b81702c docs: add getting started guide and FAQ

The top line is the commit that removed it. Restore the file from the commit before that one:

Terminal
$ git restore --source=HEAD~1 docs/faq.md
$ git status --short
?? docs/faq.md

The file is back on disk. The ?? is worth understanding: as far as the current commit is concerned this file does not exist, so the restored copy is untracked. git add and commit it, and it is properly back.

Any commit works as a source: git restore --source=b81702c docs/faq.md takes that version specifically. The older spelling, git checkout b81702c -- docs/faq.md, does the same thing and appears in a lot of documentation.

A deleted branch

Deleting a branch removes a name, and Git prints the hash it pointed at:

Output
Deleted branch docs/12-windows-install-steps (was 95a0014).
Terminal
$ git switch -c docs/12-windows-install-steps 95a0014

If the hash has scrolled away, the reflog has it (lesson 12.1): look for the checkout: moving from <branch> lines, or the commits made on it.

If the branch was ever pushed, it is simpler still: git fetch and git switch <branch> recreates it from the remote, because deleting your local branch does nothing to the server's.

A commit nothing points at

When no branch and no tag reaches a commit, it is unreachable. It still exists, and two commands find it:

Terminal
$ git fsck --lost-found
dangling commit 95a001407cde528f92b4c796b2e359e301e2b863
Terminal
$ git fsck --unreachable
unreachable commit 8130ab76127fc3e742ad5ba263e28cae1891ba45
unreachable commit d0698b2c104b4154c9d0b01add9f9106554137f6

Inspect a candidate with git show <hash> or git log --oneline -3 <hash>, and when you find the right one, give it a name:

Terminal
$ git branch rescue/lost-work 95a0014
Dropped refs/stash@{0} (8130ab76127fc3e742ad5ba263e28cae1891ba45)

A branch name makes it reachable again, which also protects it from garbage collection.

A dropped stash

Dropping a stash prints its hash, which is the whole recovery:

Terminal
$ git stash apply 8130ab76127fc3e742ad5ba263e28cae1891ba45
On branch main
Changes not staged for commit:
	modified:   docs/getting-started.md

If the hash is gone, git fsck --unreachable lists it among the others, and a dropped stash is recognisable by git show <hash> showing a commit whose message begins WIP on.

Detached HEAD

Normally HEAD points at a branch, and the branch points at a commit. Checking out a commit directly points HEAD straight at it, with no branch in between. Git says so at length:

Terminal
$ git checkout 28593d2
Note: switching to '28593d2'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

HEAD is now at 28593d2 feat: add the trail list command

That message is a description, not an error. git status confirms the state in one line:

Output
HEAD detached at 28593d2
nothing to commit, working tree clean

How you get there: checking out a commit hash or a tag, git bisect, and stopping in the middle of a rebase. It is normal and often deliberate: it is how you look at an old state of the project, or build a release from a tag.

Leaving safely

If you committed nothing, git switch - returns you to where you were and nothing is lost.

If you committed something, those commits belong to no branch, and Git warns you clearly on the way out:

Terminal
$ git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  de00aac experiment: try an extra trail

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> de00aac

Switched to branch 'main'

Git even gives you the command. If you have already left, the same command works afterwards:

Terminal
$ git branch experiment/extra-trail de00aac

The recovery table

Missing First command Then
A file, deleted in a commit git log --oneline -- <path> git restore --source=<commit>~1 <path>
A file, edited but not committed Your editor's local history Nothing in Git can help
A branch git reflog or the delete message git switch -c <name> <hash>
A branch that was pushed git fetch git switch <name>
A commit git reflog, then git fsck --unreachable git branch rescue/<name> <hash>
A stash The Dropped … line, or git fsck --unreachable git stash apply <hash>
Commits left in detached HEAD The warning Git printed git branch <name> <hash>

How to do it

The four commands that cover this lesson:

Terminal
$ git reflog
$ git restore --source=<commit> <path>
$ git branch <name> <hash>
$ git fsck --unreachable

The pattern is always the same: find the hash, then give it a name.

Common mistakes

  • Assuming a deleted branch deleted the commits. It removed a name.
  • Restoring a deleted file and forgetting to git add it. It comes back untracked.
  • Reading "detached HEAD" as an error. It is a state, and Git tells you how to leave it.
  • Leaving detached HEAD without reading the warning, which contains the exact command to keep your commits.
  • Running git gc --prune=now while trying to recover something, which removes exactly the unreachable objects you are looking for.
  • Waiting weeks. Unreachable commits expire; recover today.

Try it yourself

Goal: lose four things on purpose and get them all back.

  1. Delete docs/faq.md, commit the deletion, then restore the file from the previous commit and commit it back.
  2. Create a branch, commit on it, switch away, delete it with git branch -D, and recreate it from the hash Git printed.
  3. Run git checkout <an old hash>, commit a small change, then git switch main and read the warning. Rescue the commit with git branch.
  4. Stash something, drop the stash, and recover it with git stash apply <hash from the Dropped line>.
  5. Finish with git status clean and every rescue accounted for.

Expected result: four recoveries, each from a different starting point, none of which needed anyone else.

Show solution

Step 3 is the one people fear and the easiest of the four: Git prints the hash and the exact command as you leave. The general shape of every recovery in this lesson is identical, which is worth internalising: find the hash, give it a name.

Check yourself

1. A file was deleted three commits ago. How do you get it back?
2. What is detached HEAD?
3. You made a commit in detached HEAD and switched back to main. What keeps that commit?

Key terms

Detached HEAD Reflog Stash HEAD