你执行 git checkout v2.3.1 去看一个旧 tag,随手做了几个实验性提交,然后 git checkout main 回主分支——这时 Git 打印了一行 Warning: you are leaving N commits behind, not connected to any of your branches。刚才的提交既不在 main 上,也不在任何分支上,git log 里也找不到了。它们是悬挂提交(dangling commits):对象数据库里真实存在、但没有任何分支引用指向它们的 commit。注意:现在还什么都没被删除——commit 仍然存在,reflog 仍然指向它——所以只要尽快动手,找回几乎总能成功。
最快修复:Git 刚才那行警告里就带着 SHA。执行 git branch rescue/detached-work <SHA> 把这些提交钉到一个真正的分支上,再从 rescue/detached-work 合并或 cherry-pick 即可。如果你已经清屏了,SHA 还在 git reflog 里。下面是完整步骤。
你到底还有多少时间?
这一点最容易被搞混,因为 Git 有两个不同的过期默认值(截至 2026 年 6 月,Git 2.x):
| reflog 条目类型 | 配置项 | 默认值 |
|---|---|---|
| 仍可从某个分支顶端到达 | gc.reflogExpire | 90 天 |
| 不可达(你放弃掉的 detached-HEAD 提交) | gc.reflogExpireUnreachable | 30 天 |
你丢下的 detached-HEAD 提交属于不可达,所以适用的是 30 天 这个数字,而不是 90 天。过了这个窗口,下一次 git gc(或任何 git gc --auto,它可能在常规命令后悄悄触发)就可能把它们永久清理掉。请把 30 天当成截止线,今天就把它找回来。
常见原因
按命中率从高到低排列。
1. checkout 某个 tag 或 SHA 后直接提交
git checkout v2.3.1 或 git checkout abc1234 都会让 HEAD 进入 detached 状态。此时没有活动分支,你做的任何提交在切走的那一刻就成了孤儿。
怎么判断:切走之前先 git status——第一行是 HEAD detached at v2.3.1,而不是 On branch feature。
2. git bisect 结束后仍停在 detached HEAD
git bisect start / git bisect good / git bisect bad 全程都在 detached HEAD 下运行。如果你在 bisect 期间提交了一个验证用的临时改动,又忘了运行 git bisect reset,这些提交就是悬挂的。
怎么判断:git bisect log 仍显示有活动的 bisect 会话,或 .git/BISECT_LOG 文件存在。
3. 直接 checkout 远程追踪分支
git checkout origin/feature 检出的是远程追踪引用,而不是本地分支,因此进入 detached HEAD。新提交不会被推到任何地方,也没有本地分支引用。
怎么判断:git status 显示 HEAD detached at origin/feature 而不是分支名。(应改用 git checkout feature,不带 origin/ 前缀,Git 会创建并追踪一个本地分支。)
4. CI/CD 流水线检出了具体 SHA
自动化流水线(GitHub Actions、Jenkins、GitLab CI)为了可复现,经常克隆后 git checkout <sha>。尤其是 GitHub 的 actions/checkout,会把 runner 停在一个 detached 的 SHA 上;在 pull_request 事件下,它检出的还是一个临时的合并 commit。如果某个步骤在这个工作区里提交,产生的就是永远到不了分支的悬挂提交。
怎么判断:cat .git/HEAD 显示的是裸 SHA,而不是 ref: refs/heads/<branch>;CI 日志里出现 HEAD is now at <sha>,后面没有分支名。
5. checkout 之后 git stash pop 进入了意料之外的状态
一些较老的工作流会:stash、checkout 一个 SHA、pop 出 stash、改文件、提交——全程都在 detached HEAD,却没意识到 stash pop 并不会把 HEAD 重新挂回分支。
怎么判断:git reflog | head -5 显示 stash@{0}: pop 紧接着一连串 commit,中间没有任何切换分支的条目。
我属于哪种情况?
| 当前症状 | 你的状态 | 跳到 |
|---|---|---|
还在会话里,git status 显示 HEAD detached at ... | 尚未切走 | Step 2,最简单的情况 |
已切走,commit 从 git log 消失 | 已悬挂但还在 reflog | Step 1 再 Step 2 |
git reflog 里找不到,且已经跑过 git gc | reflog 已被清理 | FAQ:git fsck --lost-found |
最短修复路径
Step 1:找到 detached-HEAD 提交的 SHA
如果你还没切走,git log --oneline 会把它们显示在顶部。记下最上面那个 SHA,直接跳到 Step 2。
如果已经切走,reflog 仍然记录了 HEAD 走过的每一个位置:
git reflog --date=relative | head -20
找到 detached HEAD 会话下那行 commit: <你的提交信息>,左列的短 SHA 就是你最近一个 detached 提交。你还会看到一行 checkout: moving from <sha> to main——它离开的那个 <sha> 就是你丢失工作的顶端。
Step 2:立刻创建分支指向这些提交
# 如果你还在 detached HEAD 状态
git branch rescue/detached-work HEAD
# 如果你已经切到了别的分支
git branch rescue/detached-work <reflog 里的 SHA>
这里特意用 git branch(而不是 git switch / git checkout):它只创建救援引用、不动你的工作区,这样在你理清状况期间不会再丢失任何状态。
Step 3:验证分支里确实有你的工作
git log --oneline rescue/detached-work | head -10
git diff main..rescue/detached-work -- src/
确认提交和改动都在,再继续。
Step 4:选择把提交并回去的方式
方式 A——合并进目标分支(保留原始历史):
git checkout main
git merge rescue/detached-work --no-ff -m "merge: restore detached-HEAD work"
方式 B——逐个 cherry-pick(只要其中一部分时):
git checkout main
git cherry-pick rescue/detached-work~2..rescue/detached-work
方式 C——先 rebase 到目标分支,再 fast-forward(线性历史):
git checkout rescue/detached-work
git rebase main
git checkout main
git merge rescue/detached-work --ff-only
Step 5:确认修复成功,再推送并清理
# 救回的提交现在应当可从 main 到达
git branch --contains <reflog 里的 SHA> # 应当列出 main
git log --oneline -5 main # 你的提交在最上面
git push origin main
git branch -d rescue/detached-work # 安全:未合并时 -d 会拒绝删除
git branch --contains <SHA> 能列出 main,就是「确实修好了」的权威判据:它证明该提交现在可从一个真实分支到达,不再是垃圾回收的目标。
预防建议
- 用
git switch代替git checkout。git switch在不加--detach时拒绝进入 detached 状态,你就不会再误打误撞进去。查看旧 tag 用git switch --detach v2.3.1;要在它上面提交,用git switch -c investigate/v2.3.1 v2.3.1创建一个真正的分支。 - 提交前永远先
git status,确认第一行是分支名,而不是HEAD detached at ...。 git bisect结束后务必git bisect reset回到原分支,再做任何提交。- 让 shell 提示符显示当前分支(以及 detached HEAD 状态)。oh-my-zsh、
starship、bash-git-prompt 默认都会把 detached HEAD 单独标出来。 - CI 里检出后立刻挂上分支:
git switch -c ci/run-$BUILD_ID,这样 job 里的任何提交都落在命名引用上。用actions/checkout时,这是避免 CI 悬挂提交最省事的办法。 - 如果你和 AI 编码 agent 协作,在
CLAUDE.md/AGENTS.md里加一行:agent 提交前必须git status并确认 HEAD 在某个分支上。 - 给自己留更宽的安全网:
git config --global gc.reflogExpireUnreachable 90,把不可达提交的保留期从 30 天延长到 90 天。
常见问答 (FAQ)
Q:悬挂提交多久会被垃圾回收删除?
A:默认情况下,不可达提交在 30 天后离开 reflog(gc.reflogExpireUnreachable = 30),此后 git gc 就能把它们清理掉。你可能见过的 90 天指的是 gc.reflogExpire,那只管仍可从某个分支到达的条目——并不适用于被你放弃的 detached-HEAD 工作。请在 30 天内找回;越早越保险,因为 git gc --auto 可能自行触发。
Q:我已经跑过 git gc,reflog 条目也没了,工作是不是彻底丢了?
A:未必。运行 git fsck --lost-found(或 git fsck --full --no-reflogs --unreachable)。Git 会把悬挂的 commit、tree、blob 对象写到 .git/lost-found/commit/。用 git show <sha> 逐个查看找到你的工作,再对正确的那个执行 git branch rescue <sha>。
Q:能直接在 detached HEAD 上 push 吗?
A:能,用显式 refspec:git push origin HEAD:refs/heads/new-branch-name。这会在远端创建 new-branch-name,指向你当前的 detached SHA——当你来不及先建本地分支时,这是一行救命的逃生口。
Q:detached HEAD 有时是故意的吗?
A:是。git bisect、只读地查看历史状态、submodule 检出、以及一些 CI 可复现流程,都会有意使用 detached HEAD。只有当你不先建分支就提交新工作时,它才是问题。
Q:怎么彻底防止在 detached HEAD 下提交?
A:用 pre-commit 钩子拦住它。在 .git/hooks/pre-commit 里加:git symbolic-ref -q HEAD >/dev/null || { echo "Refusing to commit in detached HEAD"; exit 1; }。HEAD 处于 detached 时 git symbolic-ref HEAD 会失败,于是提交被中止。
相关阅读
- Rebase 之后 commit 消失了
- 解决冲突后 cherry-pick 变成空提交
- Bisect 卡在被 skip 的 commit 上
- Worktree 在分支删除后变成幽灵
- AI 帮你回滚代码改动
- 找回 Git 历史里的旧版本文件
标签: #git #version-control #排查