你执行 git pull --rebase origin main 同步自己的 feature 分支,Git 在 src/config.ts 上停下报冲突——冲突里显示的改动你根本没写过,或者一堆冲突标记把三个人的 commit 搅成一团。解完冲突继续之后,git log --oneline 变得完全陌生:SHA 全变了、commit 顺序变了、committer 时间戳全更新了,有时候还冒出一个你没打算要的 Merge branch 'main' 或 fix conflict 提交。
最快的办法:如果还没 push,先 git rebase --abort 退回到开始前的状态,再用 git log --oneline --left-right HEAD...origin/main 把分叉看清楚再重试。看着”乱”的东西大多是正常的:rebase 本来就会重写每个本地 commit 的 SHA 和 committer 时间。真正出问题的情况(丢了 commit、多了 fix conflict 提交、同一改动出现两次)下面都有干净的修法。
哪些是正常的、哪些是真坏了
| rebase 之后的现象 | 正常吗 | 含义 |
|---|---|---|
| 每个本地 commit 的 SHA 都变了 | 正常 | rebase 把每个 commit 在新基底上重新应用,hash 必然变 |
| committer 时间戳全部变成”现在” | 正常 | author date 保留,committer date 在重新应用时被重置 |
| 你的 commit 相对远端 commit 顺序变了 | 正常 | 你的 commit 现在整体落在 origin/main 之上,不再按时间穿插 |
冒出一个 Merge branch 'main' 或 fix conflict 提交 | 坏了 | rebase 过程中你用了 git commit 而不是 git rebase --continue |
| 你的某个 commit 不见了 | 坏了 | 本地的 merge commit 被展平丢弃,或某个 commit 被跳过了 |
| 同一处改动出现了两次 | 坏了 | 某个 commit 冲突后被重新应用,留下了一个近似重复 |
动手之前先跑 git log --oneline --graph -15 和 git log --format="%h %ai %ci %s" -8(对比 author date %ai 和 committer date %ci),先判断自己落在哪一行。
常见原因
按命中率从高到低排列。
1. 解完冲突用了 git commit 而不是 git rebase --continue
这是历史”变乱”最常见的原因。某个 commit 冲突了,你改好文件、git add 之后顺手敲了 git commit。这会在 rebase 内部新建一个独立 commit(往往自动命名为 Merge branch 'main' 或你随手写的内容),而不是把解冲突的结果折叠回正在被 replay 的那个 commit。rebase 继续往下走,历史里就留下一个孤立的 fix conflict 式提交。
怎么判断:git log --oneline -10 出现一个像 fix conflict、merge、resolve 这种泛泛消息、你不认得是真实改动的 commit。
2. 长期分支,要 replay 的远端 commit 太多
feature 分支几十个 commit 前就和 main 分叉了。git pull --rebase 要把你的 10 个 commit 逐一 replay 到比如 40 个新远端 commit 之上,其中任何一次 replay 都可能冲突。压垮人的是这些冲突累积起来的噪音,其实每个单独冲突都很小。
怎么判断:git rev-list --count HEAD..origin/main——若超过 10–15,预期这次 rebase 会牵动大量陈旧上下文。
3. 同一个文件在你的多个本地 commit 里都改过
你的分支里有三个 commit 都动了 src/config.ts。replay 时每一个都可能和远端版本冲突,产生三次需要分别解决的冲突,单独看很难想清楚,而且容易解得前后不一致。
怎么判断:在本地分支上跑 git log --oneline -- src/config.ts(即 origin/main..HEAD 里的 commit)。若这个文件出现在你的多个 commit 里,预期会反复冲突。
4. 本地的 merge commit 让 rebase 出错并丢了 commit
你的分支里有一个 merge commit(往往来自之前一次没带 --rebase 的 git pull)。默认情况下 git rebase 会展平历史、不保留 merge commit——它把底层 commit 线性地 replay,从而打乱顺序,还可能丢掉你以为安全的一条侧分支。
怎么判断:git log --oneline --merges origin/main..HEAD——只要打印出 merge commit,它们就会被展平。如果你确实需要保留这种结构,用 git rebase --rebase-merges(或 git pull --rebase=merges)。
5. 全局设了 pull.rebase,每次 git pull 都在悄悄 rebase
git config --global pull.rebase true 让每次 git pull 都 rebase,哪怕你以为在做 merge。你按 merge 的思路解冲突、敲了 git commit,于是正好制造出原因 #1 的坏状态。
怎么判断:git config pull.rebase 返回 true,而你命令行里并没有加 --rebase。
6. rebase 中途 git stash pop 把额外改动混进了某个 commit
rebase 在冲突处暂停时,你跑了 git stash pop 恢复无关的工作,然后 git add -A 继续——把 stash 的改动折进了一个不该包含它的 replay commit。
怎么判断:git show HEAD(或那个可疑 commit)里包含超出该 commit 逻辑范围的改动。
最短修复路径
Step 1:rebase 还在进行中就先 abort
git rebase --abort
这会把分支退回到 rebase 开始前的精确状态。判断 rebase 是否还在进行中(进行中的状态存在 .git/rebase-merge/):
git status # 还在 rebase 时会打印 "interactive rebase in progress"
git rev-parse --verify REBASE_HEAD # 只有 rebase 进行中才会成功(打印一个 SHA)
没有 git rebase --status 这个子命令——git status 才是标准检查方式。
Step 2:重试前先把分叉看清楚
git fetch origin
git log --oneline --left-right HEAD...origin/main
--left-right 用 < 标记本地 commit、> 标记远端 commit。数清两组各有多少,你就知道 rebase 要 replay 什么、又要 replay 到什么之上。
Step 3:分叉很大或是共享分支时,用 merge 而不是 rebase
git pull --no-rebase origin main # 只产生一个 merge commit,不逐一 replay
merge commit 如实记录了集成点,避免把每个本地 commit 都 replay 一遍。只要别人已经 pull 过这个分支,就一定用 merge——rebase 会重写队友手里已经有的 SHA,后面会引出新的麻烦。
Step 4:想要干净的 rebase,先把本地 commit 压成一个
# pull 之前先把本地 commit 压成一个
git rebase -i origin/main
# 编辑器里第一个 commit 保留 'pick',其余改成 'squash'(或 'fixup')
git pull --rebase origin main # 现在最多一个 commit 要 replay → 最多一次冲突
git rebase -i origin/main 会打开 origin/main..HEAD 里的 commit 列表。先 squash 能把”十个 replay 十次冲突”变成一次。
Step 5:按正确顺序解冲突(绝不用 git commit)
# git pull --rebase 过程中出现冲突时:
git status # 列出冲突文件
# 编辑每个冲突文件:留下正确代码,删掉 <<<<<<< / ======= / >>>>>>> 标记
git add src/config.ts # 把每个已解决的文件 stage 进去
git rebase --continue # 不要用 git commit
git rebase --continue 可能会打开编辑器让你确认 commit 消息——这是正常的,保存关闭即可。长 rebase 想跳过编辑器,跑 GIT_EDITOR=true git rebase --continue。如果某个 replay commit 的改动上游已经有了,Git 会提示 “No changes — did you forget to use ‘git add’?”;若确实没有要 add 的内容,用 git rebase --skip 而不是强行产生空 commit。每个 replay commit 重复以上步骤。
Step 6:删掉多余的 fix conflict 提交(对应原因 #1)
如果 rebase 时你已经造出了一个垃圾 commit:
git tag backup/before-cleanup HEAD # 安全网
git rebase -i origin/main
# 编辑器里:把那行 'fix conflict' 的 'pick' 改成 'fixup',折进它上面那个 commit;
# 若纯属噪音就改成 'drop'
Step 7:验证最终状态
git log --oneline -10
git diff origin/main
git status
git grep -n '<<<<<<<\|>>>>>>>' # 必须无输出——没有残留冲突标记
如果你已经把坏历史 push 出去了,把清理好的版本发布上去:
git fetch origin
git push --force-with-lease # 拒绝覆盖队友新推的 commit
优先用 --force-with-lease 而不是 --force;先 fetch,让”lease”反映真实的远端状态。
怎么恢复被丢掉的 commit
如果 Step 7 显示某个 commit 不见了,它没真消失——reflog 还指向它:
git reflog --date=relative | head -20 # 找到 rebase "之前"那个 SHA
git branch recover-it <那个-sha> # 先停到一个分支上
git cherry-pick <丢失的-sha> # 或者只把丢的那个 commit replay 回当前分支
完整恢复流程见Rebase 之后 commit 消失了。
预防建议
- feature 分支尽量短命。3 个 commit replay 到 5 个远端 commit 之上很轻松;10 个 replay 到 40 个之上才是混乱的根源。
- rebase 过程中
git add之后永远用git rebase --continue——绝不用git commit或git merge。光这一个习惯就能避免大部分”历史变乱”。 - 把默认改回 merge,只在需要时主动 rebase:
git config --global pull.rebase false。 - 启用 rerere,让 Git 记录并自动重放你的冲突解决方案:
git config --global rerere.enabled true。这是长而重复的 rebase 上最省时间的一招。 - pull 之前先把 WIP commit 压成有意义的单元,让每个 replay commit 都是独立可理解的。
- 先预览即将进来的内容:
git fetch && git log --oneline HEAD..origin/main。没有意外,就不会在 rebase 中途慌乱地敲git commit。 - 任何队友 pull 过的分支,用
git pull --no-rebase——绝不重写已发布的历史。
常见问答 (FAQ)
Q:rebase 之后 commit 时间戳全变了,这有问题吗?
A:没有。Git 保留 author date(你最初写代码的时间),但把 committer date 重置成现在,因为每个 commit 都在新基底上被物理重建了。git log --format="%ai" 能看到 author date 没变。只有当审计追溯明确依赖 committer 时间时,才需要改用 merge。
Q:git pull --rebase 把我一个 commit 整个弄丢了,怎么找回来?
A:它还在 reflog 里。跑 git reflog | head -20,找到 rebase 之前那个 SHA,再 git branch recover-it <sha> 并 git cherry-pick 把丢的 commit 接回来。丢 commit 几乎都来自被展平的本地 merge commit(原因 #4)。详细步骤见Rebase 之后 commit 消失了。
Q:我在 rebase 里敲 git commit 造出了一个 fix conflict 提交,怎么删掉?
A:git rebase -i origin/main,把那个 commit 的 pick 改成 fixup(把它的改动折进上面那个 commit)或 drop(如果它没加任何真实内容)。先 git tag backup/before-cleanup HEAD,这样随时能退回去。
Q:怎么让 git pull --rebase 不再自动跑?
A:查 git config pull.rebase。若是 true,是全局设置在强制 rebase。用 git config --global pull.rebase false 改回去,之后每次需要 rebase 再显式加 --rebase。Git 没有内置”先问我一下”的提示,但可以用 .git/hooks/pre-rebase 脚本拦一道:read -p "Rebase? [y/N]: " yn && [ "$yn" = y ]。
Q:git rerere 是什么,要不要启用?
A:rerere(Reuse Recorded Resolution)会记录你每次怎么解决冲突,rebase 后面再遇到完全相同的冲突时自动套用同一方案。对长而重复的 rebase 极其有用。用 git config --global rerere.enabled true 启用;用 git rerere diff 看它记住了什么。
Q:git rebase --continue 之后 Git 打开编辑器让我改 commit 消息,是出问题了吗?
A:没有——较新的 Git 在 continue 时会打开编辑器,让你确认或微调刚解决那个 commit 的消息。保存关闭即可继续。长 rebase 想跳过它,跑 GIT_EDITOR=true git rebase --continue。
相关阅读
- Rebase 之后 commit 消失了
- Cherry-pick 解冲突后变成空 commit
- Force push 覆盖了队友的 commit
- 二进制文件合并冲突 — 手动无法 resolve
- AI 帮你解决合并冲突
- AI 意外修改了 Git 历史
标签: #git #version-control #排查