1. 从一次代码评审的尴尬说起
上周帮一个刚转入代码评审流程的朋友排查问题,他负责的一个模块需要批量拉取某代码评审平台上近三个月的提交记录,用来做代码变更趋势分析。他折腾了一整天,要么拉下来的数据缺字段,要么分页拉不全,要么被平台的权限校验挡在门外。他问我:“不就是调个接口拿数据吗,怎么这么费劲?”
这个问题其实特别典型。很多人第一次接触代码评审平台的提交信息拉取,都会低估它的复杂度——以为一个HTTP请求就能搞定,结果发现要处理认证、分页、字段映射、变更文件列表、评审状态过滤等一堆细节。从代码评审平台获取提交信息这件事,看起来简单,实际上涉及接口选型、认证方式、数据解析、增量同步等多个环节,每个环节都有坑。
这篇内容就是把我这些年在这件事上踩过的坑、总结的方法完整梳理一遍。不管你是要做代码变更统计、构建自动化报表、做代码评审质量分析,还是单纯想把提交记录同步到自己的系统里,这里面的思路和代码都能直接参考。我会从接口选型讲到实操代码,从认证配置讲到分页处理,再讲到常见问题的排查,尽量让不同基础的朋友都能上手。
2. 先搞清楚你要拿的是什么数据
2.1 提交信息的三个层次
在动手写代码之前,必须先明确一件事:你说的“提交信息”到底指什么?在代码评审平台的语境下,提交信息通常分三个层次,获取方式和数据量差异很大。
第一个层次是提交的基本元数据,包括提交哈希、作者、提交时间、提交信息、变更的仓库和分支等。这部分数据量相对小,一次请求能拿一批,适合做统计分析。
第二个层次是变更文件列表,也就是这次提交改了哪些文件、每个文件的增删行数、变更类型(新增、修改、删除、重命名)。这部分数据量会大很多,因为一次提交可能涉及几十上百个文件。
第三个层次是评审相关的信息,包括评审状态、评审人、投票结果、评论内容等。这部分数据在代码评审平台上有独立的接口,和提交本身的数据是分开的。
我见过不少人一开始没想清楚要哪层数据,直接调了最全的接口,结果返回的JSON巨大无比,解析起来又慢又容易出错。所以第一步一定是明确需求:如果你只是做提交频率统计,第一层就够了;如果你要做代码变更影响分析,那第二层必须拿;如果你要做评审效率分析,第三层才是重点。
2.2 接口选型:REST还是SSH
代码评审平台一般提供两种获取数据的方式:REST风格的HTTP接口和SSH命令。两者各有适用场景,选错了会多走很多弯路。
REST接口的优势是通用性强,任何能发HTTP请求的语言都能用,返回的是结构化JSON,解析方便。缺点是认证配置相对复杂,分页需要自己处理,大数据量拉取时效率一般。
SSH命令的优势是认证简单(配置好密钥就行),支持流式输出,适合在Shell脚本里做批量处理。缺点是返回的是文本格式,需要自己解析,而且不同版本的命令输出格式可能有差异。
我的建议是:如果你在做自动化系统集成,优先用REST接口;如果你在做运维脚本或者一次性数据导出,SSH命令更省事。下面两种方式我都会给出完整的实操方案。
2.3 认证方式的选择逻辑
认证是很多人卡住的第一关。代码评审平台常见的认证方式有HTTP Basic Auth、Digest Auth、OAuth令牌等。选择哪种,取决于你的使用场景。
如果你是在自己的机器上跑一次性脚本,用HTTP密码认证最直接,配置简单。如果你是在CI/CD流水线或者共享服务器上跑,强烈建议用访问令牌,因为令牌可以单独撤销,不会泄露主账号密码。如果你要做的是长期运行的服务,那OAuth是更规范的选择,但配置复杂度也最高。
这里有个经验:很多平台的REST接口对匿名访问是有限制的,即使仓库是公开的,提交信息接口也可能需要认证。所以不要假设“公开仓库就能匿名拉数据”,先配好认证再测试。
3. 用REST接口拉取提交信息的完整实操
3.1 认证配置与请求构造
先讲REST方式。假设你已经拿到了访问令牌,接下来要构造请求。代码评审平台的REST接口通常有一个基础路径,比如/changes/或者/projects/,具体取决于你要查的是变更还是项目。
请求的URL一般长这样:
https://<平台地址>/a/changes/?q=project:<项目名>+status:merged&n=100注意那个/a/前缀,它表示需要认证。如果你的请求没带认证信息,平台会返回401。认证信息放在请求头里:
curl -H "Authorization: Bearer <你的令牌>" \ "https://<平台地址>/a/changes/?q=project:myproject+status:merged&n=100"如果你用的是用户名密码方式,可以用Basic Auth:
curl -u "用户名:密码" \ "https://<平台地址>/a/changes/?q=project:myproject+status:merged&n=100"这里有个细节:返回的JSON前面通常会有一个防跨站脚本的前缀,比如)]}',解析之前必须把这行去掉。我第一次遇到的时候以为是数据格式错了,排查了半天才发现是平台的安全机制。
3.2 查询参数的写法与组合技巧
查询参数是REST接口的核心,写对了能精准过滤,写错了要么拉不到数据,要么拉一堆没用的。常用的查询条件包括:
project:<项目名>:按项目过滤status:merged或status:open:按评审状态过滤after:<日期>和before:<日期>:按时间范围过滤owner:<用户名>:按提交者过滤branch:<分支名>:按分支过滤
多个条件用+连接,表示逻辑与。比如要查某项目在某个时间之后合并的所有提交:
q=project:myproject+status:merged+after:2024-01-01这里有个容易踩的坑:日期格式必须是YYYY-MM-DD,而且after和before是包含边界的。如果你要查某一天的提交,用after:2024-01-01+before:2024-01-01是查不到的,因为两个条件互斥了。正确做法是用after:2024-01-01+before:2024-01-02。
另外,查询参数里的特殊字符需要URL编码,比如项目名里有空格或者斜杠,必须编码后再拼进去,否则请求会失败。
3.3 分页处理:最容易出错的地方
分页是拉取大量数据时绕不开的问题。代码评审平台的REST接口一般用n参数控制每页数量,用返回结果里的_more_changes字段判断是否还有下一页。
很多人第一次写分页逻辑会写成这样:
import requests def fetch_all_changes(base_url, token, project): all_changes = [] start = 0 while True: url = f"{base_url}/a/changes/?q=project:{project}&n=100&S={start}" resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}) text = resp.text if text.startswith(")]}'"): text = text[4:] data = json.loads(text) if not data: break all_changes.extend(data) start += 100 return all_changes这段代码看起来没问题,但实际上有个隐患:如果两次请求之间数据有更新,分页的偏移量会错位,导致漏数据或重复数据。更稳妥的做法是用返回结果里的_more_changes字段来判断,并且用最后一条记录的ID作为下一页的起点。
def fetch_all_changes_safe(base_url, token, project): all_changes = [] last_id = None while True: query = f"project:{project}&n=100" if last_id: query += f"&S={last_id}" url = f"{base_url}/a/changes/?q={query}" resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}) text = resp.text if text.startswith(")]}'"): text = text[4:] data = json.loads(text) if not data: break all_changes.extend(data) if len(data) < 100: break last_id = data[-1]["_sortkey"] return all_changes提示:不同平台的排序键字段名可能不同,有的是
_sortkey,有的是id,具体要看接口文档。用之前先确认一下。
3.4 数据解析与字段映射
拿到JSON之后,下一步是解析出你需要的字段。代码评审平台的提交信息字段比较多,常用的有:
| 字段名 | 含义 | 备注 |
|---|---|---|
id | 变更ID | 格式通常是项目名~分支名~变更号 |
subject | 提交标题 | 第一行提交信息 |
owner | 提交者 | 包含姓名和邮箱 |
created | 创建时间 | ISO格式,带时区 |
submitted | 合并时间 | 未合并时可能为空 |
status | 状态 | NEW、MERGED、ABANDONED |
insertions | 新增行数 | 需要额外请求才能拿到 |
deletions | 删除行数 | 需要额外请求才能拿到 |
注意最后两个字段:增删行数不在列表接口里返回,需要单独请求每个变更的详情接口。如果你要统计代码变更量,这一步不能省。但逐个请求会很慢,建议用并发请求或者批量接口。
from concurrent.futures import ThreadPoolExecutor def fetch_change_detail(base_url, token, change_id): url = f"{base_url}/a/changes/{change_id}/detail" resp = requests.get(url, headers={"Authorization": f"Bearer {token}"}) text = resp.text if text.startswith(")]}'"): text = text[4:] return json.loads(text) def enrich_changes(base_url, token, changes): with ThreadPoolExecutor(max_workers=10) as executor: futures = [ executor.submit(fetch_change_detail, base_url, token, c["id"]) for c in changes ] details = [f.result() for f in futures] return details并发数不要设太大,一般10到20就够了,太大了容易被平台限流。
4. 用SSH命令拉取提交信息的实操方案
4.1 命令格式与参数说明
如果你不想折腾HTTP认证,SSH方式是更轻量的选择。代码评审平台的SSH接口通常支持query命令,格式如下:
ssh -p 29418 <用户名>@<平台地址> gerrit query \ --format=JSON \ --current-patch-set \ --all-approvals \ project:myproject status:merged limit:100几个关键参数:
--format=JSON:输出JSON格式,方便解析--current-patch-set:包含当前补丁集的信息--all-approvals:包含所有评审投票信息limit:100:限制返回数量
不加--format=JSON的话,默认输出是文本格式,解析起来麻烦很多。强烈建议始终加上这个参数。
4.2 批量拉取与结果解析
SSH命令的输出是每行一个JSON对象,最后一行是统计信息。解析的时候要注意跳过最后一行:
ssh -p 29418 user@host gerrit query --format=JSON project:myproject limit:100 \ | head -n -1 \ | jq -r '[.project, .number, .subject, .owner.name, .created] | @tsv'这里用了jq来做解析,它是处理JSON的命令行利器。head -n -1是为了去掉最后一行的统计信息。
如果你要拉取超过单次限制的数据,需要结合--start参数做分页:
#!/bin/bash START=0 LIMIT=100 while true; do RESULT=$(ssh -p 29418 user@host gerrit query \ --format=JSON --start=$START project:myproject limit:$LIMIT) COUNT=$(echo "$RESULT" | head -n -1 | wc -l) echo "$RESULT" | head -n -1 if [ "$COUNT" -lt "$LIMIT" ]; then break fi START=$((START + LIMIT)) done注意:SSH方式的分页也是基于偏移量的,同样存在数据更新导致错位的问题。如果对数据一致性要求高,建议用REST方式配合排序键。
4.3 两种方式的对比与选择建议
| 对比维度 | REST接口 | SSH命令 |
|---|---|---|
| 认证复杂度 | 中(需要令牌或密码) | 低(配置密钥即可) |
| 返回格式 | JSON | JSON或文本 |
| 分页支持 | 完善 | 基础 |
| 并发能力 | 强 | 弱 |
| 适合场景 | 系统集成、批量分析 | 运维脚本、快速查询 |
| 限流风险 | 有 | 较低 |
我的经验是:做数据分析和系统集成,用REST;做日常运维和临时查询,用SSH。两者不是互斥的,可以结合使用。
5. 常见问题与排查技巧实录
5.1 认证失败的五种典型情况
认证问题是最常见的,我整理了一个速查表:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 401 Unauthorized | 令牌过期或错误 | 重新生成令牌 |
| 403 Forbidden | 账号无该项目权限 | 联系管理员加权限 |
| 返回HTML而非JSON | 认证信息没带上 | 检查请求头 |
| SSH连接被拒 | 公钥没上传 | 上传公钥到平台 |
| 匿名可读但接口报错 | 接口需要认证 | 配置认证信息 |
有一个特别隐蔽的坑:有些平台的REST接口在认证失败时返回的是登录页面的HTML,而不是401错误码。如果你直接json.loads就会报解析错误,排查半天以为是数据格式问题。所以解析之前先判断返回内容是不是以)]}'开头,不是的话大概率是认证出了问题。
5.2 数据缺失与重复的排查思路
数据缺失和重复通常和分页有关。排查步骤:
- 先确认单次请求能拿到多少条,和
n参数是否一致 - 检查分页的偏移量计算是否正确
- 检查是否有数据在两次请求之间被更新
- 用提交ID做去重,确认是否有重复
如果发现数据缺失,可以换一种分页方式:不用偏移量,而是用时间范围切片。比如每次拉取一天的数据,这样即使有更新也不会影响其他时间段。
5.3 性能优化的几个实用技巧
拉取大量数据时,性能是个问题。几个实用的优化技巧:
- 只请求需要的字段:有些接口支持字段过滤,能显著减少传输量
- 合理设置并发数:10到20比较合适,太大容易被限流
- 本地缓存:已经拉过的数据存本地,下次只拉增量
- 增量同步:记录上次拉取的最后时间,下次只拉这个时间之后的
增量同步的代码大概长这样:
import json import os CACHE_FILE = "last_sync.json" def load_last_sync(): if os.path.exists(CACHE_FILE): with open(CACHE_FILE) as f: return json.load(f) return {"last_time": "1970-01-01"} def save_last_sync(last_time): with open(CACHE_FILE, "w") as f: json.dump({"last_time": last_time}, f) def incremental_fetch(base_url, token, project): state = load_last_sync() after = state["last_time"] changes = fetch_changes(base_url, token, project, after=after) if changes: latest = max(c["created"] for c in changes) save_last_sync(latest) return changes这个模式在实际项目里非常实用,能把每次拉取的数据量降到最低。
5.4 我踩过的三个印象最深的坑
第一个坑是时区问题。平台返回的时间是带时区的ISO格式,但我在做时间范围过滤时用的是本地时间,结果拉出来的数据总是差几个小时。后来统一用UTC时间做过滤和比较,问题就解决了。
第二个坑是特殊字符编码。项目名里有个斜杠,我直接拼到URL里,结果请求一直失败。后来用urllib.parse.quote编码后才正常。这个坑很隐蔽,因为错误信息不会直接告诉你编码问题。
第三个坑是限流。有一次我并发数设了50,结果被平台限流,返回了一堆429错误。后来降到10,并且加了重试机制,就稳定了。重试的时候要加退避策略,不要立即重试。
import time def request_with_retry(url, headers, max_retries=3): for i in range(max_retries): resp = requests.get(url, headers=headers) if resp.status_code == 429: wait = 2 ** i time.sleep(wait) continue return resp raise Exception("请求失败,已重试多次")6. 把数据用起来:从拉取到分析的完整链路
6.1 数据清洗与结构化存储
拉下来的原始数据通常需要清洗才能用。常见的清洗操作包括:去掉防跨站脚本前缀、解析嵌套的owner字段、把时间统一转成UTC、过滤掉不需要的状态。
清洗完之后建议存到结构化存储里,方便后续查询。小数据量用SQLite就够了,大数据量可以考虑PostgreSQL或者ClickHouse。表结构大概这样:
CREATE TABLE commits ( change_id TEXT PRIMARY KEY, project TEXT, branch TEXT, subject TEXT, owner_name TEXT, owner_email TEXT, created_at TIMESTAMP, submitted_at TIMESTAMP, status TEXT, insertions INTEGER, deletions INTEGER );用change_id做主键,天然支持去重和增量更新。
6.2 几个实用的分析场景
数据拉下来之后能做的事情很多,举几个我实际做过的场景:
提交频率趋势:按天或按周统计提交数量,看团队的开发节奏。这个用SQL的GROUP BY就能搞定。
代码变更量排行:按提交者统计增删行数,识别高产出和低产出的模块。注意增删行数不能完全代表工作量,要结合业务复杂度看。
评审时长分析:从创建到合并的时间差,反映评审效率。这个指标能帮团队发现评审瓶颈。
模块变更热点:统计哪些文件被改得最频繁,这些文件往往是重构的重点目标。
6.3 自动化报表的搭建思路
如果要做定期报表,可以用定时任务加脚本的方式。核心逻辑是:定时触发拉取、清洗、入库、生成报表、发送通知。
# 每天凌晨2点执行 0 2 * * * /path/to/fetch_and_report.sh >> /var/log/report.log 2>&1脚本里做好错误处理和日志记录,出问题的时候能快速定位。报表可以用Markdown或者HTML格式生成,方便查看和分享。
7. 一些个人体会
这套流程我用了好几年,从最开始的手动拉取到后来的自动化流水线,中间踩的坑基本都在这篇里了。最大的体会是:拉数据这件事,难点从来不在“拉”本身,而在“拉什么”和“怎么拉得稳”。想清楚需求再动手,比急着写代码重要得多。
另外,增量同步和本地缓存这两个机制,建议从一开始就加上。我见过太多项目一开始图省事全量拉取,数据量大了之后性能问题集中爆发,回头再改成本很高。还有认证信息的管理,千万别硬编码在脚本里,用环境变量或者配置文件,既安全又方便切换环境。
最后分享一个小技巧:如果你不确定查询参数怎么写,可以先用平台的Web界面做一次筛选,然后看浏览器开发者工具里的请求URL,把参数抄下来就行。这个方法比翻文档快得多,尤其是查询语法比较复杂的时候。