10.06 代码审查与合并:让 IM 历史规则变更可复核
返回第十卷 · Git 前置:10.03 · 重构前置:10.05
10.06 代码审查与合并:让 IM 历史规则变更可复核
DeepTutor 初稿经技术与教学审阅后的静态课程。代码差异、分支图、命令输出和冲突标记都是教学推演;本次没有运行 Go、执行教学 Git 合并、创建真实 PR、构建站点或连接 IM 服务。
本章的业务任务和先修
10.03 已从工作区 W、暂存区 I、提交 H 讲到分支;10.05 已说明重构应保留外部行为。本章把两者结合:一位学习者准备审查本地 IM 历史工具的一组变更。第一组只整理 JSON 读取的包边界,第二组依据新需求把单条消息正文上限从 6 字节提高到 9 字节。审查者要回答:改动是否符合需求?还有哪条边界会退化?两个分支相遇时怎样保留正确规则?
这仍是虚构 c-a/u-a/m-a 的教学场景。01.12 的 CLI -max-bytes 默认 1 MiB,限制整份输入文件;本章的 6→9 字节限制只属于 10.01–10.02 的单条正文教学规则。两者名称相似,改变一个不意味着另一个改变。没有任何一步证明服务端受理、设备送达或已读。
| 阅读层 | 问题 | 练习成果 |
|---|---|---|
| 起步 | 差异、提交、分支、评审分别是什么? | 能从需求逐行标注一组变更 |
| 原理 | Git 如何把两侧历史合到一起? | 能画出共同祖先和冲突位置 |
| 工程 | 文本冲突与业务冲突怎样处理? | 能写出解决依据和回归矩阵 |
| 协作 | 什么证据支持合并? | 一份范围、风险、验证与未覆盖说明 |
第一次先读一至三节,确认会看差异和提问;第二遍再读四至七节的合并、rebase 与回归。最后在自己的独立练习仓库完成第八节。不在课程仓库执行这些教学命令。
一、从需求到变更包:先知道审什么
一次变更包是为一个可解释目标准备的一组差异、提交、验证与说明。它不一定等于一个文件,也不一定等于一个提交。比如“把 v1 JSON 解码从命令包移到 internal/historyfile”是重构,按 10.05 必须保持 check/list/export、错误类别、默认不覆盖和文件格式不变;“把正文上限改成 9 字节”则改变合法输入集合,是单独的需求变更。两者可以有各自的提交或分支,避免审阅者分不清行为差异来自哪里。
本章假设需求 R9 明确约定:single、group 有效;先判断种类,再判断上限正数和正文非空;按 UTF-8 字节计算,len(body) <= 9 合法。你好呀 为 9 字节,应通过;你好呀a 为 10 字节,应拒绝。旧的 6 字节规则由另一份版本记录保留,不在重构提交里偷改。若产品根本没有批准 R9,审阅者不能凭代码作者说“更合理”就接受规则变化。
审查需要四类材料:
- 需求与合同:R9 的依据、版本、边界与错误优先级。
- 差异与历史:改了哪些行,哪些提交只移动代码,哪些提交改变行为。
- 验证证据:独立预期、个人实际运行记录、文件前后状态及明确未覆盖范围。
- 风险与后续:未知字段、旧文件、CLI 输出、敏感正文、远端依赖是否受影响。
有审查意见并不表示已验证运行;有绿色测试也不意味着测试包含了 9 字节等号边界。审阅者必须把“看到了什么”和“据此能推断什么”分开。
二、先读差异,再读代码
10.03 的 W/I/H 现在用于确定比较范围。在个人练习仓库中,常见命令的含义是:
| 命令 | 比较或展示什么 | 适合回答 |
|---|---|---|
git status --short | 当前工作区和索引的概况 | 是否有未纳入提交的变化 |
git diff | 工作区相对索引的未暂存差异 | 刚才编辑了什么 |
git diff --cached | 索引相对 HEAD 的待提交差异 | 下一次普通提交会包含什么 |
git show <提交> | 一次提交的信息和差异 | 这一提交做了哪一步 |
git diff main...topic | 共同祖先到 topic 端点的差异 | 这条主题分支带来什么 |
git log --oneline --graph --all | 本地已知提交图 | 两个分支怎样分叉、合并 |
main...topic 在 git diff 中用共同祖先作为比较起点;它不等于两个分支端点直接相减。git log main..topic 的两个点又是“可从 topic 到达、不可从 main 到达”的提交集合,和 git diff 的内容比较回答不同问题。初学者先看图,再说自己比较了哪两个状态,不要只记符号。
审查 R9 的错误候选片段:
- if len(body) > maxBodyBytes {
+ if len(body) >= maxBodyBytes {
return ErrTooLong
}新上限若为 9,这个 >= 会把恰好 9 字节的 你好呀 拒绝。审查意见不能只写“这里好像不对”,应指出“R9 约定等于上限合法;len("你好呀") == 9;这里应仍以 > 拒绝,新增 9/10 字节边界案例”。如果旧代码本来就是 >,R9 的主要改动可能只在配置上限与预期,而无需改变比较符。
读差异时从需求风险定位:会话种类优先级是否变了?错误是否仍由 errors.Is 保留身份?本地 v1 messages:null 是否仍拒绝、[] 是否仍合法?list 是否保持文件顺序且默认不输出正文?export 目标已存在是否仍拒绝?CLI 退出码 0/2/3/4/5 是否稳定?最后再看命名、重复代码和注释。git diff --check 可以找尾随空白等格式问题,不能证明这些业务规则正确。
三、把审阅意见写成可行动的证据
Pull Request(PR)是代码托管平台上提出合并、讨论和查看差异的协作记录;Git 自身保存的是提交、引用和对象,不需要有 PR 才能在本地提交或合并。平台是否要求审批、自动检查或保护分支,取决于具体仓库设置。本章只展示静态模板,不创建真实 PR,也不向任何人发送评论。
一个可复核的变更说明可以写成:
目标:依据 R9 将单条文本消息上限由 6 字节改为 9 字节。
范围:只改变领域规则的上限和对应测试预期;文件 -max-bytes 仍为 1 MiB。
前后:6 字节始终合法;9 字节由拒绝变合法;10 字节仍拒绝。
保持:未知种类优先、空正文拒绝、重复 ID 不替换、v1 文件和 CLI 合同不变。
证据:列出个人实际执行的版本、命令、9/10 字节结果和状态前后值。
未覆盖:网络受理、跨设备同步、崩溃原子写入。审阅意见宜带位置、输入、规则、预期。例如:“ValidateMessage 的长度分支在 9 字节时返回 TOO_LONG;R9 规定等号合法,请加 你好呀 与 你好呀a 两例并解释前后消息状态。”作者回应可给出更正的差异和独立执行记录;如果原要求含糊,应先补需求,而不是把作者和审阅者的猜测当成规则。
不同意见有不同处理方式:阻塞缺陷指出当前变更违反已知合同;澄清请求指出需求或证据不足,可能需要产品决定;建议提出可读性改进,可以讨论是否属于本次范围。对人格、资历的评价不帮助定位问题。审批也只表明特定审阅范围内的判断,不代替所有条件下的正确性证明。
四、三方合并:为何会有冲突
分支是指向提交的引用。设共同祖先 B0 写着 maxMessageBytes = 6,topic-r9 在 T1 改成 9,main 的另一次变更 M1 改成 12:
T1 topic-r9: 6 → 9
/
B0: 6 ─┤
\
M1 main: 6 → 12Git 合并用共同祖先、当前分支端点和待并入分支端点这三份状态推导结果。若只有一侧改变一处,通常可自动采用它;两侧改同一片文本且无法自动选择,会留下冲突供人解决。若当前分支没有自己的新提交,引用可能直接前移,这是快进;两边都前进时,普通合并可能产生有两个父提交的合并提交。具体策略和配置会影响是否强制建合并提交,不能把每次 merge 都说成一定生成新提交。
文本冲突可能显示为(下方标记多缩进一格,实际文件的标记从行首开始):
<<<<<<< HEAD
maxMessageBytes = 12
=======
maxMessageBytes = 9
>>>>>>> topic-r9这些是 Git 插入的冲突标记,不能留在最终源码里。HEAD 与另一侧标签帮助定位来源,但“ours/theirs”要按当前正在执行的操作解释;在 rebase 过程中尤其不能凭日常语义盲选。Git 只能提示两侧内容不同,不能决定已批准业务需求到底是 9、12、每类会话不同上限,还是根本不该合并。这是需求决策,必须追踪 R9 与另一份需求的优先级和生效版本。
还有无文本冲突的语义冲突。假设 T1 把正文上限改为 9,M1 在另一个文件把 10 字节正文加入“成功”用例;两处文本不重叠,Git 可能自动合并,却留下实现与测试、或 API 与调用方互相矛盾。合并成功只是“构成了一个文件树”,还要从需求与回归矩阵判断它是否满足业务合同。
五、冲突解决:先确认业务,再编辑文件
以下步骤仅供学习者在自己新建、无未提交工作的练习仓库中演练。若仓库里已有重要未提交修改,应先整理和保存,再开始合并;不要把冲突演习放进课程仓库或工作仓库。
1. git status:确认正处于合并,以及哪些路径冲突。
2. 读共同祖先和两侧差异:谁改了哪条规则、依据是什么。
3. 明确最终合同:R9、R12 的优先级与生效范围;不明则停止决策并澄清。
4. 手工编辑冲突文件:留下完整且自洽的目标内容,去掉全部标记。
5. 核对关联测试、帮助与文档;git add <已解决路径> 标记解决。
6. 再看 git status 和待提交差异;按 Git 状态完成合并提交或继续。
7. 对新合成的 HEAD 重新做相关验证,记录真实结果与未覆盖范围。Git 当前支持 git merge --continue 来继续已解决的合并;也可按提示完成合并提交。若本次合并不该继续,git merge --abort 尝试回到合并前状态。它不应成为清理任意未提交工作的借口:开始前保持工作区清楚,必要时先在个人练习仓库确认状态。尤其不要为了“冲突太多”随手执行 git reset --hard;那可能丢掉尚未记录的修改。
若 R9 最终被批准、R12 尚未被批准,结果就应表达上限 9,并补 6/9/10 字节边界。若 R12 也已批准且生效顺序在后,应先说明是否用 12 取代 9,再修改期望。若需求要求不同会话有不同上限,则单个整数已不足以表达规则,需要另立功能设计;不能把 9 和 12 取平均。无论选哪种,都要保留种类优先级、重复 ID 不替换、文件大小与 CLI 合同。
解决文件冲突后还要核对相邻但未报冲突的变更:list 顺序、错误包装、messages 的 nil 检查、导出目标保护以及默认不打印正文。学习者的实际验证应记录合并后提交身份、命令、预期、实际和文件前后状态;课程里的表是待执行清单,不是已通过报告。
六、merge 与 rebase 改变了哪段历史
merge 将两侧历史在合并点联系起来,保留原来的提交身份;rebase 把主题分支上的提交逐个重放到新的基底上,生成新的提交身份,即使最终文件内容看起来相同。两者都可能遇到冲突,都不能取代业务审查。
合并前: B0──M1 main
└──T1 topic
merge 后: B0──M1──G main (G 同时关联 M1 与 T1)
└──T1──┘
rebase 后:B0──M1──T1' topic (T1' 是重放得到的新提交)图中是典型形状,不代表每次命令都恰好只有一个主题提交。对尚未与他人共享的个人主题分支,rebase 可以让改动建立在更新的基底上;若别人已经基于原 T1 工作,重写其历史会让协作变复杂,应先协商。合并共享历史时,保留原提交身份通常更容易追溯。团队也可能使用 squash 合并,把多个主题提交合成一个新提交;这样改变审查粒度和历史形状,应明确取舍。
rebase 发生冲突时,按 git status 指示编辑、git add,再 git rebase --continue;若决定放弃此次重放,用 git rebase --abort。git rebase --skip 会跳过正在重放的提交,可能直接丢掉需要的业务改动;初学者不应把它当成“解决冲突”。在 Git 托管平台上创建 PR、得到审批、推送分支、执行 merge/rebase、发布程序是不同事件,不可互相推断。
七、小提交、审查通过与合并后回归
10.05 的重构适合按“提取纯校验 → 移动 v1 文件适配 → 核对 CLI 合同”分成可解释步骤。R9 是行为变更,另起需求、预期和提交。不是机械规定“一函数一提交”:一次提交应有清晰意图,让审阅者能说明为什么改、如何核对、怎样单独撤销或修正。改名、移动代码和改变长度比较符混在同一巨大差异里,会让审阅者很难发现 >= 的等号错误。
合并后的回归清单应针对新组合状态,不只复述两个分支各自的绿灯:
| 风险 | 合并后应核对的结果 |
|---|---|
| 正文上限 R9 | 6、9 字节接受;10 字节拒绝;未知种类优先 |
| 消息状态 | 重复 ID 不替换旧消息,拒绝后不产生成功返回 |
| v1 文件 | messages:[] 合法;缺失、null、未知字段和尾随值拒绝 |
| CLI | check/list/export、stdout/stderr、退出 0/2/3/4/5 保持合同 |
| 输出与写入 | list 保持文件顺序和正文脱敏;目标已存在不覆盖 |
审阅者可以判断代码和证据是否充分;学习者执行测试才能记录某次环境里的实际结果。推送只是把引用和对象传到远端,合并是把两条开发历史结合,发布才涉及用户运行的制品。即使当前项目配置了自动检查,也必须问这些检查覆盖了哪条需求和哪个版本。
八、完整审查练习与反馈
学习者可以在一个新建的个人练习目录,仿照 10.03 建立独立仓库,分别做重构提交和 R9 提交。先在纸上推演,再自行执行、记录输出,避免直接在课程仓库模拟冲突。最小交付物包括:R9 决策表、差异审查意见、共同祖先图、冲突解决依据、合并后回归记录和未覆盖范围。
分层练习:先回答,再核对反馈
1. `git diff` 默认看哪两个位置?
工作区与暂存区;它显示尚未暂存的改动。
2. `git diff --cached` 的用途是什么?
检查索引与当前提交的差异,即下一次普通提交准备记录的内容。
3. 为什么还要看整条主题分支差异?
单个提交可能只是一小步;合并评审需要看所有主题提交组合后的净变化。
4. `git diff main...topic` 从哪里开始比较?
从两分支的共同祖先到 topic 端点,适合看主题分支带来的内容变化。
5. `git diff --check` 通过证明业务规则正确吗?
不能。它只检查部分格式问题;仍需独立需求和边界证据。
6. 正文上限从 6 改 9,会连带改变 `-max-bytes` 吗?
不会。前者是单条正文教学规则,后者是整份文件的读取资源限制。
7. `len(body) >= 9` 为什么不符合 R9?
它拒绝恰好 9 字节;R9 规定等于上限合法,应在 len(body) > 9 时拒绝。
8. 审查意见怎样比“这里有问题”更有效?
给出位置、具体输入、规则依据和预期,例如 9 字节 你好呀 应通过。
9. PR 是 Git 的提交对象吗?
不是。PR 是托管平台的协作记录;Git 的提交与分支可以独立存在。
10. 有审批就能宣称所有边界已验证吗?
不能。审批针对一定范围,需另查实际测试、运行环境和未覆盖风险。
11. 三方合并是哪三份状态?
共同祖先、当前分支端点和待并入分支端点。
12. 两边都把同一行从 6 改成不同值,Git 能决定哪份需求更优先吗?
不能。文本冲突要追查 R9、R12 的批准和生效范围。
13. 没有冲突标记就没有业务冲突吗?
不是。不同文件中的实现和测试可能语义矛盾,Git 仍能自动合并文本。
14. 为什么解决冲突后还要检查文件前后状态?
改动可能影响导出覆盖、失败后的原文件或部分输出,这些不能从冲突标记消失推出。
15. `git add` 冲突文件表示什么?
把编辑后的解决结果放入索引,告诉 Git 此路径已处理;它不证明业务结果正确。
16. `git merge --abort` 的作用是什么?
尝试撤销正在进行的合并并回到合并前状态;开始合并前应先整理工作区。
17. merge 和 rebase 对原提交身份的区别是什么?
普通合并保留两侧原提交;rebase 重放主题提交并生成新提交身份。
18. 为什么不能随手 rebase 他人正基于其开发的共享分支?
它会改写提交身份,使他人的旧历史与新历史分叉,增加整合成本。
19. `git rebase --skip` 为什么危险?
它跳过当前正在重放的提交,可能遗漏这次需求的改动。
20. 合并后只看两个分支各自的测试结果够吗?
不够。新 HEAD 是两组变更的组合,还要核对组合后的业务合同。
21. 推送成功等于代码已发布给用户吗?
不等于。推送、合并、制品构建和发布是不同步骤。
22. 本章静态冲突示例能证明远端 IM 送达吗?
不能。本章只讨论虚构本地规则与协作流程,远端行为需要独立系统证据。
来源与下一步
- Pro Git:基础分支与合并和变基:提交图、合并与历史重放。
- Git
merge文档和rebase文档:冲突继续、撤销与跳过的命令边界。 - GitHub Pull Request Review 文档:平台上的评审概念;课程不假定当前仓库启用了特定审批规则。
S1 的本地工具主线到这里具备需求、实现边界、测试、重构和协作说明。下一阶段先读03.02 进程与系统调用,再按学习路线补线程、网络与同步前置,逐步扩展到在线 IM。