git pull --rebase 把冲突历史弄乱了

git pull --rebase 在一个看不懂的冲突上停下,解完后 git log 全乱了:SHA 变了、commit 顺序变了、还多出一个 fix conflict 提交。哪些是正常的、哪些是真坏了、怎么干净恢复。

你执行 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 -15git 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 conflictmergeresolve 这种泛泛消息、你不认得是真实改动的 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(往往来自之前一次没带 --rebasegit 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 commitgit 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

相关阅读

标签: #git #version-control #排查