一、反馈长期没人管,PR 就成了“留言板”

代码审查本来是为了让代码更好,但现实里经常是另一幅样子:一个 PR 上挂着好几条评论,提醒弹窗被忽略,作者一脸茫然,审查者也不知道自己的意见有没有被看到。最后这些评论变成“历史遗留问题”,代码还是按原样合进去了。这个现象不是个例,很多团队都有。

我见过一个团队,PR 发出去之后,大家习惯性地去评论区写下自己的担忧。但没人定义“下一步”,评论只是意见,没有负责人,也没有处理截止时间。等代码合并以后,新问题又冒出来。所以,评论管理不是锦上添花,而是代码审查能不能真正跑完的关键。

1.1 一个常见的小悲剧

假设小王在周一提交了一个 PR,代码里有一个循环边界问题。小李在评论里指了出来,还写了一句“这里可能会越界,请检查”。小王当时在开会,看到了通知但没细看。第二天他忘记这件事,评论就沉底了。周五 PR 被合并,测试环境立刻崩了。其实这条评论已经给出了预警,只是它没有进入一个“必须流转”的通道。

1.2 为什么评论会“静默消失”

因为评论和任务是两种不同的东西。评论是“说一句话”,任务是“需要被完成”。在 Bitbucket 的 Pull Request 页面里,这两者可以混在一起。如果没有额外的机制把评论变成任务,人就只能靠脑子记。人的记忆力不可靠,事情一多就忘。

所以这篇文章想聊的,是怎么在 Bitbucket 的环境里,让每一条评论都变得更像“待办事项”,而不是一句飘过的话。

二、Bitbucket 评论里有什么,哪些东西需要管理

再动手之前,先看看 Bitbucket 里的评论长什么样。它大体分成三类:整体评论、行内评论、还有带状态的评论。整体评论是贴在 PR 最下面的留言,适合总结;行内评论是选代码某一行来提意见,最常用;带状态的评论一般是指那些需要回应的“任务”,在界面上会有“已解决”之类的标记。

很多团队只用到了“留言”属性,没用状态。评论一多,页面就会变得很难看。我们可以利用这些状态和页面的接口,把评论捞出来,统一处理。

2.1 评论管理为什么容易失败

第一个坑:评论和任务没有绑定。一个评论出现之后,没人知道下一步该干嘛。

第二个坑:缺少定时巡检。每天开 PR 几十个,不会有人手动去翻每个评论。

第三个坑:没有催办机制。就算知道有评论,拖着不做也正常。

第四个坑:信息分散。评论在 Bitbucket 里,聊天软件在另一个工具里,两边不同步。

这些坑,单靠人治很难填平。我们需要写一点小脚本,让机器去当那个“提醒的人”。

2.2 需要关注的评论字段

从 Bitbucket 的接口拿到的评论对象里,有几个信息很关键:评论是谁写的、正文是什么、创建时间、是否已解决、是否被删除。只要把这些字段读出来,我们就能判断哪些评论值得跟进。比如“已解决”字段是 true,这条评论就可以不管;“已删除”字段是 true,也不需要提醒。剩下的评论,才真正需要人去看。

三、反馈流转的优化思路

要让评论流动起来,可以拆成四个步骤:抓取、筛选、通知、销项。

抓取是定期从 Bitbucket 把评论拉到本地;筛选是只留下那些还没有被解决的评论;通知是把筛选结果发给对应的人;销项是评论处理完之后,把它标记成“已解决”或者回复一句“已处理”。这四个步骤形成闭环,反馈就不会堵在评论区里。

3.1 把评论当任务看待

如果一条评论需要修改代码,就把它当成一个任务。任务必须有状态:未开始、处理中、已完成。Bitbucket 本身有“已解决”这种状态,我们可以在脚本里读取它。每一条待办评论都应该对应一位负责人。负责人不明确,就默认是 PR 的作者。

3.2 代码是给团队用的,不是给自己炫技的

写评论管理脚本时,不要追求高性能,而要追求好读懂。团队里能维护脚本的人越多越好。所以下面示例用 JavaScript 来写,运行环境是 Node.js。这个技术栈普遍,很多前端后端开发者都看得懂。

四、用 Node.js 实现一个简单的评论督察工

4.1 准备工作

你需要准备三样东西:一个可以访问 Bitbucket API 的 Access Token;一个具体的 Bitbucket 工作空间名称;还有仓库标识和 PR 编号。Token 不要写死在代码里,最好放在环境变量中。下面示例会在 Node.js 18+ 环境下运行,因为用到了内置的 fetch 方法,不需要额外安装网络请求库。

Token 可以到 Bitbucket 的个人设置里创建。创建的时候,只勾选读取评论和发表评论的权限,不要给整个仓库的写权限。这样即使 Token 不小心泄露,问题也能控制在比较小的范围内。

下面每个示例都是可以直接运行的单文件脚本,不过为了演示清晰,变量名会尽量保持一致。

4.2 示例一:把某个 PR 的评论全部拉下来

