1. 从“代码评审”四个字说起:为什么我又造了一个轮子
第一次听到“open-code-review”这个说法,很多人脑子里蹦出来的第一反应是:代码评审这事不是早就被各种平台做烂了吗?提交合并请求、拉人审批、留评论、打回重改,流程闭环得不能再闭环。我一开始也是这么想的,直到我在三个不同规模的项目里连续踩了同一类坑,才意识到问题根本不在“有没有评审流程”,而在于“评审这件事到底有没有真正发生”。
先把这个项目的定位说清楚。open-code-review 不是某个具体产品的名字,它更像是一类做法的统称:把代码评审从“平台内置的一个按钮”里拆出来,变成一套开放、可自托管、可定制、不绑定任何单一代码托管服务的评审机制。它的核心诉求很朴素——让评审回归到“人看代码、人给意见、人做判断”这件事本身,而不是被平台的流程设计牵着走。
我见过太多团队,合并请求开得飞快,审批点得飞快,评论区一片“LGTM”,结果上线之后问题照样出。你回头去翻那次评审记录,会发现根本没人真正读过那段逻辑。评审变成了一个仪式,一个为了满足流程合规而存在的动作。open-code-review 想解决的,就是这种“评审空转”的问题。
这篇文章适合谁看?如果你是团队里负责代码质量的那个人,如果你正在为“评审流于形式”发愁,如果你所在的环境对数据自主可控有要求、不想把代码评审的全部痕迹都放在别人的服务器上,那这篇内容会对你有用。我会从评审机制的设计、工具链的搭建、实际跑起来之后的坑,一直讲到怎么让评审真正产生价值,而不是变成又一个打卡任务。
需要提前说明的是,下面提到的所有工具、脚本、配置,都是基于常见工程实践给出的合理方案,你可以直接抄,也可以按自己的情况改。我尽量把“为什么这么选”讲透,因为评审这件事,选型背后的逻辑比选型本身重要得多。
2. 评审机制到底该由谁定义:平台内置流程的三个隐性代价
2.1 流程被平台绑架之后,团队失去了什么
大多数团队一开始用的都是代码托管平台自带的评审流程。这东西上手快,开箱即用,没什么好挑剔的。但用得越久,你越会发现有些东西在悄悄流失。
第一个代价是评审规则的僵化。平台给你的是“提交—指派—审批—合并”这条标准链路,你想加点自己的规则,比如“涉及核心模块的改动必须由两个人分别从安全和性能两个角度审”,平台要么不支持,要么得升级到很贵的套餐。你只能把这种需求塞进人的记忆里,而人的记忆是靠不住的。
第二个代价是评审数据的封闭。你所有的评审记录、评论、审批时间线,都躺在平台的数据库里。你想做个统计,看看哪个模块的评审最容易出问题、哪个评审人的意见最有价值,对不起,导出功能有限,API 还限流。数据不在你手里,你就没法基于数据做改进。
第三个代价最隐蔽,也最要命:评审的上下文被切碎了。平台只呈现 diff,也就是改动的那几行。但真正懂代码的人都知道,一段改动是否合理,往往取决于它周围的代码、它依赖的模块、它违背或遵循的既有约定。只看 diff 做评审,就像只看一个人的简历就决定要不要跟他合作,信息量远远不够。
open-code-review 的思路,就是把这些被平台拿走的东西还回来。评审规则由团队自己定,评审数据存在自己能控制的地方,评审的上下文由工具去补全,而不是靠评审人自己脑补。
2.2 自托管评审的边界:哪些该自己管,哪些别硬扛
说到这里必须泼一盆冷水。自托管不等于什么都自己造。我见过一些团队,一听说“自主可控”,恨不得连 Git 服务器都自己搭,结果运维成本高得吓人,最后不了了之。
我的经验是,评审这件事可以拆成三层:代码存储层、评审交互层、评审数据层。代码存储层,也就是 Git 仓库本身,用现成的托管服务完全没问题,没必要自己扛。评审交互层,也就是“在哪儿看 diff、在哪儿留评论”,这是 open-code-review 要自己管的核心。评审数据层,也就是评论、审批记录、统计报表,这个必须自己控制,因为它是改进评审质量的依据。
所以一个务实的自托管评审方案,通常是这样的:代码还在原来的托管平台上,但评审的交互和数据走一套自己部署的服务。评审人通过自己的服务看代码、留意见,意见再同步回平台或者直接作为合并的依据。这样既拿到了自主权,又没有把运维复杂度拉满。
提示:自托管评审服务的第一原则是“别让评审人多干活”。如果评审人为了用你的系统,需要多登录一次、多复制一次链接、多切换一个窗口,那这套系统注定推不动。降低摩擦比功能强大重要一百倍。
3. 把评审从平台里拆出来:一套可落地的自托管方案
3.1 整体架构:三个组件加一条数据流
我实际跑通的方案,核心就三个组件。第一个是评审服务端,负责接收代码改动、生成评审任务、存储评论和审批状态。第二个是评审客户端,可以是一个网页界面,也可以是一个命令行工具,评审人用它来看代码、写意见。第三个是同步器,负责把评审结果同步回代码托管平台,或者在平台侧触发合并。
数据流是这样的:开发者提交代码到托管平台,平台的 webhook 触发同步器,同步器把改动拉下来,推给评审服务端,服务端生成评审任务并通知评审人。评审人在客户端完成评审,意见存回服务端,同步器再把审批结果写回平台。整个过程里,托管平台只负责存代码和最终合并,评审的全部逻辑都在自己的服务里。
这套架构的好处是解耦。哪天你想换托管平台,评审这套东西完全不用动。哪天你想改评审规则,也不用看平台的脸色。
3.2 评审服务端的选型:为什么我最终选了轻量方案
评审服务端我试过三种路子。第一种是直接用现成的开源评审工具,功能全,但配置复杂,跟自己的托管平台对接要写不少胶水代码。第二种是自己从零写,灵活是灵活,但工作量太大,而且容易在细节上翻车。第三种是找一个轻量的 Web 框架,自己搭一个最小可用的评审服务。
我最后选了第三种。原因很简单:评审这件事的核心逻辑并不复杂,无非是“存改动、存评论、存状态”这三件事。用一个轻量框架,几百行代码就能跑起来,而且完全可控。我用的是 Python 的 FastAPI,数据库用 SQLite 起步,后面量大了再换 PostgreSQL。下面是一个最简的服务端骨架,你可以直接拿去改。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List, Optional import sqlite3 import json app = FastAPI() def init_db(): conn = sqlite3.connect("review.db") conn.execute(""" CREATE TABLE IF NOT EXISTS reviews ( id INTEGER PRIMARY KEY AUTOINCREMENT, repo TEXT NOT NULL, commit_hash TEXT NOT NULL, diff_content TEXT NOT NULL, status TEXT DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, review_id INTEGER NOT NULL, file_path TEXT NOT NULL, line_number INTEGER, author TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (review_id) REFERENCES reviews(id) ) """) conn.commit() conn.close() class ReviewCreate(BaseModel): repo: str commit_hash: str diff_content: str class CommentCreate(BaseModel): review_id: int file_path: str line_number: Optional[int] = None author: str content: str @app.post("/reviews") def create_review(review: ReviewCreate): conn = sqlite3.connect("review.db") cursor = conn.execute( "INSERT INTO reviews (repo, commit_hash, diff_content) VALUES (?, ?, ?)", (review.repo, review.commit_hash, review.diff_content) ) review_id = cursor.lastrowid conn.commit() conn.close() return {"review_id": review_id, "status": "pending"} @app.post("/comments") def add_comment(comment: CommentCreate): conn = sqlite3.connect("review.db") conn.execute( "INSERT INTO comments (review_id, file_path, line_number, author, content) VALUES (?, ?, ?, ?, ?)", (comment.review_id, comment.file_path, comment.line_number, comment.author, comment.content) ) conn.commit() conn.close() return {"status": "ok"} @app.get("/reviews/{review_id}") def get_review(review_id: int): conn = sqlite3.connect("review.db") review = conn.execute("SELECT * FROM reviews WHERE id = ?", (review_id,)).fetchone() if not review: conn.close() raise HTTPException(status_code=404, detail="Review not found") comments = conn.execute("SELECT * FROM comments WHERE review_id = ?", (review_id,)).fetchall() conn.close() return { "review": { "id": review[0], "repo": review[1], "commit_hash": review[2], "status": review[4], "created_at": review[5] }, "comments": [ { "id": c[0], "file_path": c[2], "line_number": c[3], "author": c[4], "content": c[5], "created_at": c[6] } for c in comments ] } if __name__ == "__main__": init_db() import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)这段代码跑起来,你就有了一个能创建评审任务、能加评论、能查评审详情的最小服务。它当然不完整,比如没有鉴权、没有通知、没有和托管平台的对接,但核心骨架已经在了。剩下的都是在这个骨架上加东西。
3.3 评审客户端:命令行还是网页,这是个问题
评审客户端的选择,直接决定了评审人愿不愿意用。我一开始做了个网页界面,想着可视化总比命令行友好。结果发现,真正高频的评审人,也就是那些每天要看好几个改动的人,反而更愿意用命令行。因为命令行不用切窗口,不用等页面加载,看完直接写意见,效率高得多。
所以后来我改成了双客户端:网页界面给偶尔评审的人用,命令行工具给高频评审的人用。命令行工具的核心命令就三个:拉取待评审列表、查看某个改动的详情、提交评论。下面是一个用 Python 写的命令行客户端示例。
import requests import sys import os API_BASE = os.environ.get("REVIEW_API", "http://localhost:8000") def list_pending(): resp = requests.get(f"{API_BASE}/reviews?status=pending") reviews = resp.json() for r in reviews: print(f"[{r['id']}] {r['repo']} @ {r['commit_hash'][:8]} - {r['status']}") def show_review(review_id): resp = requests.get(f"{API_BASE}/reviews/{review_id}") data = resp.json() print(f"Review #{data['review']['id']}") print(f"Repo: {data['review']['repo']}") print(f"Commit: {data['review']['commit_hash']}") print("--- Comments ---") for c in data["comments"]: print(f" {c['file_path']}:{c['line_number']} by {c['author']}") print(f" {c['content']}") def add_comment(review_id, file_path, line_number, author, content): payload = { "review_id": review_id, "file_path": file_path, "line_number": line_number, "author": author, "content": content } resp = requests.post(f"{API_BASE}/comments", json=payload) if resp.status_code == 200: print("Comment added.") else: print(f"Failed: {resp.text}") if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: review-cli <list|show|comment> ...") sys.exit(1) cmd = sys.argv[1] if cmd == "list": list_pending() elif cmd == "show": show_review(int(sys.argv[2])) elif cmd == "comment": add_comment(int(sys.argv[2]), sys.argv[3], int(sys.argv[4]), sys.argv[5], sys.argv[6])这个客户端很粗糙,但它验证了一件事:评审工具的核心不是界面多漂亮,而是能不能让评审人用最少的动作完成评审。我实测下来,命令行客户端的评审完成速度比网页快将近一倍,因为省掉了所有不必要的交互。
3.4 同步器:怎么和托管平台和平共处
同步器是整套方案里最容易被低估的部分。它的职责是双向的:把托管平台的改动拉进来,把评审结果推回去。拉进来好办,托管平台基本都有 webhook,配置一下就行。推回去就麻烦一些,因为不同平台的 API 差异很大。
我的做法是抽象出一个适配层,把不同平台的操作统一成几个标准动作:获取改动详情、获取文件内容、提交审批状态、合并改动。每个平台写一个适配器,实现这几个动作。这样换平台的时候,只需要换适配器,核心逻辑不用动。
class PlatformAdapter: def get_diff(self, repo, commit_hash): raise NotImplementedError def get_file_content(self, repo, file_path, ref): raise NotImplementedError def submit_approval(self, repo, commit_hash, status, comment): raise NotImplementedError def merge(self, repo, commit_hash): raise NotImplementedError class GenericGitAdapter(PlatformAdapter): def __init__(self, base_url, token): self.base_url = base_url self.token = token def get_diff(self, repo, commit_hash): # 通过平台的 diff API 获取改动 pass def get_file_content(self, repo, file_path, ref): # 通过平台的文件 API 获取完整文件内容 pass def submit_approval(self, repo, commit_hash, status, comment): # 把审批结果写回平台 pass def merge(self, repo, commit_hash): # 触发合并 pass这个适配层的价值在于,它把“平台相关”和“评审相关”彻底分开了。评审逻辑只跟适配层的接口打交道,完全不关心背后是哪个平台。我后来换过一次托管平台,只花了一个下午写新适配器,评审服务端一行没改。
4. 让评审真正发生:从“点了通过”到“留下判断”
4.1 评审空转的根因:不是人懒,是工具没给够上下文
工具搭好了,不代表评审就会发生。我见过太多团队,工具换了三套,评审质量一点没变。问题出在哪儿?出在评审人拿到的信息太少,根本没法做出有质量的判断。
你想想,一个评审人打开一个改动,看到的是几十行 diff。他不知道这段代码在整个系统里处于什么位置,不知道它依赖的模块最近有没有变化,不知道之前有没有类似的改动出过问题。在这种信息条件下,他能做的只有两件事:要么凭直觉点通过,要么把作者叫过来问半天。前者是空转,后者是低效。
所以 open-code-review 的一个关键设计,就是给评审人补全上下文。具体来说,至少要做到三件事:第一,展示改动所在的完整文件,而不只是 diff;第二,展示这个文件最近的改动历史,让评审人知道这块代码是不是经常出问题;第三,展示相关的测试用例,让评审人知道这个改动有没有被测试覆盖。
这三件事听起来简单,但做起来需要工具的支持。比如展示完整文件,就需要同步器在拉取改动的时候,把相关文件的完整内容也拉下来。展示改动历史,就需要评审服务端维护一个按文件维度的改动索引。展示测试用例,就需要解析代码结构,找到对应的测试文件。
4.2 评论的粒度:行级评论为什么不够用
大多数平台的评论都是行级的,你只能在某一行上留一句话。但真正有价值的评审意见,往往不是针对某一行的,而是针对一段逻辑、一个设计决策、一种模式。行级评论把评审人的思维也限制在了行级别。
我在自己的方案里加了一种“块级评论”,允许评审人对一段连续的代码留意见。实现方式是在 diff 里识别出连续的改动块,每个块可以单独评论。这样评审人可以说“这个循环的边界条件有问题”,而不是被迫在某一行的末尾写“这里不对”。
块级评论的另一个好处是,它让评审意见更容易被理解和处理。作者看到“这个循环的边界条件有问题”,立刻就知道要改哪儿。而看到一行末尾的“这里不对”,他得先猜评审人说的是哪一行、哪个问题。
4.3 评审意见的分类:把“我觉得”变成“我建议”
评审意见的质量,很大程度上取决于它是不是可操作的。我观察过大量的评审评论,发现它们大致可以分成四类:提问、建议、警告、阻断。提问是“这里为什么这么写”,建议是“我觉得可以改成这样”,警告是“这里可能有性能问题”,阻断是“这个改动不能合并”。
这四类意见的处理方式完全不同。提问需要作者回答,建议需要作者考虑,警告需要作者确认,阻断需要作者修改。如果所有意见都混在一起,作者就分不清轻重,要么全部忽略,要么全部照改。
所以我在评审客户端里加了一个意见类型的选择。评审人写评论的时候,必须选一个类型。这个小小的约束,让评审意见的可操作性提升了一大截。作者打开评审,一眼就能看到哪些是必须处理的,哪些是可以讨论的。
提示:意见分类这个设计,一开始有评审人抱怨“多了一步操作”。但跑了两周之后,同一个评审人跟我说,他现在写意见之前会先想一下“我这条到底是提问还是阻断”,想清楚之后再写,意见质量明显提高了。约束有时候不是负担,是帮助思考的工具。
5. 跑起来之后才发现的坑:自托管评审的五个真实教训
5.1 通知太多等于没有通知
评审服务上线第一周,我加了一个通知功能:只要有新的评审任务,就给所有评审人发消息。结果一周之后,所有人都在抱怨消息太多,开始无视通知。我这才意识到,通知的价值不在于“多”,而在于“准”。
后来我改成了分级通知:只有被指派的评审人会收到即时通知,其他人只在每天固定时间收到一次汇总。而且汇总里只列待处理的评审,已经处理过的不再出现。改完之后,通知的打开率从不到百分之十涨到了百分之六十多。
这个教训很朴素:任何通知机制,如果不能让接收者快速判断“这件事跟我有没有关系”,那它就是在制造噪音。评审通知尤其如此,因为评审人往往同时参与多个项目,信息过载是常态。
5.2 评审超时不是靠催,是靠设计
评审拖沓是另一个常见问题。一个改动提上去,三天没人理,作者急得不行,评审人却觉得“我还没看完”。我试过加催办功能,定时给评审人发提醒。效果很差,因为催办只解决了“知不知道”,没解决“愿不愿意”。
真正有效的做法是给评审设置预期时间。每个评审任务创建的时候,就根据改动的规模、涉及的模块、指派的评审人,估算一个合理的评审时长。超过这个时长还没完成,任务会自动升级:先提醒评审人,再提醒评审人的负责人,最后自动改派给其他人。
这个机制的关键是“自动改派”。它给了评审人一个明确的信号:你不做,这件事也会有人做,但你的不作为会被记录。我实测下来,有了自动改派之后,评审的平均完成时间从两天多缩短到了半天以内。
5.3 评审数据的价值在积累,不在单次
一开始我做评审数据统计,只关注单次评审的指标:这次评审花了多长时间、留了几条意见、有没有被打回。这些指标有用,但价值有限。真正有价值的是跨评审的积累:哪个模块的改动最容易出问题、哪个评审人的意见最常被采纳、哪类改动最容易引发争议。
这些积累性指标,需要评审数据长期沉淀才能算出来。所以我在数据模型设计的时候,特意把评审记录和代码模块做了关联。每次评审都记录它涉及了哪些文件、哪些模块。跑上几个月之后,你就能看到一些很有意思的模式:比如某个模块的改动,评审意见数量是平均值的五倍,那这个模块大概率设计有问题,需要重构。
5.4 评审人的负荷要看得见
评审质量下降,很多时候不是因为评审人不用心,而是因为他太累了。一个人同时被指派了十个评审,他不可能每个都认真看。但问题是,评审人自己往往意识不到自己超负荷了,指派的人也不知道。
所以我在评审服务里加了一个“评审负荷”视图,展示每个评审人当前待处理的评审数量、平均处理时长、最近一周的评审量。这个视图对评审人自己可见,对团队负责人也可见。有了这个视图之后,指派评审的时候就会下意识地避开那些已经满负荷的人。
这个设计的巧妙之处在于,它不强制任何人做任何事,只是把信息摆出来。但信息一旦可见,人的行为就会改变。我见过好几次,负责人看到某个评审人负荷太高,主动把评审任务分给了别人,这在没有视图的时候是不会发生的。
5.5 别让评审工具变成新的审批流程
最后一个坑,也是最危险的坑:评审工具用着用着,变成了新的审批流程。本来是为了让评审更有效,结果变成了“必须在系统里点通过才能合并”,跟原来的平台审批没区别,只是换了个地方点按钮。
避免这个坑的关键,是让评审工具的价值体现在“帮助评审人做判断”上,而不是“记录评审人做了判断”上。具体来说,评审工具应该提供上下文、提供分析、提供建议,而不是只提供一个“通过/不通过”的按钮。当评审人发现用这个工具能更快地看懂代码、更准地发现问题,他自然会用。如果他发现用这个工具只是多了一道手续,他迟早会绕过它。
我自己的做法是,评审工具里集成了几个自动检查:改动涉及的函数有没有测试覆盖、改动的复杂度有没有超过阈值、改动有没有引入新的依赖。这些检查不阻断合并,只是给评审人提供参考。评审人看到这些信息,做判断的速度和准确度都提高了,工具的价值也就体现出来了。
6. 从工具到习惯:让评审成为团队的自然动作
6.1 评审文化的起点是“允许说不知道”
工具再好,如果团队文化不支持,评审也做不起来。我见过最糟糕的情况是,评审人不敢提问题,怕显得自己不懂;作者不敢接受意见,怕显得自己水平差。结果就是所有人都在走过场。
改变这种文化,得从一件小事开始:允许评审人说“我不知道这段代码是干什么的”。这句话听起来简单,但它打破了“评审人必须什么都懂”的假设。评审人不是全知全能的,他看不懂某段代码,本身就是一条有价值的信息——说明这段代码的可读性有问题,或者缺少必要的注释。
我在团队里推过一个做法:评审意见里必须至少有一条是提问。不是那种“这里为什么这么写”的敷衍提问,而是真正想弄明白的提问。这个做法逼着评审人认真读代码,也逼着作者把代码写得更清楚。跑了几个月之后,代码的可读性明显提升,因为作者知道会有人问,写的时候就会多想一步。
6.2 评审的节奏感:快评和慢评要分开
评审不是越快越好,也不是越慢越好。有些改动很小,比如改个配置、修个拼写错误,这种应该快评,几分钟搞定。有些改动很大,比如重构一个模块、引入一个新的设计模式,这种应该慢评,给评审人足够的时间去理解。
问题在于,大多数团队对所有改动都用同一种节奏。小改动被拖好几天,大改动被催着赶紧通过。这两种情况都会导致评审质量下降。
我的做法是按改动规模分级。改动行数少于五十行的,走快速通道,指派一个评审人,要求一小时内完成。改动行数在五十到五百行之间的,走标准通道,指派两个评审人,要求一天内完成。改动行数超过五百行的,走深度通道,指派三个评审人,要求三天内完成,而且必须有一次面对面的讨论。
这个分级不是拍脑袋定的,是根据实际数据调出来的。我统计过不同规模改动的评审意见数量和问题发现率,发现五百行是一个明显的分界线。超过五百行的改动,评审意见的数量和质量都会显著下降,因为评审人看不过来。所以超过五百行的改动,必须拆分成多个小改动,或者安排专门的讨论。
6.3 评审意见的闭环:每条意见都要有归宿
评审意见提了之后没人管,是评审空转的另一个表现。作者看了意见,改了,但没告诉评审人改了哪儿;或者作者觉得意见不对,但没解释为什么。评审人不知道自己的意见有没有被采纳,下次就不愿意提了。
所以我在评审服务里加了一个“意见状态”的跟踪。每条意见都有状态:待处理、已采纳、已拒绝、已讨论。作者必须给每条意见一个归宿,要么改,要么解释为什么不改。评审人可以看到自己的意见被怎么处理了。
这个机制的关键是“必须给归宿”。如果作者可以无视意见直接合并,那意见状态跟踪就没意义。所以我在合并的检查里加了一条:所有阻断类意见必须处理完毕,所有警告类意见必须有回应。这条规则不限制作者的自由,但要求作者对自己的决定负责。
6.4 评审的度量:别用数量考核评审人
最后说一个容易踩的坑:用评审数量考核评审人。我见过一些团队,把“每月评审多少个改动”作为考核指标。结果就是评审人开始抢着评审小改动,因为小改动快,数量容易上去。大改动没人愿意碰,因为费时费力还不讨好。
评审这件事,数量从来不是目标,质量才是。如果要度量,应该度量的是评审的有效性:评审意见的采纳率、评审后发现问题的比例、评审后代码的返工率。这些指标才能反映评审的真实价值。
我自己的做法是,不把评审指标跟个人绩效挂钩,而是跟团队目标挂钩。团队的目标是“降低线上问题数量”,评审是达成这个目标的手段之一。评审做得好,线上问题少,团队受益,个人自然也受益。这样评审人就不会为了数量而评审,而是为了质量而评审。
7. 一些零散但实用的经验
评审工具的通知,最好支持“稍后提醒”。评审人看到通知的时候可能正在忙,如果只能“立即处理”或“忽略”,他大概率会选忽略。有了“稍后提醒”,他可以选择一个小时后、明天早上再提醒,这样通知就不会被浪费。
评审意见的输入框,最好支持 Markdown。评审人经常需要贴代码片段、贴日志、贴链接,纯文本输入框会让这些内容变得很难读。支持 Markdown 之后,评审意见的可读性提升明显。
评审服务的数据备份,一定要做。评审记录是团队的知识资产,丢了就找不回来了。我的做法是每天自动备份一次数据库,备份文件保留三十天。这个成本很低,但关键时刻能救命。
评审人的指派,最好支持“轮值”。固定指派容易导致某些人负担过重,某些人闲着。轮值可以让评审负荷更均匀,也能让更多人参与到评审中来,提升团队整体的代码理解水平。
评审的讨论,最好支持“线程”。一条意见下面可以有多条回复,形成讨论线程。这样评审人和作者可以就一个问题深入讨论,而不会跟其他意见混在一起。线程化的讨论,也让后来的评审人更容易理解之前的讨论脉络。
评审的搜索,最好支持按文件路径搜索。有时候你想看看某个文件最近的评审记录,按文件路径搜索是最直接的方式。这个功能实现起来不难,但用起来很顺手。
评审的统计报表,最好支持导出。团队负责人可能需要把评审数据放到自己的报表里,跟其他数据一起分析。支持导出 CSV 或 JSON,会让这件事方便很多。
评审的权限,最好支持“只读”角色。有些团队成员可能不需要参与评审,但需要查看评审记录。给他们一个只读角色,既能满足查看需求,又不会干扰评审流程。
评审的界面,最好支持深色模式。评审人经常在晚上看代码,深色模式能减少眼睛疲劳。这个功能很小,但很贴心。
评审的快捷键,最好支持自定义。高频评审人往往有自己的操作习惯,支持自定义快捷键能显著提升效率。我自己的配置里,把“下一条意见”绑到了 J 键,“上一条意见”绑到了 K 键,看评审的时候手不用离开键盘。
评审的移动端支持,最好有但不用强求。评审这件事,在手机上做体验很差,因为屏幕小、输入慢。但如果评审人只是想在手机上快速看一眼有没有紧急的评审任务,一个简单的移动端界面还是有用的。
评审的集成,最好支持 Webhook。评审状态变化的时候,可以触发 Webhook,让其他系统知道。比如评审通过之后,自动触发部署流程。这个集成能力,让评审服务从一个孤立的工具,变成了研发流程里的一个环节。
评审的日志,最好记录完整。谁在什么时候创建了评审、谁在什么时候留了意见、谁在什么时候改了状态,这些都要记录。出了问题的时候,日志是唯一的追溯依据。
评审的测试,最好覆盖核心流程。评审服务本身也是代码,也需要测试。创建评审、添加评论、更新状态、合并改动,这些核心流程都要有测试覆盖。不然评审服务自己出问题,整个评审流程就断了。
评审的文档,最好写在代码旁边。评审服务的配置、部署、使用说明,最好跟代码放在一起,用 Markdown 写。这样代码更新的时候,文档也容易跟着更新。文档和代码分家,是文档过时的根本原因。
评审的升级,最好支持灰度。评审服务的新版本,先在一小部分评审任务上试用,没问题再全量。这样即使新版本有问题,影响范围也可控。
评审的回滚,最好支持一键。评审服务出问题的时候,能快速回滚到上一个版本。回滚的速度,决定了故障的影响时长。
评审的监控,最好有告警。评审服务的可用性、响应时间、错误率,都要有监控。出问题的时候,第一时间知道,而不是等评审人来投诉。
评审的成本,最好心里有数。自托管评审服务,服务器成本、运维成本、开发成本,加起来不是小数目。如果团队规模很小,用现成的托管服务可能更划算。自托管的价值,在团队规模大到一定程度、对自主可控有明确需求的时候,才会体现出来。
评审的替代方案,最好也了解一下。除了自托管,还有一些折中方案,比如用托管平台的 API 做扩展、用开源评审工具做二次开发。这些方案的成本和收益各不相同,选之前多比较一下。
评审的最终目标,是让评审消失。不是说不评审了,而是说评审变成了一种自然动作,不需要刻意推动,不需要工具强制,每个人在提交代码之前都会下意识地想“这段代码别人会怎么看”。到了这个状态,评审工具的存在感反而会降低,因为它已经融入了日常。这大概就是 open-code-review 这类实践最终想达到的效果。