news 2026/9/16 16:21:16

prek Hook Groups 实战指南:用 `--group` 打造自定义 Git Hook 运行画像

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
prek Hook Groups 实战指南:用 `--group` 打造自定义 Git Hook 运行画像

prek Hook Groups 实战指南:用--group打造自定义 Git Hook 运行画像

【免费下载链接】prek⚡ A fast Git hook manager written in Rust, designed as a drop-in alternative to pre-commit, reimagined.项目地址: https://gitcode.com/GitHub_Trending/pr/prek

导读

prek 是一个用 Rust 编写、可平替 pre-commit 的快速 Git Hook 管理器。本文围绕其特有的Hook Groups(钩子分组)机制展开:通过给 Hook 打上自定义标签,并借助prek run--group--require-group--no-group三个可重复选项,你可以按 CI、Agent、发布等任意执行画像来挑选要运行的 Hook,而无需改变 Git Hook stage 语义。读完本文,你将掌握groups字段的配置规范、三过滤器组合逻辑、与 stage 的交互规则、工作区行为及全部边界情况,并深入理解其在当前仓库中的源码实现。

该机制的完整设计源自 docs/proposals/hook-groups.md,且功能已在当前仓库中落地实现——从配置解析、CLI 参数到选择器与运行管线均有对应源码,本文会逐层给出佐证。

为什么需要 Hook Groups

prek run --all-files已经能很好地覆盖 CI、Agent 工作流以及各类显式命令行场景。但现实中的项目往往需要只运行项目自定义的 Hook 子集

  • CI 只跑 lint 与格式化 Hook,而把测试交给独立的 job;
  • CI 把检查类与格式化类 Hook 拆分到不同 job;
  • Agent 在提交前运行慢速类型检查器,而人类贡献者不应在每次提交时被这些慢 Hook 阻塞;
  • 某些 Hook 本地默认不启用,但要在特定的 MR 或发布流程中开启;
  • 某些 Hook 依赖大型本地工具链或用户刻意回避的生态,需要从本地运行中排除。

提案明确指出:stages描述的是Git Hook 上下文,而上述场景描述的是任意执行画像(execution profile)。用 stage 近似实现(例如给 Hook 加manual再用prek run --stage manual)既令人困惑,又会让调用方反复与 Git 阶段语义纠缠。若为cidockerreleaseagent等逐一新增专用 stage,则会重复同样的问题并需要无限扩张词汇表。因此设计目标是:增加一个轻量的 Hook 级标签机制,让用户自己定义运行画像,同时完全不改变 Git Hook stage 语义。

配置:Hook 级别的groups字段

