你在 feature 分支上执行 git rebase main,中途遇到冲突,于是 git rebase --abort,或者 rebase 顺利完成——结果 git log --oneline 里只剩下基准分支的提交,你那五个工作 commit 不见了。又或者 rebase 成功了,但分支只指向一个被 squash 的提交,而不是你预期的三个。
最快的修复方式:你的 commit 几乎从不会被真正删除。 rebase 开始时,Git 会把旧的分支顶点存进 ORIG_HEAD。只要你之后没有再执行过 rebase、merge 或 git reset,下面这一行就能把分支精确还原到 rebase 之前:
git reset --hard ORIG_HEAD
如果 ORIG_HEAD 已经被覆盖,reflog 里仍然保存着分支经历过的每一个状态,默认保留 90 天(不再可达的条目保留 30 天,截至 2026 年 6 月)。本文接下来会讲怎么找到正确的 SHA、确认它包含你的工作,并安全地还原分支。
先判断你属于哪种情况
执行 git reflog show HEAD | head -20,把看到的内容和下表对照。
| 你观察到的现象 | 可能的原因 | 跳转 |
|---|---|---|
| rebase 刚结束,之后没执行过其他命令 | 分支移动了,ORIG_HEAD 还完好 | 上面的最快修复 |
有 rebase (start) 和 rebase (finish) 条目,但部分 commit 不在 git log 里 | interactive 时 drop/squash 把它们合掉了 | 原因 1 |
reflog 里出现 rebase --skip | 引发冲突的那个 commit 被丢弃,而非解冲突 | 原因 2 |
git pull --rebase 之后提交数量减少 | 某个 commit 被判定为「已在远端应用」 | 原因 3 |
本地还有你的 commit,但 git fetch 后看到的是别的 | 队友 force-push 覆盖了分支 | 原因 4 |
用过 git rebase --onto,commit 在 git log 里彻底找不到 | 范围参数写错,commit 变成悬挂对象 | 原因 5 |
常见原因
按命中率从高到低排列。
1. Interactive rebase 中误操作 drop 或 squash
你执行 git rebase -i HEAD~5,不小心在某行按了 d、整行删掉了某个 pick,或者在想要 pick 的地方用了 squash/fixup。这些被合掉的 commit 仍以悬挂对象的形式存在。
怎么判断:git reflog show HEAD | head -20。你会看到类似 HEAD@{3}: rebase (finish): returning to refs/heads/feature 的条目;紧挨在 rebase (start) 之前 那一行的 SHA,就是 rebase 动手之前的分支顶点。
2. 用 git rebase --skip 丢弃了引发冲突的 commit
rebase 因冲突停下时,git rebase --skip 并不是「跳过这一步冲突」——它会 直接丢弃正在应用的整个 commit 然后继续。很多人以为可以稍后再解冲突,结果那个 commit 就从结果里悄悄消失了。
怎么判断:git reflog | grep -i 'rebase.*skip',或者找一个提交数比开始时少的 abort/finish 条目。rebase 前的顶点仍在 reflog 里,就在 rebase (start) 之前。
3. git pull --rebase 把它认为「已应用」的 commit 丢掉了
git pull --rebase 会把本地 commit 重放到拉取下来的远端顶点之上。如果某个本地 commit 是上游某次改动的干净 cherry-pick(比如队友把你的修复 cherry-pick 过去了),Git 会认为它是冗余的并丢弃它。在 merge 后端里这就是 --empty=drop 的默认行为。
怎么判断:对比前后的提交数量。git log origin/main..HEAD --oneline 列出仅本地有的提交;如果某个 commit 缺失了,用 git log origin/main --oneline | grep "关键词" 检查它的改动是否已经在上游落地。
4. 你 rebase 之后,队友又 force-push 覆盖了分支
你本地的 reflog 仍然显示着你的 commit,但远端分支已经指向别处,于是 git fetch 之后看起来就像你的提交消失了。
怎么判断:git log HEAD..origin/feature --oneline 非空、且列出一堆你没写过的提交,而 git log origin/feature..HEAD --oneline 里正是你以为丢了的那些 commit。
5. git rebase --onto 的范围参数写错了
在 git rebase --onto <newbase> <oldbase> <branch> 里,<oldbase> 写错会移动一段你本不想动的范围,并把部分 commit 留成悬挂对象,不再被任何分支可达。
怎么判断:git fsck --lost-found 会列出悬挂 commit 对象(同时写入 .git/lost-found/commit/)。用 git show <sha> 逐个查看。
最短修复路径
如果上面的最快修复(git reset --hard ORIG_HEAD)已经还原了分支,那就完成了,直接跳到「如何确认已修复」。否则通过 reflog 恢复。
Step 1:在 reflog 里找到 rebase 前的 SHA
git reflog --date=iso | head -30
找到紧挨 rebase (start)(或 rebase -i (start))之前的那一行。左列的 SHA 就是 rebase 动手前的分支顶点。把它记为 PREBASE_SHA。
Step 2:在那个 SHA 上建一个恢复分支
先建分支而不是直接 reset——这一步不破坏任何东西,给你一个安全的检查点。
git branch recovery/feature-prebase PREBASE_SHA
确认恢复分支包含你的 commit:
git log --oneline recovery/feature-prebase | head -10
Step 3:核实这些 commit 确实是你的工作
git diff main recovery/feature-prebase -- src/
确认你看到的是真实改动,而不是 rebase 的元数据或空 diff。
Step 4:把 feature 分支重置到恢复点
# 仅在 Step 3 显示出正确 diff 之后再执行
git checkout feature
git reset --hard recovery/feature-prebase
Step 5:如果仍然需要 rebase,谨慎重做一次
# --empty=keep 保留那些「rebase 后变空、否则会被丢弃」的 commit;
# --keep-empty 保留那些「一开始就是空」的有意提交
git rebase --empty=keep main
如果出现冲突,逐个文件解决后执行 git rebase --continue——除非你确实想丢弃那个 commit,否则绝不要用 --abort 或 --skip。
Step 6:推送还原后的分支
# 因为历史被改写,必须强推
git push --force-with-lease origin feature
--force-with-lease 会在「自上次 fetch 后别人又推过这个分支」时拒绝推送,所以不会像裸 --force 那样悄悄覆盖队友的工作。
如何确认已修复
git log --oneline -10按正确顺序显示出你预期的全部 commit。git diff recovery/feature-prebase(或git diff ORIG_HEAD)没有任何输出——工作区与恢复的顶点完全一致。git status干净,且在正确的分支上。- 推送之后,
git log origin/feature..HEAD为空,说明远端与本地一致。
确认无误后,删掉临时分支:git branch -D recovery/feature-prebase。
预防建议
- 任何 rebase 之后先想到
ORIG_HEAD:git log ORIG_HEAD -1 --oneline显示旧顶点,只要之后没有执行过其他移动历史的命令,git reset --hard ORIG_HEAD就能撤销这次 rebase。 - 不确定范围时用数量而非分支名:
git rebase -i HEAD~3比git rebase -i main安全。 - 设置
git config --global rebase.missingCommitsCheck error(默认是ignore),这样 interactive rebase 在你删掉一行pick/drop时会停下报错,而不是悄悄丢掉那个 commit。 - 始终用
--force-with-lease推送,绝不用裸--force;当远端出现意外移动时它会中止。 - 把 reflog 保留期调长:
git config --global gc.reflogExpire 180和git config --global gc.reflogExpireUnreachable 90(默认分别是 90 天和 30 天)。 - 高风险 rebase 前给当前顶点打标签:
git tag backup/before-rebase HEAD。标签是永久引用,即使git gc跑过这些 commit 也能保留。 - 共享分支优先用 merge 而不是 rebase,避免改写公共历史。
常见问答 (FAQ)
Q:git rebase --abort 能撤销已经完成的 rebase 吗?
A:不能。--abort 只在 rebase 因冲突暂停时有效。对于已完成的 rebase,用 git reset --hard ORIG_HEAD,或重置到你在 git reflog 里找到的 rebase 前 SHA。
Q:刚 clone 的仓库 reflog 是空的,还能恢复吗?
A:reflog 只在本地,clone 时是全新的。如果远端被 force-push 过,原始 commit 只可能保存在别人的 clone 里。让队友在他们机器上执行 git reflog 或 git log <分支>,把那个 SHA 重新推上来。
Q:git gc 跑过之后还能找回 commit 吗?
A:有可能。如果 commit 已不可达且超过了 gc.reflogExpireUnreachable(默认 30 天),可能已被清理。否则执行 git fsck --lost-found——悬挂 commit 会被列出并写入 .git/lost-found/commit/,用 git show 逐个查看 SHA。
Q:我把几个 commit squash 了,现在看起来都没了,工作丢了吗?
A:没丢。squash 是把改动合进了一个 commit,什么都没删除。git log -p <squash 后的 sha> 能看到全部改动。想拆回来就 git reset HEAD~1 再分块重新提交,或按上文从 reflog 恢复原始 commit。
Q:我 rebase 并 force-push 之后,队友该怎么同步才不会丢自己的改动?
A:他们应该 git fetch origin,再 git reset --hard origin/feature(先把本地改动 stash 或提交掉),而不是 git pull——后者会尝试合并被改写的历史,把分支搞乱。
相关阅读
- 找回 Git 历史里的旧版本文件
- Force push 覆盖了队友的 commit
- 我在 detached HEAD 上提交了,怎么办
- git pull —rebase 让冲突历史变乱
- Cherry-pick 解冲突后变成空 commit
- AI 意外修改了 Git 历史
- AI 帮你回滚代码改动
标签: #git #version-control #排查