这个示例实现了自动翻页。当评论数量比较多的时候,Bitbucket 会分页返回,代码通过 next 字段继续取下一页,直到取完为止。在每一条评论里,我们可以得到作者、正文、创建时间、是否已解决等信息。

// 技术栈:JavaScript (Node.js 18+,使用内置 fetch)
// 功能:从 Bitbucket 拉取一个 PR 的全部评论,自动处理分页

async function fetchAllComments(workspace, repoSlug, prNumber, accessToken) {
  const apiRoot = 'https://api.bitbucket.org/2.0/repositories';
  let nextUrl = `${apiRoot}/${workspace}/${repoSlug}/pullrequests/${prNumber}/comments`;
  const allComments = [];

  // Bitbucket 的列表接口会返回 values 和 next 字段
  while (nextUrl) {
    const response = await fetch(nextUrl, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`请求失败,HTTP 状态码是 ${response.status}`);
    }

    const payload = await response.json();
    allComments.push(...payload.values); // 把当前页的评论放进数组

    nextUrl = payload.next || null;       // 如果没有 next,说明没有下一页了
  }

  return allComments; // 返回全部评论
}

这段代码有三个重点。第一,用 while 循环处理分页,不会丢评论。第二,用 push(...values) 把数组拆开再合入,不会产生嵌套数组。第三,抛出异常而不是打印一句错误后继续跑,方便排查问题。

4.3 示例二:筛选出还没有解决的评论

拉下来的评论里,可能有已经解决的、被删除的,还有系统自动生成的。我们只关心真正需要人去看的那些评论。下面这段代码负责把“未解决”的评论挑出来,并且整理成可读的文本。

// 技术栈:JavaScript (Node.js 18+)
// 功能:从评论数组中挑出未解决、未删除的评论,并且打印成提醒文本

function collectUnresolvedComments(comments) {
  // 用 filter 把符合条件的评论挑出来
  const unresolved = comments.filter(comment => {
    // 如果评论被标记为已解决,就不再提醒
    if (comment.resolved === true) return false;
    // 如果评论被删除了,跳过它
    if (comment.deleted === true) return false;

    // 只保留普通评论,手工过滤掉系统迁移数据
    return comment.type !== 'migration';
  });

  // 把未解决评论整理成容易阅读的文本
  return unresolved.map(comment => {
    const author = comment.user?.display_name || '未知用户';
    const content = comment.content?.raw || '';
    const created = comment.created_on ? new Date(comment.created_on).toLocaleString() : '时间未知';
    return `【${author}】在 ${created} 说:${content}`;
  });
}

// 下面是本地模拟数据的调用例子
const sampleComments = [
  { id: 1, resolved: false, deleted: false, type: 'comment', user: { display_name: '老张' }, content: { raw: '这里要加个空指针判断' }, created_on: '2025-03-01T10:00:00Z' },
  { id: 2, resolved: true, deleted: false, type: 'comment', user: { display_name: '小李' }, content: { raw: '这个警告可以忽略' }, created_on: '2025-03-01T11:00:00Z' },
  { id: 3, resolved: false, deleted: true, type: 'comment', user: { display_name: '小王' }, content: { raw: '这条已经作废' }, created_on: '2025-03-01T12:00:00Z' }
];

const result = collectUnresolvedComments(sampleComments);
console.log(result.join('\n'));
// 只会输出老张那条评论,小李的已解决,小王的已删除

示例中的数据是我模拟的。真正接入时,字段名要以 Bitbucket 返回的 JSON 为准。注释已经说明每部分的作用,方便同事接手。

4.4 示例三:给 PR 追加一条催办评论

筛选出未解决问题之后,下一步是通知。最简单的通知方式,就是让脚本自动在 PR 下追加一条评论,并提到相关人员。在 Bitbucket 里,评论正文可以用 @用户名 来提醒对方。下面示例先把一条固定消息发到目标 PR 上。

// 技术栈:JavaScript (Node.js 18+)
// 功能:在指定 Pull Request 下面追加一条新评论

async function postComment(workspace, repoSlug, prNumber, accessToken, message) {
  const apiRoot = 'https://api.bitbucket.org/2.0/repositories';
  const url = `${apiRoot}/${workspace}/${repoSlug}/pullrequests/${prNumber}/comments`;

  // 发起 POST 请求,把 message 作为评论内容提交
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ content: { raw: message } }) // Bitbucket 的字段名是 content.raw
  });

  if (!response.ok) {
    throw new Error(`新增评论失败,HTTP 状态码是 ${response.status}`);
  }

  const createdComment = await response.json(); // 新建的评论会作为响应体返回
  console.log(`评论已发布,评论编号:${createdComment.id}`);
  return createdComment;
}

// 调用示例:提醒作者处理还有未解决问题的 PR
const token = process.env.BITBUCKET_TOKEN; // 从环境变量读取,别把密钥写死
postComment('your-workspace', 'your-repo', 42, token, '请尽快处理上面的未解决评论,谢谢!');

注意,这里发送的是“整体评论”。如果你需要在代码具体某一行上创建行内评论,还需要在请求体里补充行信息。行内评论更复杂,比如需要代码行号和源码路径。这个可以等基本流程跑通后再深入学习。

