要恢复丢失的提交,运行 git reflog,复制你想要的那个提交的哈希值,然后用 git checkout -b recovered <hash> 将其恢复。
你本想输入 HEAD~1,结果打成了 HEAD~2,然后就看着三个小时的工作成果从 git log 里消失了。从按下回车到想起 reflog 存在的那几秒钟,是使用 Git 时最难熬的时刻。当你执行 git reset --hard、把 rebase 搞砸,或者删掉一个分支时,你的提交几乎从不会真正被销毁:它们只是变成了孤立对象(orphaned),其哈希值仍然记录在你的 reflog 中。本文将为引发这种恐慌的三种典型场景提供快速恢复方案,然后介绍一些值得了解的硬性限制,好让你不会第二次丢掉这些工作成果。下面的每条命令都可以直接复制粘贴使用。
核心要点
git reflog记录了本地仓库中HEAD的每一次移动(commit、checkout、reset、rebase、merge),所以一个”丢失”的提交通常只需一条git reset --hard <hash>就能回来。- 最安全的恢复方式是
git checkout -b recovered <hash>:在一个新分支上重建该提交,先检查确认,再去动你真正的分支。 - Reflog 无法恢复未提交的工作目录改动,因为 Git 从一开始就没有把它们记录到任何 ref 中。
- 默认情况下,Git 会保留可达(reachable)的 reflog 条目 90 天,不可达(unreachable)的条目 30 天,所以要尽快恢复,并且在拿回提交之前不要运行
git gc。 - Reflog 严格属于本地,永远不会被推送,所以它能挽救你自己丢失的工作,但救不了队友尚未推送的提交。
Git reflog 是如何工作的?
Git 的 reflog 是一份本地日志,记录了你的分支顶端(branch tip)以及其他 ref 曾经指向过的每一个位置。每一次 commit、checkout、reset、rebase 和 merge 都会移动 HEAD 并被记录下来。因此,当一个提交变成”丢失”状态时(意味着不再有任何分支或标签指向它),它只是被孤立了,而不是被删除了,它的哈希值仍然留在 reflog 中,等待被重新关联。
不带任何参数运行该命令,即可查看 HEAD 的历史:
git reflog
b58145c HEAD@{0}: reset: moving to HEAD~2
dd7c37e HEAD@{1}: commit: one more commit
b5b3286 HEAD@{2}: checkout: moving from main to feature
b58145c HEAD@{3}: commit: very important commit
每一行的读法是:短哈希值、HEAD@{n} 索引(该条目位于向后数第几次移动)以及该操作的描述。在上面的输出中,HEAD@{0} 是刚刚发生的破坏性 reset,而 HEAD@{1}(dd7c37e)就是它移离的那个提交,也就是你想要找回的那个。在底层,git reflog show 执行的与 git log -g --abbrev-commit --pretty=oneline 完全相同,并且它接受任意 ref,所以 git reflog show main 会给出某一个分支的历史。
Discover how at OpenReplay.com.
从误操作的 hard reset 中恢复
git reset --hard 会移动你的分支指针,但旧提交在对象库(object store)中依然完好无损。在 reflog 中找到它,然后重新指向它即可。reset 之前的那个提交就是你丢掉的那个,通常是 HEAD@{1}。
git reflog
# HEAD@{0}: reset: moving to HEAD~2
# HEAD@{1}: commit: one more commit <-- this hash
git checkout -b recovered dd7c37e
最安全的恢复方式是先检查再覆盖:运行 git checkout -b recovered <hash> 在新分支上重建该提交,用 git log 验证无误,然后再决定是否移动你真正的分支。一旦确认,你可以直接把分支跳过去:
git reset --hard dd7c37e # or: git reset --hard HEAD@{1}
二次风险警告: git reset --hard 会丢弃工作树中当前所有未提交的改动。如果你的工作树是脏的(dirty),先执行 git stash,或者走新建分支那条路线。否则这条恢复命令会造成第二次丢失。在刚执行完 reset 之后,你也可以使用 git reset --hard ORIG_HEAD,因为 git reset 在移动之前会把分支之前的顶端记录到 ORIG_HEAD 中。
撤销一次糟糕的 rebase
rebase 会重写历史,而一次冲突解决失误可能会悄无声息地丢掉提交。Reflog 保存着 rebase 之前的分支顶端。在一次糟糕的 rebase 之后,git reset --hard ORIG_HEAD 能把你的分支拉回到起点。但 ORIG_HEAD 会被下一次 reset、rebase 或 merge 覆盖,所以如果此后你又运行过这类命令,就改为在 git reflog 中查找 rebase 前的分支顶端。
git reflog
# HEAD@{0}: rebase (finish): returning to refs/heads/feature
# HEAD@{1}: rebase (pick): one more commit
# HEAD@{2}: rebase (start): checkout main
# HEAD@{3}: checkout: moving from main to feature <-- pre-rebase tip
git reset --hard HEAD@{3}
找到 rebase (start) 这一条;紧挨在它之前的那一行就是 rebase 之前你的分支所处的状态。如果你只想找回 rebase 丢掉的特定几个提交,而不是撤销整个操作,那就从 reflog 中取出它们的哈希值并重放:
git cherry-pick <hash>
恢复被删除的分支
删除一个分支移除的是 ref,而不是提交。它的最后一个提交仍然会出现在 HEAD 的 reflog 中(来自你上一次检出它的时候),所以找到那个哈希值,然后在它上面重建分支即可。
git reflog
# HEAD@{0}: checkout: moving from example-branch to main
# HEAD@{1}: commit: very important commit <-- last commit on deleted branch
git checkout -b example-branch b5b3286 # or HEAD@{1}
git checkout -b <branch> <hash> 会重建一个指向已恢复提交的分支,且其历史完好无损。如果你只记得提交信息的一部分而不记得哈希值,git log --oneline -g --grep='<fragment>' 可以在 reflog 中搜索它。(git switch -c 是 checkout -b 的现代等价写法;两者都可用,且 checkout 并未被废弃。)
Git reflog 无法恢复什么?
Reflog 能够恢复任何移动过 HEAD 或分支顶端的东西(提交、reset、rebase、被删除的分支),但它无法恢复未提交的工作目录改动,因为 Git 从一开始就没有把它们记录到任何 ref 中。如果一处改动从未被提交,就不存在对应的 ref 更新供 reflog 指回,因此在 reflog 里搜索它是不会有任何结果的。
| Reflog 能恢复 | Reflog 不能恢复 |
|---|---|
被 reset --hard 孤立的提交 | 工作树中未提交的编辑 |
| 被糟糕的 rebase 丢掉的提交 | 已暂存但未提交的改动 |
| 被删除的本地分支(较近期的) | 队友尚未推送的提交 |
| 你本地对被强制推送覆盖的远端的视图 | 已过期并被 gc 清理的条目 |
还有三条约束制约着恢复操作:
- 它是本地的、临时的。 Reflog 只存在于你的
.git目录中,永远不会被推送,所以它能救你自己的工作,但救不了队友尚未推送的提交。 - 条目会过期。 默认情况下,Git 通过
gc.reflogExpire和gc.reflogExpireUnreachable保留可达的 reflog 条目 90 天、不可达的条目 30 天。孤立提交会一直存活到过期并经历一次垃圾回收(garbage-collection)之后才消失,所以要尽快恢复,并在完成之前不要运行git gc。 - 先恢复到一个新分支上。 把
git checkout -b recovered <hash>作为你的默认动作,这样你就能在改动共享分支之前先做验证。另外,请撰写描述清晰的提交信息:reflog 会显示它们,因此 “Fix login bug” 远比含糊的描述容易辨认。
附加技巧(在一次 force-push 之后): 远端服务器上没有你能读取的 reflog,但你本地的跟踪 ref 有。要恢复被 force-push 从远端抹掉的提交,用 git reflog show origin/<branch> 查看你的本地记录。紧挨在 force-push 之前的那条条目保存着旧的分支顶端。
结论
Reflog 是 Git 的本地安全网,它把几乎所有”我把工作弄丢了”的时刻变成了两条命令就能解决的问题:读日志,重新指向哈希值。它唯一救不了的,是你从未提交过的工作,所以真正持久有效的经验是尽早、频繁地提交,这能保证总有一条 reflog 条目可供查找。下次终端让你心头一沉时,先运行 git reflog,别做其他事。
常见问题
git reflog 和 git log 有什么区别?
git log 显示的是从某个分支顶端可达的提交历史,它沿着父指针回溯,因此孤立提交永远不会出现在其中。git reflog 显示的是本地仓库中 HEAD 或某个 ref 曾经占据过的每一个位置——commit、checkout、reset、rebase 和 merge——包括那些已经没有任何分支指向的提交。这就是为什么在 hard reset 之后 reflog 能找到某个提交而 git log 找不到:该提交已被孤立,但它的 ref 更新记录仍在日志中。
克隆仓库之后 git reflog 还能用吗?
不能。Reflog 只存储在你本地的 .git 目录中,永远不会被推送或拉取,所以一个全新的克隆一开始的 reflog 是空的,只覆盖你克隆之后所做的操作。它无法显示克隆之前的 HEAD 移动,也无法揭示队友的本地历史。Reflog 挽救的是你自己仓库中你自己丢失的工作;它不是一个共享的或有远端支撑的恢复工具。
reflog 条目过期之后我还能恢复提交吗?
通过 reflog 本身不行。默认情况下,可达条目在 90 天后过期,不可达条目在 30 天后过期,一旦一次垃圾回收清理掉这些孤立对象,它们就真的消失了。在那之前,你有时仍能通过 git fsck --lost-found 找到哈希值,该命令会列出对象库中的悬空(dangling)提交。可靠的做法是尽快恢复,并在拿回提交之前不要运行 git gc。
如果我不知道哈希值,怎么找到丢失的提交?
直接搜索 reflog。运行 git log --oneline -g --grep='fragment' 按提交信息的片段过滤 reflog 条目,或者用 git reflog --date=relative 按时间浏览条目,找出你想撤销的那次操作。对于没有 reflog 条目的孤立提交,git fsck --lost-found 会列出悬空提交,你可以用 git show 检查它们,然后再用 git checkout -b recovered <hash> 恢复到一个新分支上。