YAML(.pre-commit-config.yaml

groups是项目 Hook 配置中新增的可选字段:

repos: - repo: local hooks: - id: format name: Format Python entry: ruff format language: system groups: ["format", "ci"] - id: lint name: Lint Python entry: ruff check language: system groups: ["lint", "ci"] - id: typecheck name: Typecheck Python entry: pyright language: system groups: ["slow", "agent"]

TOML(prek.toml

prek.toml使用完全相同的字段名:

[[repos]] repo = "local" hooks = [ { id = "format", name = "Format Python", entry = "ruff format", language = "system", groups = ["format", "ci"], }, ]

字段属性一览

| 属性 | 值 | | -- | -- | | 类型 | 字符串列表 | | 默认值 | 空列表 | | 作用域 | 仅限项目配置文件(project configuration) | | 匹配方式 | 精确匹配、大小写敏感 | | 命名约束 | 非空字符串、不含空白字符、不以@开头 |

prek 专属字段:不进入远程 Hook manifest

groupsprek 独有字段,不属于上游 pre-commit,也不应被当作远程 Hook manifest 的元数据。分组描述的是"当前项目希望如何运行 Hook",而非某个 Hook 仓库的内在属性。如果远程 manifest(.pre-commit-hooks.yaml)中出现了groups,prek 会警告并忽略该字段,与它处理priority等其他仅配置生效字段的方式一致。

这一行为在源码中有明确体现:远程 manifest 的解析类型是ManifestHook(见 crates/prek/src/config/hook.rs),它只包含idnameentrylanguageoptions没有groups字段;而项目配置侧的RemoteHookLocalHook才声明groups(crates/prek/src/config/hook.rs)。HookWire中通过#[serde(flatten)] _unused_keys收集未知键(crates/prek/src/config/hook.rs),保证多余字段不会导致解析崩溃,而是被安全地忽略。

名称校验:解析期即拦截非法值

groups的反序列化走deserialize_groups(crates/prek/src/config/priority.rs),它在配置解析阶段就对每个名称调用validate_group_name(crates/prek/src/config/priority.rs),校验规则为:非空、不含任意空白字符(char::is_whitespace)、不以@开头。对应测试hook_groups_reject_invalid_names_during_deserialize位于 crates/prek/src/config/mod.rs,用于逐条验证非法名称在反序列化时被拒绝。

CLI:三个可重复的过滤器

prek run新增三个可重复选项:

prek run --group <name> prek run --require-group <name> prek run --no-group <name>

各自的语义

  • --group <name>包含过滤器(并集)。指定一个或多个分组时,Hook 只要groups中包含任意一个被请求的分组即被选中。
  • --require-group <name>交集过滤器。Hook 只有在groups同时包含每一个被要求的分组时才被选中。
  • --no-group <name>排除过滤器。Hook 的groups中只要包含任意一个被排除的分组即被移除。

特殊选择器@ungrouped

@ungrouped是保留的特殊选择器,匹配有效groups列表为空的 Hook。它像普通分组成员一样可与三个选项组合使用,但不能配置在 Hook 的groups列表中(名称校验的@前缀保留规则正是为此服务)。

组合规则:顺序无关,排除优先

三个选项独立组合,与参数顺序无关,且排除优先

  1. 若提供了任意--group,先选出与这些分组匹配的 Hook;
  2. 若提供了任意--require-group,仅保留与每一个必选分组都匹配的 Hook;
  3. 若提供了任意--no-group,移除与任意排除分组匹配的 Hook。

等价地,一个 Hook 必须同时满足:

(如果指定了 --group:命中任意一个) AND (命中每一个 --require-group) AND (不命中任何 --no-group)

常用示例

prek run --all-files --group ci prek run --all-files --group lint --group typecheck prek run --all-files --require-group lint --require-group fast prek run --all-files --no-group format prek run --all-files --group ci --require-group lint --no-group slow prek run --all-files --group ci --group @ungrouped prek run --all-files --group ci --stage pre-push

组合示例:fast AND (format OR lint-only)

考虑如下 Hook 分组:

| Hook | Groups | | -- | -- | |ty|lint-only,fast,local| |ruff-format|format,fast,local| |mypy|lint-only,slow,ci| |black|format,slow,ci|

prek run --all-files --require-group fast --group format --group lint-only

这表示fast AND (format OR lint-only),因此恰好选中tyruff-format。调整选项顺序不会改变选择结果。

源码实现:GroupFilters

以上语义在 crates/prek/src/cli/run/selector.rs 的GroupFilters中实现:内部维护include_anyrequire_allexclude_any三个GroupSelector列表;GroupSelectorNamed(String)Ungrouped的枚举,UNGROUPED_GROUP = "@ungrouped"(crates/prek/src/cli/run/selector.rs)。matches_groups的实现严格遵循"排除优先、include 任意、require 全部"的逻辑,并对每次命中记录 usage 以便后续报告未匹配的过滤器(crates/prek/src/cli/run/selector.rs)。

CLI 参数在RunArgs中定义(crates/prek/src/cli/mod.rs):groupsrequired_groupsno_groups三个Vec<String>分别对应--group--require-group--no-group,均为可重复参数,并归入 "Hook selection" 帮助分组。注意prek run --stage的帮助文档也明确写道:不指定 stage 且无 group 过滤器时,默认先取pre-commit阶段 Hook,若命中了命令指定的 Hook ID 再回退匹配manual;而使用任一 group 选项时省略 stage 则允许任意阶段匹配。

选择模型:过滤器的作用顺序

分组过滤与现有的项目/Hook 选择器叠加生效,有效选择顺序如下:

  1. 从选中的项目加载 Hook;
  2. 应用位置参数的 Hook 或项目包含选择;
  3. 应用--skip选择器与 skip 环境变量;
  4. 应用分组的包含(include)、交集(require)与排除(exclude)过滤;
  5. 若提供了显式--stage,应用 stage 过滤;
  6. 若既无 group 过滤器也无显式--stage,沿用现有默认pre-commitstage 过滤与 Hook 目标的manual回退;
  7. 应用现有的文件匹配与运行输入逻辑。

例如:

prek run frontend/ --group ci --no-group slow

会运行frontend/目录下所有标记了ci、但标记slow的 Hook。

在 crates/prek/src/cli/run/run.rs 中可以看到该顺序的落地:先解析GroupFilters,再在 Hook 初始化后依次经过selectors.select_hook(include/skip)与group_filters.matches_hook(分组过滤)双重要求才进入selected_hooks

被分组过滤排除的 Hook 绝不会被安装或执行——这一点对依赖大型工具链、本地不支持的依赖或用户刻意回避的生态的 Hook 尤其重要。从源码看,分组过滤甚至在克隆远程仓库之前就生效:HookInitFilters::keeps_configured_hookkeeps_remote_repo会基于配置信息先行排除不可能匹配的远程仓库(crates/prek/src/workspace.rs),ProjectInitPlan::new据此跳过整个仓库配置(crates/prek/src/workspace.rs)。也就是说,--no-group排除的 Hook 不仅不会执行,连其语言环境都不会被准备、安装或克隆。

与 Git Hook stage 的交互

分组与 Git Hook stage 相互独立,这也是整个设计最核心的部分。

未使用 group 选项时:行为不变

不使用--group--require-group--no-group时,现有 stage 行为完全不变:省略--stage/--hook-stage时先选择pre-commit阶段符合条件的 Hook;若没有选中且命令点名了 Hook ID,则用相同 ID 再匹配配置为manual的 Hook。

使用 group 选项且未显式指定 stage:进入分组选择模式

一旦使用了任一 group 选项而未显式给出--stageprek run进入分组选择模式

  • 任意配置阶段的 Hook 都可能被匹配;
  • 不再执行针对manual的第二次点名匹配;
  • 文件输入按普通手动prek run文件模式收集:显式--files/--directory--all-files、合并冲突文件或暂存文件;
  • 仅配置了commit-msg和/或prepare-commit-msg的 Hook 无法在此模式下运行——因为它们需要 Git 的消息文件参数,会被过滤掉。

如果消息文件过滤把分组过滤匹配到的 Hook 全部移除,prek run警告并失败而不是静默成功。源码中的infer_stage_and_input_mode(crates/prek/src/cli/run/run.rs)正是实现:存在 group 过滤器且无显式 stage 时返回(None, RunInputMode::Files);随后uses_only_message_file_input(crates/prek/src/cli/run/run.rs)配合stage_uses_message_file_input(crates/prek/src/cli/filter.rs 中的stage_uses_message_file_input,实际定义于 crates/prek/src/cli/run/filter.rs)判断Stage::CommitMsg | Stage::PrepareCommitMsg。若最终为空,会输出警告 "all hooks selected by group filters requirecommit-msgorprepare-commit-msgstage ..." 并返回失败(crates/prek/src/cli/run/run.rs)。

其原理在于:手动prek run --group ...没有 Git Hook payload。所有非消息文件阶段都可以按各 Hook 自身的过滤规则与pass_filenames设置,用文件输入或无文件名执行;只有消息文件阶段需要无法推断的输入,因此在无 stage 的分组运行中不被选中。

分组 + 显式 stage:按交集组合

分组选择器与显式 stage 组合时按交集过滤:

prek run --group ci --stage pre-push

该命令只运行同时满足"标记了ci"与"适用于pre-push"的 Hook。等价的--hook-stage拼写行为相同。

这套设计让基于分组的 CI 用法保持简洁——prek run --group ci不需要用户给每个 CI Hook 添加manual;同时,当用户确实想将分组收窄到真实 Git Hook 上下文时也完全可行。

运行时语义:分组只决定"谁能跑"

分组只决定哪些 Hook 具备运行资格,之后的既有 Hook 行为完全不变:

  • filesexcludetypestypes_orexclude_types仍负责过滤文件;
  • always_run仅在 Hook 通过分组过滤后才生效;
  • pass_filenames: false仍是 Hook 执行设置,而非分组选择设置;
  • priority继续调度剩余的 Hook;
  • fail_fastrequire_serial、diff 检测、修改文件报告、输出处理与 Hook 结果语义均不变;
  • 语言支持检查仍适用于被选中的 Hook;
  • 既有的 Hook 缓存与安装行为只考虑通过分组过滤的 Hook。

如果分组过滤后没有任何 Hook 可运行,prek run应报告"没有 Hook 匹配请求的选择器"并返回失败——与显式选择器写错时的行为一致。运行管线中select_runnable_env_hooks(crates/prek/src/cli/run/run.rs)也印证了这一点:只有既通过分组过滤、又有匹配文件(或always_run)的 Hook 才会进入环境安装阶段。

工作区行为

在 workspace 模式下,分组名是跨所有选中项目应用的 CLI 选择器:

  • prek run --group ci选中每个选中项目中所有标记ci的 Hook;
  • 项目选择器可以先用项目前缀收窄 workspace 范围,再执行分组过滤;
  • 分组名无需全局声明
  • 分组名不跨项目配置文件协调调度

分组匹配按 Hook 逐一评估。没有匹配 Hook 的项目会直接退出本次运行。源码中run对 workspace 内每个项目独立执行init_hooks并传递同一组GroupFilters(crates/prek/src/cli/run/run.rs),随后统一经过selectors.select_hookgroup_filters.matches_hook双重过滤。

边界情况全览

未分组 Hook(ungrouped hooks)

省略groupsgroups: []的 Hook 视为未分组:

  • 未传任何 group 选项时,未分组 Hook 照常运行(行为与今天一致);
  • 传了--group <name>时,未分组 Hook 不匹配、不运行;--group @ungrouped可显式包含它们;
  • 传了--require-group <name>时,未分组 Hook 同样不匹配;
  • 只传--no-group <name>时,未分组 Hook 仍被选中,因为它们不属于被排除的分组。

@ungrouped也与其他过滤器配合:--require-group @ungrouped只保留未分组 Hook,--no-group @ungrouped则移除它们。在源码中,matches_hookGroupSelector::Ungrouped的判定是hook.groups.is_empty()(crates/prek/src/cli/run/selector.rs)。

多分组 Hook

一个 Hook 可以属于多个分组:

- id: ruff groups: ["lint", "python", "ci"]

它命中任意 include 分组、必须命中全部 required 分组、被任意 exclude 分组排除:

  • --group lint选中它;
  • --require-group lint --require-group ci选中它;
  • --require-group lint --require-group format不选中它;
  • --group ci --no-group python排除它;
  • --no-group format不排除它。

重复分组名

同一 Hook 上的重复分组名按单个分组成员关系处理,实现可在解析或匹配阶段去重。这也与运行时表示一致:Hook中的groups字段是BTreeSet<String>(crates/prek/src/hook.rs),构建时通过collect::<BTreeSet<_>>()天然去重(crates/prek/src/hook.rs)。例如groups: ["ci", "ci"]等价于groups: ["ci"]

非法分组名

分组名必须是非空字符串、不含空白字符、不以@开头。名称按原样精确匹配,实现不应在验证前对名称做 trim 或归一化@命名空间保留给特殊 CLI 选择器。

非法示例:

groups: ["", "ci slow", " ci", "ci\nslow", "@custom"]

除保留的@命名空间外,不应强制任何固定词汇表。ciagentslowformatlint都只是示例,不是保留名称。项目配置的 schema 正则^[^@\s]\S*$(见 crates/prek/src/config/hook.rs)正是这一约束的机器可读表达。

大小写敏感

分组匹配大小写敏感,CIci是不同分组。这避免了平台相关的归一化差异,也与大多数既有配置键/值的匹配行为一致。

未知 CLI 分组

  • 如果某个分组选择器没有匹配到任何 Hook,运行应失败,报错方式与未匹配的 Hook 选择器一致;
  • 如果请求了多个分组且至少一个有匹配,未匹配的分组名应产生警告而非让整个运行失败,这与现有选择器报告风格一致。

例如:

prek run --group ci --group does-not-exist

应运行ci分组的 Hook,并警告does-not-exist未匹配任何 Hook。GroupFilters::report_unused(crates/prek/src/cli/run/selector.rs)通过FilterUsage记录每个 include/require/exclude 是否被实际使用,未使用的以warn_user!输出(单个时)或列表形式(多个时)。

Skip 选择器

--skip持续生效,即使 Hook 匹配了分组也会被移除:

prek run --group ci --skip ruff

ruffHook 会被跳过。

环境变量 Skip

既有的 skip 环境变量继续生效,且本提案不为其新增分组语法。是否需要PREK_GROUPPREK_NO_GROUP之类的环境级分组选择,可另行讨论(详见 crates/prek-consts/src/env_vars.rs 中既有 skip 变量如SKIPPREK_SKIP的读取逻辑 crates/prek/src/cli/run/selector.rs)。

本地与远程 Hook

当项目配置可以附加 Hook 选项时,分组同等地适用于本地、远程、meta 与 builtin Hook。远程 Hook manifest 不应定义默认分组——远程仓库不知道消费项目的 CI、Agent 或本地工作流策略。若 manifest 中出现groups,该字段被忽略并给出警告(与"prek 专属字段"一节的源码证据一致)。

优先级调度

groups选择器,不是调度组。分组过滤之后,剩余 Hook 按既有priority语义调度;同priority的 Hook 仍可并行运行。属于同一groups值的 Hook 之间不产生任何顺序或并发关系。运行期的调度在 crates/prek/src/cli/run/run.rs 中体现:hooks.sort_by(|a, b| a.priority.cmp(&b.priority).then(a.idx.cmp(&b.idx))),随后按 priority 分组并发执行,与 groups 无关。相关文档也特别注明 "Priority aliases are not hook groups"(见 docs/reference/configuration.md)。

修改文件(modified files)

分组选择不改变修改文件检测。若被选中的 Hook 修改了文件,既有失败与 diff 报告行为照常生效。若 Hook 被分组过滤排除,它不可能产生文件修改,prek 也不应安装或执行它(与"被排除即不安装"一致)。

try-repo 不接受分组选项

prek try-repo不应接受--group--require-group--no-group。因为try-repo从远程 Hook manifest 构建临时项目配置,生成的配置只含 Hook ID,没有项目本地的groups元数据——接受分组过滤器会让命令看似支持分组选择,而每个生成的 Hook 实际都是未分组的。因此这些标志应由 CLI 直接拒绝(TryRepoArgs定义于 crates/prek/src/cli/mod.rs,不含任何 group 参数)。

list 命令

prek list接受与prek run相同的--group--require-group--no-group过滤器,让用户在不运行 Hook 的情况下预览某分组表达式会选中哪些 Hook。其实现复用同一GroupFilters(crates/prek/src/cli/list.rs),并对selectorsgroup_filtersstagelanguage做联合过滤。该功能--output-format=json添加分组元数据,暴露该数据是另一项独立变更(SerializableHook结构见 crates/prek/src/cli/list.rs,目前不包含 groups 字段)。

安装行为:不持久化分组

本提案不引入prek install --group。把分组选择持久化到已安装的 Git shim 中,会让"普通 Git Hook 到底跑什么"难以审视。已安装 Hook 应继续使用 stage 语义。希望按画像执行的用户可以在 CI、Agent 工作流或贡献者文档中显式调用prek run --group ...。未来的提案可以讨论安装期分组或默认分组,但那会改变默认运行的含义,应单独设计。

非目标(Non-goals)

本提案新增:

  • 新的cistage;
  • 调度器或依赖组(priority仍是唯一调度机制);
  • DAG 调度或after依赖;
  • --output-format=grouped
  • 默认禁用(default-disabled)的 Hook;
  • 用于分组选择的环境变量;
  • prek install --group
  • prek try-repo --groupprek try-repo --require-groupprek try-repo --no-group
  • 全局分组声明,或针对根级列表的校验。

向后兼容性

  • 不使用groups--group--require-group--no-group时,行为完全不变;
  • 既有stages行为在普通prek run与已安装 Git Hook 执行中不变;唯一新增行为是显式分组选择模式,以及该模式与显式 stage 求交的能力;
  • @开头的分组名被拒绝,防止特殊选择器与配置的分组成员冲突;
  • 现有配置中未知的groups键目前会被忽略;本提案实现后该键开始有意义。这是可接受的:它是增量特性,未知键不属于保证稳定的行为契约。

实现落地与测试验证

虽然文档以 proposal 形式呈现,当前仓库中该功能已完整实现,实现要点与提案一一对应:

  1. groups已加入配置 Hook 类型(HookWire/RemoteHook/LocalHook,见 crates/prek/src/config/hook.rs)与构建后的Hook类型(crates/prek/src/hook.rs);
  2. 分组名在配置解析期通过deserialize_groups校验(crates/prek/src/config/priority.rs);
  3. prek run已加入可重复的--group/--require-group/--no-group参数(crates/prek/src/cli/mod.rs);
  4. 分组过滤发生在显式 stage 过滤与安装选择之前(crates/prek/src/cli/run/run.rs);
  5. group 模式激活且未显式--stage时跳过默认 stage 过滤(infer_stage_and_input_mode,crates/prek/src/cli/run/run.rs);
  6. 分组选择器在prek try-repo中不可用(TryRepoArgs无对应字段);
  7. schema 与正式文档已同步更新:groups的机器可读约束体现在 schema 正则^[^@\s]\S*$,正式使用文档见 docs/reference/configuration.md,CLI 参考见 docs/reference/cli.md(--group--require-group--no-group条目);
  8. 测试覆盖 include、require 交集、exclude、混合分组过滤器、未分组 Hook、显式 stage 交集、workspace 选择与"被排除 Hook 不安装"等场景——非法名称拒绝测试见 crates/prek/src/config/mod.rs,Hook 构建测试中亦包含groups: Some(vec!["ci", "format"])的合并用例(crates/prek/src/hook.rs)。

总结

Hook Groups 是 prek 在 Git Hook 管理上的关键差异化能力:它以"轻量标签 + 三个可重复 CLI 过滤器"的方式,把"何时运行哪些 Hook"从 Git stage 语义中彻底解耦,让 CI、Agent、本地开发、发布流程各自拥有清晰、可组合、可审计的执行画像。--group做并集包含,--require-group做交集收窄,--no-group做排除,@ungrouped补齐未分组场景,排除优先、顺序无关的组合规则保证了可预测性。配合"被排除即不克隆、不安装、不执行"的实现策略,它还天然解决了大型工具链与慢速检查器带来的资源与体验问题。这套机制既保持了与既有配置、stage 行为及 pre-commit 生态的向后兼容,又为未来的默认分组、安装期分组等演进预留了清晰边界。

【免费下载链接】prek⚡ A fast Git hook manager written in Rust, designed as a drop-in alternative to pre-commit, reimagined.项目地址: https://gitcode.com/GitHub_Trending/pr/prek

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:20:28

Django实现视频点播:MTV架构、ORM模型与部署实战

简介&#xff1a;基于Django框架的视频点播网站完整源码包&#xff0c;主要面向计算机、数学、电子信息等专业的学生&#xff0c;适合用作课程设计、期末大作业或毕业设计参考资料。项目实现了视频播放、收藏、后台管理等核心功能&#xff0c;页面与后端逻辑均打包在内&#xf…

作者头像 李华
网站建设 2026/9/16 16:18:33

基于多智能体大语言模型的中文金融分析框架解析

1. 项目背景与核心价值这个名为TradingAgents-CN的开源项目&#xff0c;本质上是一个基于多智能体大语言模型的中文金融分析框架。它最吸引人的地方在于&#xff0c;将前沿的AI技术与传统金融分析进行了深度融合&#xff0c;打造了一个专门面向中文市场的股票研究平台。从技术架…

作者头像 李华