anti-slop源码深度解析(二):inherited数据机制如何消灭非默认分支PR的误报

发布时间:2026/10/26 1:36:28
anti-slop源码深度解析(二):inherited数据机制如何消灭非默认分支PR的误报

【免费下载链接】anti-slop

A GitHub action that detects and automatically closes low-quality and AI slop PRs.

项目地址:https://gitcode.com/gh_mirrors/an/anti-slop
点击查看免费下载

anti-slop 是一个自动检测并关闭低质量与 AI slop PR 的 GitHub Action。本文从源码层面拆解它的 inherited 数据机制,讲清楚它如何用一次 API 比较调用,消灭 PR 指向非默认分支(如next)时产生的误报,让检查只评价作者真正写出的代码。

🧩 一、问题背景:为什么非默认分支 PR 会"背锅"

多数仓库的 PR 直接指向默认分支main,但不少团队采用发布流:日常开发先进next分支,再合回main。贡献者的 PR 常常指向next。

当next落后于main时,问题出现了:

main(默认分支): A ── B ── C ── D │ next(PR目标分支): A ── B ── C │ feature(PR源分支): A ── B ── C ── D ── E ↑ ↑ 继承而来 E:作者真正的提交
  • 贡献者通常基于main拉分支,feature 分支天然包含提交D;
  • PR 指向next时,GitHub API 返回的 commit 列表和文件列表里会带出D以及它改动的文件;
  • 这些内容不是本次 PR 作者写的,但 commit 检查、file 检查会照单全收 → 误报;
  • 误报累计达到max-failures(默认 4)时,一个无辜 PR 就会被自动关闭。

官方 CHANGELOG.md 用两个版本逐步修掉了这个问题:

  • v0.2.0:将继承的默认分支提交排除出 commit 检查
  • v0.2.1:将继承文件排除出 file 检查,并把检测逻辑抽取为共享模块

🗂️ 二、inherited 数据是什么

一句话定义:inherited 数据 = 仓库默认分支上已有、但 PR 目标分支尚未合入的那批 commit 和文件。

数据结构非常简单,定义在 src/types/inherited-data.ts:

export interface InheritedData { shas: Set<string>; // 继承的 commit sha files: Set<string>; // 继承的文件路径 }

两个Set,各自提供 O(1) 的成员查找。下面看它们怎么产生。

⚙️ 三、getInheritedData 源码拆解:一次调用拿到全部

核心实现在 src/inherited.ts,总共 30 行左右,关键有 4 步。

第 1 步:默认分支短路

if (context.baseBranch === context.defaultBranch) return { shas: new Set(), files: new Set() };

见 src/inherited.ts。PR 直接指向main时,继承数据必然为空,直接返回空集合,一次 API 都不发。这是最常见的场景,这一行省掉了绝大多数请求。baseBranch与defaultBranch来自 PR 事件 payload,提取逻辑在 src/context.ts。

第 2 步:一次 compare 拿到全部

const { data } = await client.rest.repos.compareCommitsWithBasehead({ owner: context.owner, repo: context.repo, basehead: `${context.baseBranch}...${context.defaultBranch}`, });

见 src/inherited.ts。注意三点语法:next...main表示"在main上但不在next上的 commit",方向恰好就是继承集合,反过来比就不对了。

第 3 步:构建两个 Set

const shas = new Set(data.commits.map((commit) => commit.sha)); const files = new Set((data.files ?? []).map((file) => file.filename));

见 src/inherited.ts。

第 4 步:输出审计日志

对每个继承的 commit 和文件各打一行core.info,最后汇总Found X inherited commit(s) and Y inherited file(s)(src/inherited.ts)。维护者在 workflow 日志里一眼就能看到哪些内容被排除了。

🔍 四、inherited 数据的两个消费点

4.1 commit 检查:按 sha 过滤

src/checks/commit-checks.ts 在拉取 PR 全部 commit 后立即过滤:

const commits = allCommits.filter((commit) => !inheritedShas.has(commit.sha));

后续的 commit 消息长度、Conventional Commits、commit 作者匹配、block 作者四项检查,全部跑在这份过滤后的列表上——继承 commit 永远不会被检查。

4.2 file 检查:按文件名过滤

src/checks/file-checks.ts 同样的思路:

const files = changedFiles .filter((file) => !inheritedFiles.has(file.filename))

文件扩展名、允许路径、block 路径、末尾换行、新增注释数这五项 file 检查,都被这一行豁免了继承文件。

4.3 单点采集、双点消费

src/main.ts 中的调用顺序是:

  1. 先跑 branch / size / title 检查(不需要 GitHub API);
  2. 再调用一次getInheritedData,得到{ shas, files };
  3. shas传给 commit 检查,files传给 file 检查。

全程只发一次compare API 请求,供两组检查共享——这正是 v0.2.1 重构要达到的效果。

💡 五、inherited 数据机制的 3 个设计亮点

设计点效果
默认分支短路判断最常见场景(指向main)零 API 调用
一次 compare 供两组检查复用请求更少、整个 action 更快(官方宣传 15 秒内跑完)
只过滤内容类检查体积检查仍统计"相对目标分支的实际增量",与 GitHub 页面展示一致

第三点值得展开:max-changed-files/max-changed-lines并没有用 inherited 数据过滤,统计直接来自 PR payload(见 src/checks/size-checks.ts)。这是有意为之——继承而来的改动会随这个 PR 一起落到目标分支,计入体积是合理的;真正造成误报的,是"把作者没写的代码算到作者头上"。

📜 六、误报修复时间线

版本变化
v0.2.0commit 检查排除继承 commit,修复作者匹配、消息长度等检查的误报
v0.2.1file 检查同样排除继承文件;检测逻辑从 commit 检查中抽取为独立的 src/inherited.ts,两组检查共享一次比较调用

这条演进路径也展示了这类 bug 的通用修法:先在误报最痛的检查点堵漏,再重构为共享模块。

🧾 七、小结

  • 是什么:inherited 数据 = 默认分支上已有、PR 目标分支尚未合入的 commit 与文件;
  • 怎么来:一次compareCommitsWithBasehead(base...default),外加默认分支短路判断;
  • 怎么用:shas过滤 commit 检查、files过滤 file 检查,均为 O(1) 的Set查找;
  • 解决什么:anti-slop 只评价"作者真正写的部分",指向next等分支的 PR 不再被误关。

想继续深入,各检查实现都在 src/checks/ 目录下,整体入口是 src/main.ts。

【免费下载链接】anti-slop

A GitHub action that detects and automatically closes low-quality and AI slop PRs.

项目地址:https://gitcode.com/gh_mirrors/an/anti-slop
点击查看免费下载
上一篇:OpenMemories-Tweak:索尼相机隐藏功能深度解锁专业指南
下一篇:如何彻底解决Windows 11中PL2303驱动黄色感叹号问题:完整技术指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考