Git rebase 之后 commit 消失了

执行 git rebase 后部分提交从历史中消失。用 ORIG_HEAD 或 reflog 在两分钟内找回丢失的 commit,零数据损失。

你在 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 loginteractive 时 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 那样悄悄覆盖队友的工作。

如何确认已修复

  1. git log --oneline -10 按正确顺序显示出你预期的全部 commit。
  2. git diff recovery/feature-prebase(或 git diff ORIG_HEAD)没有任何输出——工作区与恢复的顶点完全一致。
  3. git status 干净,且在正确的分支上。
  4. 推送之后,git log origin/feature..HEAD 为空,说明远端与本地一致。

确认无误后,删掉临时分支:git branch -D recovery/feature-prebase

预防建议

  • 任何 rebase 之后先想到 ORIG_HEADgit log ORIG_HEAD -1 --oneline 显示旧顶点,只要之后没有执行过其他移动历史的命令,git reset --hard ORIG_HEAD 就能撤销这次 rebase。
  • 不确定范围时用数量而非分支名:git rebase -i HEAD~3git 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 180git 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 refloggit 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 #version-control #排查