4.5 把几个函数串起来用

上面的函数可以串成一个定时执行的脚本。每天上午十点,脚本检查一批 PR,把所有未解决的评论汇总起来,统一发给作者,再在 PR 里追加一条提醒。这样一来,没有人需要手动去翻每一个 PR,反馈自然会推动。

如果你希望实现得更完整,可以把 Token 放在系统环境变量里,用系统自带的任务计划程序来定时运行。比如在 Linux 上可以用定时任务,在 Windows 上可以用任务计划程序。脚本本身只需要把刚才几个函数按顺序调用,然后输出结果就行。

到这里,核心的优化思路和代码就演示完了。接下来聊聊在真实场景里还有哪些值得注意的细节。

五、应用场景

5.1 日常代码审查

这是最直接的应用。有评论没人看,就靠脚本每天扫一遍。开发者早上打开电脑,收到的第一条消息就是“昨天你的 PR 还有两条评论没处理”。这种提醒比被动看通知要有效得多。

5.2 跨团队协作

当一个 PR 里有前后端同事、测试同事、产品同事的评论时,评论很容易被漏掉。每个角色说话轻重不同,脚本可以把不同角色产生的评论统一汇总,再按照模块分发给对应负责人。这样每个人的待办列表都是完整的。

5.3 自动化质量稽核

在提交发布清单之前,脚本可以先检查一遍“是否有未解决评论”。如果还有未处理评论,就阻断发布。这个环节可以放在 CI/CD 流水线里,让机器决定一个 PR 是否真正“可合并”。不过要注意,这里需要配置好权限,避免误伤。

5.4 与内部聊天工具联动

很多团队习惯在群里看通知。脚本可以在筛出未解决的评论后,直接把消息推送到企业微信、钉钉或者飞书。这样连打开 Bitbucket 的时间都省了。实现思路是用聊天工具提供的机器人接口,把脚本得到的文本消息转发过去。这部分和评论管理流程配合起来,反馈流转会更顺畅。

六、技术优缺点

6.1 优点

第一,节省时间。不用人工去翻几十个 PR 的评论区。

第二,反馈闭环更完整。有抓取、有通知、有状态记录,意见不会沉底。

第三,可扩展。拿到评论数据以后,想做统计、报表、自动催办都很方便。

第四,成本低。只用 Node.js 和 Bitbucket 的开放接口,不需要购买额外工具。

6.2 缺点

第一,依赖 Bitbucket API 的稳定性和字段结构。API 一旦变化,脚本需要跟着调整。

第二,提醒如果太频繁,容易变成一种骚扰,开发者的抵触情绪会上升。

第三,自动追加评论有风险。如果脚本判断错误,会把无用评论刷得满屏都是。

第四,权限管理要小心。一个带错权限的 Token,可能会让不该看到评论的人看到所有内容。

6.3 折中方案

如果团队还不想上自动化,可以先从人工约定开始。比如规定:所有评论必须有“@负责人”和“截止时间”;合并前必须检查是否还有未解决评论。等这个约定稳定了,再用脚本去代替重复劳动。这样既不会太激进,也能逐步养成习惯。

七、注意事项

7.1 Token 权限越小越好

用于自动化脚本的 Token,建议只授予“读取评论”和“发表评论”两个权限。不要使用拥有写全部仓库权限的账号,更不要把个人密码当 Token 用。密码泄露的代价太大。

7.2 注意 API 限流

Bitbucket 对 API 请求有频率限制。如果脚本里循环处理几百个 PR,一瞬间发出大量请求,很容易触发限流。建议在每次请求之间加一点延迟,或者错开执行时间。

7.3 不要自动把评论标记为已解决

评论的处理最终应该由人来做,脚本可以辅助提醒,但不要把“已解决”这个动作也完全自动化。如果脚本判断错了,把还没处理的评论标记成已解决,问题就会被掩盖。更稳妥的做法是,脚本只负责收集和提醒,找到解决状态后再让开发者在页面上手动勾选。

7.4 记得处理敏感信息

评论内容可能包含路径、服务器地址、客户数据等敏感信息。把评论拉到本地或者聊天工具时,要注意脱敏,不要随手发到公共频道。

7.5 评论提醒的措辞要友好

自动提醒不是问责,而是协助。消息里尽量用“有两个评论需要确认”这样的语气,不要用“你又没处理评论”这种话。好的工具体验,应该让人感受到帮助,而不是压力。

八、总结

代码审查里最怕的不是“有意见”,而是“有意见没有下文”。Bitbucket 的评论功能给了我们一个收集意见的地方,但它本身不会自动推动反馈流转。借助 Node.js 脚本,我们可以定时抓取评论、筛选出未解决项、生成提醒,再把提醒发给对的人。这样评论就不再是一堆漂浮在页面里的文字,而是一条条有主人、有状态、有截止时间的任务。

实际推动的时候,也别一上来就写复杂系统。可以先从一个“每日提醒”脚本开始,跑通后再慢慢加功能。工具只是辅助,最终还是要靠团队的共识。大家约定了“评论必须有人处理”,脚本才会成为那句话的监督者。