
前阵子复盘了一次印象深刻的线上事故一个支付金额计算的改动代码评审记录里显示两位同事都点了 approve可上线后还是出了问题。事后看 diff问题其实很明显——金额精度在小数点后第三位被截断了。为什么这么明显的问题没人发现因为那次 review评审人实际上只花了两分钟对着 500 多行的 diff 重点扫了一圈点了个批准按钮就算完事。这不是两个人的问题是流程本身出了问题。这个项目叫 open-code-review它不是商业 SaaS也不是又一个塞满 GitHub 插件的网红工具而是我花了两个周末为了解决代码审查沦为形式这件事设计的一套开源工作流。核心思路极其朴素不搭 Web 服务器不搞数据库完全基于 Git 原生的能力加一套文件约定配一个叫ocr的命令行工具让一次代码审查从发起、批注、讨论到归档全部发生在仓库本身里。整个过程不离开终端和编辑器没有额外的网页平台不需要打开另一个网址去查一下 review 状态。如果你正在维护一个小团队或者想在个人项目里建立一种更轻的复查机制又或者对大平台上那套僵硬的 MR 流程已经忍无可忍这篇内容应该能给你一些可以直接抄走的方案。1. 代码审查这件事为什么在很多团队里成了摆设1.1 一次假 Review引发的线上事故先说回开头那个事故。那个改动本身并不复杂把订单金额从分改成厘也就是把整数单位改成长整数单位。问题出在一个边界条件商品单价在数据库中存的是分前端传入的金额是厘转换时有一个除以 10 还是乘以 10 的逻辑没处理好导致部分订单在特定折扣组合下被截断。理论上这种问题在 code review 阶段应该被拦下来。但实际情况是这个 MR 包含了 12 个文件、500 多行改动其中夹杂了大量格式化改动和无关重构。评审人打开页面时先看到的是满屏的代码风格变动真正核心的业务逻辑只有那么一小段被淹没在噪声里。他点开那个文件看了一会儿习惯性地留下了 LGTM然后关闭页面。我想说的是如果评审一个 500 行 diff 平均只需要两分钟那这个评审的质量约等于零。人的工作记忆是有限的一次 review 能关注的代码量是有上限的。当 diff 规模超出这个上限评审就不可避免地从深入理解退化成扫视确认。这条规律不会因为你使用了某个昂贵的工具或者严格的流程就改变除非你从机制上强制让评审者面对更小、更聚焦的变更。1.2 传统 Review 流程的三个致命伤传统基于 Web 平台的 code review 流程问题通常集中在三个方面。入口负担重提交一个 MR要填标题、描述、关联的 ticket、测试计划、影响范围模板长得像入职登记表。很多人填到一半就烦了随便写两句后面的环节也跟着应付。评审负担重一个大 MR 挂在列表里评审人面对的是一个巨大的 diff没有上下文、没有拆解、没有引导。他不知道该把注意力放在哪里很容易陷入全程点头的状态。反馈回路断裂评审意见散落在网页上开发者在编辑器里改代码两边软件互不相通。一个 review 问题从提出到解决可能需要切换三四次窗口沟通成本远超问题本身。我把这三个问题梳理清楚后当时的第一反应是换一个更智能的工具比如带 AI 辅助、自动分配 Reviewer 的平台。但后来想明白了一件事工具能解决的是意见收集效率解决不了评审者愿意认真看代码这个根本问题。当一个流程设计得反人性换工具只是在表面止损真正该改的是流程本身的形态。1.3 我期望的流程像看评论一样自然我的目标很清楚让代码审查的整个过程发生在开发者的日常操作路径里不额外增加操作负担。具体说就是开发者提交了一个分支他把分支推到远端然后写一个非常轻量的评审请求文件里面告诉他这次改了什么、重点在哪里、哪些地方他希望别人帮忙看。评审人不需要打开任何网页只需要在 Git 仓库里执行一条命令或者干脆在编辑器里打开那个请求文件就能看到全部信息。他把意见直接写在文件旁边标注 BLOCK 或 NIT开发者看到后修改代码再更新结果。听起来很简单对吧但就是这么简单的事大多数 Web 平台反而做不好。因为它们时刻在引导你去关注平台的流程而不是关注代码本身。open-code-review 做的第一步就是把这个颠倒的秩序掰回来。2. open-code-review 的设计思路把审查门槛降到最低2.1 核心原则不改变开发者的日常工作习惯我在设计 open-code-review 时给自己定的第一条原则是绝对不引入新的工作场景。很多 review 工具的问题是它需要你去某个地方才能完成 review可能是网页、桌面应用或者一个新的 IDE 插件。这产生的隐性成本是我之前低估的开发者对于切换场景这件事非常敏感。当一个人的工作流是终端——编辑器——终端的闭环时任何让他额外去一个网页的操作都会被视为一种打断。所以 open-code-review 选择了纯 Git 原生的机制。所有 review 相关的文件都放在仓库里的一个目录下比如.ocr/requests/review-id/。你创建评审请求就是创建一个文件并提交你给出意见就是往这个目录里加一个文件你通过评审就是创建一个批准标记文件。整个过程完全就是一个普通的 Git 操作流程没有任何网络请求也不需要任何后台服务。第一次跑通这个流程时我最直观的感受是以前我在高铁上、在没有稳定网络的环境下也能安静地做一个 review因为所有上下文都在本地仓库里。这种体验是任何浏览器端方案都给不了的。2.2 数据模型一次 Review 请求由哪些元素构成open-code-review 的数据模型非常轻量一个 review 请求对应一个目录目录下固定放几个文件.ocr/ └── requests/ └── 0001-init-payment-rounding/ ├── request.md # 评审请求说明由发起人创建 ├── comments/ # 评审意见目录评审人在这里添加文件 │ ├── 0001-BLOCK-amount-rounding.md │ └── 0002-NIT-rename-variable.md └── approved.ok # 通过标记评审通过后由发起人写入request.md是评审请求的核心它用 Markdown 写由发起人填写。里面包含四个部分变更主题、代码位置、变更摘要、以及一个可选的请重点关注列表。字段说明是否必填title这次 review 要解决什么问题是source_branch功能分支名是target_branch合并目标分支默认main是summary变更的核心逻辑和影响面是focus希望评审者特别留意的点否为什么这么设计因为我在实际观察中发现请重点看什么这句话的价值被严重低估了。很多时候评审低效不是因为评审人能力不行而是因为他不知道发起人心里最没底的地方在哪里。focus字段就是用来把这个信息显式化的它让评审人从第一秒就带着问题去看代码而不是漫无目的地通读。2.3 为什么选择 Git 原生能力而不是搭一套 Web 服务这是我在设计过程中做过一次比较痛苦的取舍。最初的想法很简单写一个 Web 应用用户登录、创建 review、添加评论再配一个 CI 检查来校验审批状态。听起来很完整但实现完第一个可运行原型后我发现它违背了自己的初衷。一个 Web 服务意味着一台需要维护的服务器、一套认证体系、一套数据持久化方案、一套前端页面。这意味着任何想用的人都需要先解决部署和维护的问题。小团队没有这个精力个人项目更不可能。而选择 Git 原生能力成本低到令人发指零部署不需要服务器仓库本身就是数据库零维护没有进程守护没有数据备份Git 已经替你做了版本管理离线可用所有数据都在本地仓库断网也能 review可审计谁在什么时候写了什么意见都是 Git 提交历史的一部分从功能对比来看open-code-review 相比主流工具缺少的是实时通知和可视化面板但换来的是极低的进入门槛和高度的可控性。对中小团队来说我认为这是一笔划算的买卖。2.4ocr命令的设计逻辑有了文件目录和约定剩下的事情就是把常见操作封装成简短的命令。ocr是目前实现的命令行工具主要命令和用途如下命令作用说明ocr start发起评审请求生成评审请求目录和request.md模板ocr list列出所有未归档的评审请求过滤approved.ok不存在的请求ocr view id查看某个评审请求的完整内容聚合展示request.md和所有评论ocr comment id添加评审意见按模板生成一个意见文件ocr ok id标记评审通过校验无全局 BLOCK 意见后写入approved.okocr exec id合并分支并归档执行 merge 后把评审目录移动到archived/这些命令本身并非高深技术但它们刻意追求一个动作对应一个高频操作。简单、直白、不容易出错比任何花哨的交互都更愿意让人持续使用。3. 手把手搭建从仓库初始化到第一个评审单3.1 分支结构设计open-code-review 的分支结构很简单默认只有一个长期分支main开发者在review/*前缀下创建功能分支。比如review/fix-payment-rounding。为什么用review/前缀因为 CI 脚本可以通过前缀清晰地区分普通开发分支和待评审分支。后续如果要做自动化检查只需要扫描所有以review/开头的远端分支不需要额外的分支登记逻辑也不会误伤其他类型的分支。3.2 初始化与发起一次 Review 请求假设你正在review/fix-payment-rounding分支上开发功能做完准备发起 review。先提交好代码然后执行ocr start这条命令在底层做了三件事解析当前分支名提取 review 名称创建.ocr/requests/branch-name/request.md预填标题、分支名等信息打开编辑器让你填写summary和focus拿我最初实现的脚本举例核心逻辑大致是这样的#!/usr/bin/env bash BRANCH_NAME$(git rev-parse --abbrev-ref HEAD) REVIEW_DIR.ocr/requests/${BRANCH_NAME} REVIEW_ID$(basename $BRANCH_NAME) mkdir -p $REVIEW_DIR/comments cat $REVIEW_DIR/request.md EOF --- title: 待填写本次评审主题 source_branch: $BRANCH_NAME target_branch: main created_at: $(date -u %Y-%m-%dT%H:%M:%SZ) --- ## Summary 用一两段话说明这次改了什么、为什么这么改 ## Focus 列出希望 Reviewer 重点关注的区域或问题 EOF git add $REVIEW_DIR git commit -m ocr: 发起评审请求 $REVIEW_ID git push -u origin $BRANCH_NAME这里有一个容易被忽视的细节request.md里的created_at用了 UTC 时间而不是本地时间。原因是开发团队分布在多个时区或者有远程协作者统一用 UTC 可以避免在统计评审周期时出现时间差混乱。这个小细节在之后的评审数据分析里节省了很多不必要的麻烦。3.3 Reviewer 怎么批注三种意见格式评审人拿到评审请求后执行ocr view id查看上下文然后执行ocr comment id添加一条意见。意见文件用编号加类型作为前缀格式约定如下--- type: BLOCK file: src/payment/rounding.go line: 47 --- 这里直接对 int64 做截断会丢失折扣场景下的小数位。 建议使用 math/big 或调整转换公式。意见类型一共有三种含义非常明确类型含义动作BLOCK存在必须修复的问题不修复不能合并发起人必须处理并回复NIT非阻塞的小建议可修可不修发起人自行判断QUESTION对逻辑不明确需要解释发起人回复解释即可为什么要刻意把意见分为这三类我先说一个反面教训在传统 MR 流程里评审人的每一条评论都是平等的导致开发者面对 30 条评论时不知道哪些必须改、哪些只是意见。这种扁平化的信息结构给了开发者全盘敷衍的空间。而有了 BLOCK 作为唯一硬性门槛发起人可以快速过滤出最需要处理的意见评审人也被迫明确表达我不同意和我无所谓的区别。这套机制在团队里跑了一段时间后我发现一个明显变化有些习惯性刷存在感的评审者开始克制了因为每条 BLOCK 都是要有后续的不能再随手点个must fix。3.4 验收与归档流程当发起人处理完所有 BLOCK 意见后执行ocr ok id工具会做三件校验检查comments/目录下是否存在任何BLOCK-*类型的意见未标记 resolved检查request.md是否存在检查当前分支是否与target_branch完全同步避免合并时产生冲突校验不通过approved.ok不会被写入。通过后CI 会检测到这个文件并允许合并。合并时执行ocr exec id它内部会做git merge和归档#!/usr/bin/env bash REVIEW_DIR.ocr/requests/${REVIEW_ID} TARGET_BRANCH$(grep ^target_branch: $REVIEW_DIR/request.md | awk {print $2}) git checkout $TARGET_BRANCH git merge --no-ff review/${REVIEW_ID} -m merge: ${REVIEW_ID} (ocr) git push origin $TARGET_BRANCH mkdir -p .ocr/archive git mv $REVIEW_DIR .ocr/archive/${REVIEW_ID}-$(date %Y%m%d) git commit -m ocr: 归档评审请求 ${REVIEW_ID} git push origin $TARGET_BRANCH这个流程走完之后所有评审请求都有清晰的终态要么归档在.ocr/archive/下要么作为历史提交记录留在 Git 日志中。任何时候想回顾某次评审的来龙去脉一条git show就能看到全部内容。4. 与 CI/CD 集成让审查结果自动卡住合并4.1 一个 CI 检查脚本的核心实现流程设计得再好如果没有自动化强制保障也会逐渐被绕过去。open-code-review 的 CI 检查脚本是整个流程的守门员。脚本的逻辑不复杂核心步骤是这几步#!/usr/bin/env bash set -euo pipefail HEAD_REF${GITHUB_HEAD_REF:-$(git rev-parse --abbrev-ref HEAD)} BASE_REFmain # 切换回目标分支找出变更范围内的评审目录 git fetch origin $BASE_REF --depth1 REVIEW_DIR.ocr/requests/${HEAD_REF} if [ ! -f $REVIEW_DIR/request.md ]; then echo ❌ 未找到评审请求文件: $REVIEW_DIR/request.md exit 1 fi # 检查是否存在未解决的 BLOCK 意见 UNRESOLVED_BLOCKS$(find $REVIEW_DIR/comments -name BLOCK-* ! -name *.resolved.md | wc -l | tr -d ) if [ $UNRESOLVED_BLOCKS -ne 0 ]; then echo 存在 $UNRESOLVED_BLOCKS 条未解决的 BLOCK 意见请先处理。 exit 1 fi # 检查 approved.ok 是否存在 if [ ! -f $REVIEW_DIR/approved.ok ]; then echo 评审未通过未找到 approved.ok exit 1 fi echo ✅ 代码评审已通过这个脚本的关键设计在于它把存在性作为检查依据而不是去解析文件内容再判断是否准备就绪。文件在就是通过文件不在就是没通过。简单到任何人都能审计逻辑。4.2 阈值如何定三个经验值脚本本身不复杂但哪些参数需要被重点关注阈值设多少是一个值得认真考虑的问题。我从实际使用中沉淀下来三个经验值diff 行数阈值当一次评审请求的 diff 总行数超过 400 行CI 会输出一个红色警告但不直接拦截。这个 400 并非拍脑袋而是结合我的实际观察超过这个规模后评审人往往无法在合理时间内完成深入理解。它的作用是提醒发起人自己评估是这次变更本身就很大还是混入了不应该有的重构如果把格式化变更单独拆一个 commit核心逻辑控制在 200 行以内评审质量大概率会提升一个台阶。评审超时阈值我设置为 48 小时。超过 48 小时没有人发表任何评审意见CI 会在日志里标记review timeout但这需要结合消息推送把超时事件推给相关人。这个值取决于团队节奏单周迭代制团队可以设定为 24 小时双周冲刺的可以放宽到 72 小时。BLOCK 数量阈值硬性设为 0。只要存在未解决的 BLOCK就绝对不允许合并。这个没有商量余地。因为一旦允许小阻塞先合并后续再修本质上就回到了假 review的路径上。4.3 机器注意到的人眼盲区Diff 上下文收敛这是我在 CI 集成中踩得最深的坑之一统计 diff 行数时如果依赖git diff --stat它会把每个文件的首部上下文和尾部噪音都算进去导致一个只有 3 行有效改动的变更被统计成 120 行的 diff进而触发无谓的警告。后来我调整了统计方式只看新增行数和资源文件的净变化git diff --numstat $BASE_REF...$HEAD_REF | awk { added $1; deleted $2 } END { print added:, added, deleted:, deleted, total:, added deleted }另外还有一个非常实际的问题自动生成的配置文件、锁文件的 diff 会干扰统计。如果项目里有package-lock.json、go.sum这类文件我采用的做法是单独包一层判断把它们从 diff 阈值计算中排除掉。这样做不是要忽略它们而是因为这类文件的改动模式无法通过人类通读来评审应该交给依赖安全检查工具去处理不需要占用评审者宝贵的注意力。4.4 在 GitHub Actions 里跑起来配置文件的写法并不特殊但有一个坑值得专门写出来actions/checkoutv4默认只会拉取单次提交的浅层仓库而 open-code-review 的检查脚本需要对比main和当前分支之间的 diff浅层仓库会导致找不到祖先 commit。需要在 actions 里显式设置fetch-depth: 0name: open-code-review-check on: pull_request: types: [opened, synchronize, reopened] jobs: code-review-check: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Run open-code-review check run: | chmod x ./scripts/ocr-ci-check.sh ./scripts/ocr-ci-check.sh如果你的开发环境不是 GitHub而是 GitLab CI 或者 Gitea逻辑完全一致先保证完整克隆再执行脚本。工具不绑定具体平台因为它的核心是仓库内的文件约定CI 只是负责在特定时机执行一次检查而已。5. 团队落地与推行踩过的坑和想通的事5.1 第一次推行失败的复盘最初我把 open-code-review 拿给团队试用时预期很乐观不需要装新软件、不需要学新平台成本这么低大家应该会喜欢。结果是两周后用的人只剩我自己。有同事给了很直接的反馈我知道它挺轻的但问题是我打开自己的 MR 时如果它不在我本来要用的那个系统里我就会忘记它。这个反馈点醒了我工具本身足够轻并不代表使用率就高关键是它必须嵌入一个已经存在的流程而不是成为又一个独立流程节点。于是我换了一种推行思路。不要求大家主动去创建 review 请求而是把ocr start放到一个 hook 里。一旦开发者向review/*分支推送代码Git pre-push hook 自动初始化一份request.md模板。开发者只需要填摘要和 focus减少了从零开始的心理负担。这个小小的变化让参与度出现了实质性的提升。5.2 移动端团队的实践证明描述比 Review 人更重要在移动端团队实践时我观察到了一个意外收获评审的质量很大程度上取决于发起人写的request.md写得好不好。以前在 Web MR 流程中很多人填写描述纯粹是为了应付模板两句话搞定。到了 open-code-review 的体系里因为没有平台自动汇总 commit 信息发起人必须主动梳理变更脉络。这个想清楚再写下来的过程本身就促使他重新审视自己的代码。我做过一个对比用 open-code-review 评审的 12 次请求中凡是focus字段写得很具体的评审意见的平均数量和质量都远高于字段为空的情况。具体到行动建议focus字段不要写请检查边界条件这种空话要写具体文件和场景比如请重点关注src/payment/rounding.go第 45 行在折扣场景下的取整逻辑。这种写法评审者在看到代码之前就已经知道自己该看哪里。5.3 用推送机器人解决评审超时问题轻量流程有一个天生的短板没有平台自带的站内信通知容易把评审晾在那里没人理。这个问题不解决超时就只是 CI 日志里一行字而已。我的方案是写一个简单的定时脚本扫描所有未归档的评审请求如果超过 24 小时没有新评论就把请求信息推送到团队的 IM 群里。推送内容只包含三行评审 ID、标题、距离上次响应的时间。这里用到的技术极其简单核心就是调用 Webhook#!/usr/bin/env bash STALE_REVIEWS$(find .ocr/requests -name request.md -mtime 1) for review in $STALE_REVIEWS; do REVIEW_ID$(basename $(dirname $review)) curl -s -X POST \ -H Content-Type: application/json \ -d {\text\:\⚠️ 评审超时提醒: $REVIEW_ID请相关同事尽快处理\} \ $DINGTALK_WEBHOOK_URL done把脚本挂到 cron 里每两小时跑一次。效果立竿见影评审平均响应时间从没人记得降到了第二天上午基本处理完。这件事给我最大的启发是轻量流程需要配合主动的触达手段不能让参与者自己去记着。5.4 用脚本做评审周期复盘流程跑起来后我开始关注一个指标从发起评审到最终合并平均要多久。这个数字能反映团队的 review 效率。具体脚本是这样做的#!/usr/bin/env bash for dir in .ocr/archive/*; do if [ -f $dir/request.md ]; then created_line$(grep ^created_at: $dir/request.md | awk {print $2}) approved_line$(git log --prettyformat:%aI -1 -- $dir/approved.ok) echo $dir|$created_line|$approved_line fi done我拿这个脚本跑了一遍团队近两个月的评审数据结果很能说明问题所有成功合并的评审平均周期是 19 个小时。最慢的是周一上午发起的请求因为大家都在忙别的平均要拖 30 小时。于是我们把周一尽量不发新评审请求写进了团队约定。一个小小的流程调整之后几周的评审周期数据明显更好看了。复盘这件事的意义不在于追求一个好看的数字而是让团队看到流程到底在哪里卡住了。当你发现某个环节反复拖慢整体节奏那就该改的是那个环节而不是优化其他无关的部分。工具跑顺之后我最大的感受是代码审查这件事效率不取决于工具花了多少万行代码实现而取决于它有没有降低说真话的门槛、有没有让被审查的人少一点应付的感觉。open-code-review 没有什么玄妙的技术它只是把认真看一遍代码这件事变得足够简单让参与的人愿意去做而已。如果你想在团队里实践这套思路建议从一个小项目开始先跑通一次完整的评审再慢慢扩大范围。方向上不需要大动干戈把入口变得足够轻自然有人愿意走进去。