Git 冲突处理与 rerere
冲突标记删干净了,为什么功能还是丢了
两个分支同时修改鉴权配置,集成人员看到冲突后整文件选择了 ours。冲突标记消失了,git status 也允许继续,测试却在上线前才发现另一条分支新增的权限校验被一起覆盖。Git 没有选错:它只把无法自动决定的路径留在 unmerged 状态,最终业务语义必须由人和测试共同确认。
冲突不是一段特殊文本,而是 index 中一组尚未收敛的对象状态。共同祖先、当前合并侧和被合入侧分别保存在 stage 1、2、3;工作区只是用冲突标记或合并工具展示这些候选内容。merge、rebase、cherry-pick 和 revert 又各自保存进行中的序列状态。只删标记、不读取三阶段和当前操作,相当于在不知道输入的情况下提交输出。
先保护现场,再读取状态
先在可丢弃的实验仓库中练习,并记录实际 Git 版本和配置来源:
git --version
git config --show-origin --get merge.conflictStyle
git config --show-origin --get rerere.enabled
git config --show-origin --get rerere.autoupdate在真实项目开始 merge、rebase 或 cherry-pick 前,工作区和 index 应该干净:
git status --short
git diff
git diff --staged未提交工作不应和冲突解决混在一起。先提交到临时分支,或在团队允许时使用带说明的 stash,并确认 untracked 文件是否也需要保护。git merge --abort 官方明确只是“尝试”重建合并前状态;如果合并前已有未提交修改,而且冲突期间又继续改动,原状态可能无法完整恢复。
还要准备项目最小验证命令,例如单元测试、类型检查、格式检查和关键模块构建。冲突解决验证的是组合后的语义,不是文件里已经没有 <<<<<<<。git-merge 也提醒,合并前存在未提交改动时,--abort 未必能完整重建原状态,所以 clean 不是形式要求,而是恢复边界。
冲突处理能力随 Git 提供,不需要额外插件。先只在实验仓库启用易读冲突样式和 rerere,验证后再决定是否进入全局基线:
git config --local merge.conflictStyle zdiff3
git config --local rerere.enabled true
git config --local rerere.autoupdate false
git config --local --list --show-originzdiff3 在 ours 和 theirs 之外显示共同祖先,并省略三边相同的冗余上下文,通常比默认两段标记更利于判断“谁改变了什么”。如果团队 Git 基线不支持 zdiff3,先使用 diff3;不要在未验证的老版本上强推配置。
rerere.enabled=true 表示 reuse recorded resolution。git-rerere 描述了 preimage、postimage、复用、forget 和清理行为:Git 会记录某种冲突形态及其后续人工解法,在同类冲突再次出现时尝试复用。这里故意把 rerere.autoupdate 设为 false,让复用结果先进入工作区,人仍需审查并执行 git add。直接启用 autoupdate 会同时更新 index,效率更高,但错误解法也更容易进入下一次提交。
index 三阶段保存了什么
先理解 index 的三阶段
正常文件在 index 中只有 stage 0。冲突文件最多保留三份:
| stage | 含义 | 查看方式 |
|---|---|---|
| 1 | 共同祖先 base | git show :1:path/to/file |
| 2 | ours,当前合并侧 | git show :2:path/to/file |
| 3 | theirs,被合入侧 | git show :3:path/to/file |
git ls-files --unmerged
git show :1:app.conf
git show :2:app.conf
git show :3:app.conf普通 merge 中,ours 通常是当前分支,theirs 是传给 git merge 的分支。但 rebase 是把待重放提交逐个应用到新基线:冲突时 ours 通常代表“已经重放到的新基线一侧”,theirs 代表当前正在重放的提交。看到按钮或命令写着 ours/theirs 时,先用 git status、git show REBASE_HEAD 和 stage 内容确认,不能只凭分支名猜。
用状态而不是标记判断冲突
git status
git status --short
git diff --name-only --diff-filter=U
git ls-files --unmergedgit-status 用 UU 表示双方都修改、AA 表示双方都新增同一路径,UD / DU 表示一边修改、一边删除;git-ls-files 则暴露 stage 1、2、3。并非所有冲突都能表现为文本标记:二进制冲突、文件/目录冲突、重命名组合、submodule 指针冲突都必须从 status 和 index 读取。
让工具保持可审查
可以配置 merge.tool 或 IDE,但团队模板应把下面四步留作统一出口:列出 unmerged 路径、审查三阶段、暂存明确解法、执行项目验证。外部 mergetool 只负责编辑工作区,不能替代 git diff --staged 和测试。生成文件若能由权威源重新生成,应先解决源文件,再运行锁定版本的生成器;不要手工拼接大型产物。
用重复冲突观察 rerere
下面的实验仓库会制造一次稳定的内容冲突,再证明 rerere 可以复用解法。所有命令都应在专用测试目录执行,不要粘贴到业务仓库。
git init -b main conflict-lab
cd conflict-lab
git config user.name "Example Developer"
git config user.email "developer@example.invalid"
git config rerere.enabled true
git config rerere.autoupdate false
git config merge.conflictStyle zdiff3
printf 'mode=base\n' > app.conf
git add app.conf
git commit -m "建立冲突实验基线"
git switch -c feature
printf 'mode=feature\n' > app.conf
git commit -am "功能分支修改模式"
git switch main
printf 'mode=main\n' > app.conf
git commit -am "主分支修改模式"
git merge feature预期看到 CONFLICT (content),git status --short 显示 UU app.conf。先查看三阶段,再写入双方都认可的结果:
git ls-files --unmerged
git show :1:app.conf
git show :2:app.conf
git show :3:app.conf
printf 'mode=main+feature\n' > app.conf
git diff --check
git add app.conf
git diff --staged
git commit -m "解决模式配置冲突"首次提交后,rerere 已记录这组 preimage 与 postimage。回到合并前的第一父提交并再次合并:
git reset --hard HEAD^
git merge feature
git rerere status
git rerere diff
git diff
git status --short预期工作区中的 app.conf 已被写成 mode=main+feature,但在 rerere.autoupdate=false 下 index 仍处于 unmerged,需要人工复核:
git diff --check
git add app.conf
git diff --staged
git merge --continue实验成功标准不是第二次“没有提示”,而是:重复冲突被正确复用;index 未绕过人工确认;提交后的文件内容、历史图和最小测试均符合预期。实验完成后退出目录,确认路径确为专用实验目录,再按操作系统的安全方式删除整个 conflict-lab。
真实项目应把冲突处理接进协作流程,而不是写成个人技巧。CONTRIBUTING.md 至少说明:开始集成前保持 clean、团队采用 merge 还是 rebase、每种操作如何 continue/abort、哪些验证必须执行、哪些文件必须由 owner 解决、何时禁止使用整文件 ours/theirs。
一次可审查的处理顺序如下:
git status 确认当前操作和剩余冲突,不先改文件。git diff --name-only --diff-filter=U 划定范围,按模块 owner 分工。对每个文件读取 base/ours/theirs,先说清两边意图,再编辑最终内容。
git add <path> 只标记已经解决并审查的路径;删除是正确解法时使用 git rm <path>。git diff --check、git diff --staged 和项目最小测试一起通过。使用当前操作对应的 continue,而不是随手 git commit 掩盖状态。
push 前查看 git log --graph --oneline --decorate,确认没有意外丢提交或制造错误 merge。
对于高风险模块,冲突解决提交或 rebase 后的新提交仍需正常 PR/MR 审查。解决者应在描述中列出冲突文件、两侧意图、采用的组合方案和验证结果,方便 reviewer 针对语义而非标记做检查。
merge 冲突
git merge <topic>
git status
# 编辑、测试、逐个 git add / git rm
git merge --continue放弃整个 merge 使用 git merge --abort。git merge --quit 只忘记“正在 merge”的元数据,保留当前 index 和工作区;它不是回到起点,通常只在明确要保留现场并自行接管时使用。
rebase 冲突
git rebase <upstream>
git show REBASE_HEAD
# 编辑、测试、git add
git rebase --continuegit rebase --skip 会跳过当前正在重放的整个提交,不是跳过一个冲突文件。只有确认该提交已经被上游等价吸收或本来就应删除时才使用。--abort 回到 rebase 开始前的分支位置;--quit 则停止序列但保留当前 HEAD、index 和工作区,若使用 autostash,临时 stash 的去向也与 abort 不同。
cherry-pick 与 revert 冲突
git cherry-pick <commit>
# 解决后
git cherry-pick --continue
git revert <commit>
# 解决后
git revert --continue两者都使用 sequencer,支持 --continue、--skip、--abort、--quit。先通过 CHERRY_PICK_HEAD 或 REVERT_HEAD 确认当前补丁,避免把“补丁内容不再需要”误判成“当前文件可以删掉”。
rerere 观察与纠错
git rerere status
git rerere remaining
git rerere diff
git rerere forget -- path/to/file
git rerere gcstatus 列出 rerere 正在跟踪的冲突,remaining 列出尚未被自动解决的路径,diff 展示当前人工解法相对冲突 preimage 的变化。若已记录的解法错误,在当前同类冲突仍存在时用 forget 清除该路径的记录,再重新解决、暂存和验证。不要直接编辑 .git/rr-cache,它是仓库内部状态,不是团队配置接口。
文件已删掉冲突标记,continue 仍提示 unresolved
文件看起来正常,但 git rebase --continue 或 git merge --continue 拒绝继续。运行 git status --short 与 git ls-files --unmerged;若路径仍有 stage 1/2/3,index 还没有接受解法。编辑工作区不等于更新 index,或者 modify/delete 冲突只改了文件却没有 git add / git rm。
确认最终内容后执行 git add <path>;正确结果是删除时执行 git rm <path>。git diff --name-only --diff-filter=U 无输出,git diff --staged 只含预期解法,再执行对应 continue。
rebase 中选择 ours 后功能提交消失
冲突消失了,但待重放提交的改动不见了。比较 git show REBASE_HEAD、:2:path、:3:path,确认当前 sides 的实际内容。把普通 merge 的 ours/theirs 直觉套到 rebase;rebase 的 ours 是已重放的新基线一侧,theirs 才可能是当前业务提交。
若序列尚未结束,重新构造正确内容;无法确认时 git rebase --abort,从 clean 状态重做。用 git range-diff <old-range> <new-range> 或提交清单核对重写前后补丁,再跑模块测试。
rerere 自动写入了过期或错误解法
重复冲突很快“解决”,但组合结果不符合当前业务语义。检查 git rerere diff、工作区 diff 和 rerere.autoupdate 来源;确认是否已被自动暂存。冲突形态相似不等于上下文语义仍相同,或历史上记录过错误 postimage。
在提交前恢复当前冲突现场,执行 git rerere forget -- <path>,重新读取三阶段并人工解决。团队默认关闭 autoupdate。git diff --check、staged diff、模块测试和 reviewer 审查全部通过后才 continue。
abort 无法恢复合并前未提交修改
git merge --abort 失败,或退出后本地修改与开始前不同。查看 git status、ORIG_HEAD 和操作前是否存在未提交工作;不要立刻 hard reset。在脏工作区开始 merge,又在冲突期间改写了同一路径,Git 无法无歧义重建原状态。
先复制或提交仍可识别的工作到救援分支,再基于 ORIG_HEAD、reflog 和差异逐项恢复。后续把 clean 检查设为集成前置。恢复后比较救援分支、ORIG_HEAD 与当前工作区,确认未提交内容和原分支提交均在。
冲突只在 CI 或另一台机器出现
本机合并干净,CI 的集成分支或同事环境出现冲突、大小写路径冲突或生成文件差异。比较双方实际 commit、Git 版本、.gitattributes、大小写语义、生成器和合并方向,而不是只比较分支名。远端主干已推进、浅克隆缺少预期对象、文件系统大小写不同,或生成物未锁定。
fetch 后用同一提交对重放实验;统一 attributes 和生成器版本。自动化可评估 git merge-tree --write-tree <base> <topic> 做无工作区预演。在与 CI 相同提交对和工具基线上重跑,确认预演退出码、冲突路径和最终测试一致。
本地冲突计算通常不需要网络;只有 fetch、push 或拉取缺失对象时才经过代理与凭证。排障时先分层:git status、git ls-files 能运行而 git fetch 失败,问题在远端连接链,不应通过重置冲突状态“修网络”。代理、CA、HTTPS 凭证和 SSH key 应进入统一凭证基线,不在冲突日志中粘贴带凭证 URL。
冲突解决本身没有额外仓库权限,但最终 push 受保护分支、force push 和 PR/MR 规则约束。rebase 后提交 ID 改变,若分支已经共享,只能按团队规则使用受控的 --force-with-lease,并先同步远端引用;不要为了让冲突结果上线而索取主干绕过权限。
.git/rr-cache 可能包含冲突片段和人工解法,它位于本地 Git 元数据中,不会随普通 commit 自动共享。不要把它打包进构建日志、支持工单或缓存制品;冲突内容可能包含尚未发布的代码或敏感配置。团队若希望共享“解决知识”,应把规则写入代码、测试、生成器或文档,而不是共享个人 rr-cache。
团队应把冲突看成代码变更,而不是清障杂务。最低治理建议是:
指定 Git 基线版本与 merge.conflictStyle,先在实验仓库验证再推广。个人可启用 rerere.enabled,默认 rerere.autoupdate=false;是否允许全局启用由仓库 owner 决定。高风险目录由 CODEOWNERS 或评审规则要求 owner 复核冲突后的 staged diff。
PR/MR 模板要求记录 rebase/merge 后重新执行的测试,以及被人工解决的路径。禁止用“整仓选择 ours/theirs”“删标记即完成”“--skip 直到通过”作为标准流程。定期演练内容冲突、modify/delete、错误 rerere 解法和 abort 恢复,演练仓库不得使用真实凭证与业务数据。
是否采用 rerere 取决于冲突重复度和审查能力。短生命周期分支、很少重复冲突的团队收益有限;长期维护分支、多版本回合、反复 rebase 的团队收益明显。它优化的是“重复手工编辑成本”,不会降低第一次解决冲突所需的领域知识,也不会替代测试。
自动解决会放大一次错误
rerere 的价值与风险来自同一机制:一次人工解法可以被多次复用。团队应把首次解法当作可传播资产审查,默认不自动更新 index。只要冲突附近语义、生成器版本或配置模型变化,就应重新审查,而不是相信“Git 识别为同类”。
冲突数量不是风险大小
一个权限判断、迁移脚本或锁文件冲突,风险可能高于几十个文档冲突。分工应按模块语义和 owner,不按文件数量。对锁文件优先解决依赖声明再重生成;对迁移序列先确认不可重复编号和执行顺序;对安全策略文件必须重新跑策略测试。
rename 与目录冲突不能靠搜索标记治理
文本扫描只能发现部分内容冲突。rename/rename、rename/delete、文件/目录、模式位、二进制和 submodule 冲突可能没有 <<<<<<<。CI 冲突预检应消费 Git 的 unmerged/index 信息或 merge-tree 结构化输出,不能把 rg '<<<<<<<' 当完整门禁。
重写历史会改变审查基线
rebase 解决冲突后产生新 commit ID,旧审批、比较基线和签名状态可能失效。长 PR 应在最终 rebase 后重新运行检查并触发必要复审;团队需要明确谁可 force-with-lease、何时冻结分支、如何保存旧 tip 以便 range-diff。
冲突现场也是证据
复杂事故中,不要一上来执行 reset --hard、clean 或删除 .git 状态。先记录 git status、当前 HEAD、操作头引用、unmerged 路径和 staged diff,必要时创建救援分支或工作区副本。可恢复性来自对象和引用仍在,而不是来自某条万能命令。
开始前
已确认 Git 版本、当前分支、目标提交和集成方式。工作区与 index 干净,未提交工作已单独保护。已知道项目最小测试、生成器和模块 owner。
代理或凭证问题与本地冲突状态已经分层。
解决中
已用 status 和 unmerged index 判断冲突类型,而非只搜标记。已读取 base、ours、theirs,并确认 rebase 下 sides 的真实含义。每个 git add / git rm 都对应一个已审查的最终决策。
没有用 --skip 代替文件级解决,也没有盲选整文件 ours/theirs。
继续前
git diff --name-only --diff-filter=U 无输出。git diff --check 与 staged diff 无异常。项目最小测试和受影响模块检查通过。
执行的是当前操作对应的 continue,最终历史图符合预期。
团队推广前
rerere 已在实验仓库复测,autoupdate 策略明确。错误记录有 rerere forget 纠错流程,rr-cache 不进入共享制品。高风险目录的冲突解法需要 owner 复审。
团队已演练 abort、救援分支和重写历史后的 range-diff。
