Recover files, branches, commits; detached HEAD
Advanced
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:
$ git log --oneline -- docs/faq.mdd348c93 docs: remove the FAQ
b81702c docs: add getting started guide and FAQThe top line is the commit that removed it. Restore the file from the commit before that one:
$ git restore --source=HEAD~1 docs/faq.md
$ git status --short?? docs/faq.mdThe 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:
Deleted branch docs/12-windows-install-steps (was 95a0014).$ git switch -c docs/12-windows-install-steps 95a0014If 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:
$ git fsck --lost-founddangling commit 95a001407cde528f92b4c796b2e359e301e2b863$ git fsck --unreachableunreachable commit 8130ab76127fc3e742ad5ba263e28cae1891ba45
unreachable commit d0698b2c104b4154c9d0b01add9f9106554137f6Inspect a candidate with git show <hash> or git log --oneline -3 <hash>, and when you find the right one, give it a name:
$ git branch rescue/lost-work 95a0014Dropped 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:
$ git stash apply 8130ab76127fc3e742ad5ba263e28cae1891ba45On branch main
Changes not staged for commit:
modified: docs/getting-started.mdIf 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:
$ git checkout 28593d2Note: 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 commandThat message is a description, not an error. git status confirms the state in one line:
HEAD detached at 28593d2
nothing to commit, working tree cleanHow 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:
$ git switch mainWarning: 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:
$ git branch experiment/extra-trail de00aacThe 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:
$ git reflog
$ git restore --source=<commit> <path>
$ git branch <name> <hash>
$ git fsck --unreachableThe pattern is always the same: find the hash, then give it a name.
The Timeline view on a file shows its commits and lets you restore an old version. For deleted branches and unreachable commits there is no interface; use the terminal. VS Code's own Local History in the Timeline can rescue uncommitted edits Git never saw.
Git → Show History on a file restores old versions. For lost commits, Git → Recent Branches and the Log's search cover most cases, and Local History on a file or folder recovers uncommitted work.
If the branch or commit was ever pushed, the platform still has it: the branch list, the commit's page by hash, and the merge request that contained it. git fetch brings any of them back. Deleting a branch on GitLab does not delete its commits while a merge request still references them.
The same. A pushed commit remains reachable by its URL and by git fetch origin <hash> in many cases, and a merged pull request keeps its commits listed even after the branch is deleted.
Common mistakes
- Assuming a deleted branch deleted the commits. It removed a name.
- Restoring a deleted file and forgetting to
git addit. 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=nowwhile 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.
- Delete
docs/faq.md, commit the deletion, then restore the file from the previous commit and commit it back. - Create a branch, commit on it, switch away, delete it with
git branch -D, and recreate it from the hash Git printed. - Run
git checkout <an old hash>, commit a small change, thengit switch mainand read the warning. Rescue the commit withgit branch. - Stash something, drop the stash, and recover it with
git stash apply <hash from the Dropped line>. - Finish with
git statusclean 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.