ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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 Groupsprek run --all-files已经能很好地覆盖 CI、Agent 工作流以及各类显式命令行场景。但现实中的项目往往需要只运行项目自定义的 Hook 子集CI 只跑 lint 与格式化 Hook而把测试交给独立的 jobCI 把检查类与格式化类 Hook 拆分到不同 jobAgent 在提交前运行慢速类型检查器而人类贡献者不应在每次提交时被这些慢 Hook 阻塞某些 Hook 本地默认不启用但要在特定的 MR 或发布流程中开启某些 Hook 依赖大型本地工具链或用户刻意回避的生态需要从本地运行中排除。提案明确指出stages描述的是Git Hook 上下文而上述场景描述的是任意执行画像execution profile。用 stage 近似实现例如给 Hook 加manual再用prek run --stage manual既令人困惑又会让调用方反复与 Git 阶段语义纠缠。若为ci、docker、release、agent等逐一新增专用 stage则会重复同样的问题并需要无限扩张词汇表。因此设计目标是增加一个轻量的 Hook 级标签机制让用户自己定义运行画像同时完全不改变 Git Hook stage 语义。配置Hook 级别的groups字段YAML.pre-commit-config.yamlgroups是项目 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]TOMLprek.tomlprek.toml使用完全相同的字段名[[repos]] repo local hooks [ { id format, name Format Python, entry ruff format, language system, groups [format, ci], }, ]字段属性一览| 属性 | 值 | | -- | -- | | 类型 | 字符串列表 | | 默认值 | 空列表 | | 作用域 | 仅限项目配置文件project configuration | | 匹配方式 | 精确匹配、大小写敏感 | | 命名约束 | 非空字符串、不含空白字符、不以开头 |prek 专属字段不进入远程 Hook manifestgroups是prek 独有字段不属于上游 pre-commit也不应被当作远程 Hook manifest 的元数据。分组描述的是当前项目希望如何运行 Hook而非某个 Hook 仓库的内在属性。如果远程 manifest.pre-commit-hooks.yaml中出现了groupsprek 会警告并忽略该字段与它处理priority等其他仅配置生效字段的方式一致。这一行为在源码中有明确体现远程 manifest 的解析类型是ManifestHook见 crates/prek/src/config/hook.rs它只包含id、name、entry、language与options没有groups字段而项目配置侧的RemoteHook与LocalHook才声明groupscrates/prek/src/config/hook.rs。HookWire中通过#[serde(flatten)] _unused_keys收集未知键crates/prek/src/config/hook.rs保证多余字段不会导致解析崩溃而是被安全地忽略。名称校验解析期即拦截非法值groups的反序列化走deserialize_groupscrates/prek/src/config/priority.rs它在配置解析阶段就对每个名称调用validate_group_namecrates/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中只要包含任意一个被排除的分组即被移除。特殊选择器ungroupedungrouped是保留的特殊选择器匹配有效groups列表为空的 Hook。它像普通分组成员一样可与三个选项组合使用但不能配置在 Hook 的groups列表中名称校验的前缀保留规则正是为此服务。组合规则顺序无关排除优先三个选项独立组合与参数顺序无关且排除优先若提供了任意--group先选出与这些分组匹配的 Hook若提供了任意--require-group仅保留与每一个必选分组都匹配的 Hook若提供了任意--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)因此恰好选中ty与ruff-format。调整选项顺序不会改变选择结果。源码实现GroupFilters以上语义在 crates/prek/src/cli/run/selector.rs 的GroupFilters中实现内部维护include_any、require_all、exclude_any三个GroupSelector列表GroupSelector是Named(String)与Ungrouped的枚举UNGROUPED_GROUP ungroupedcrates/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.rsgroups、required_groups、no_groups三个VecString分别对应--group、--require-group、--no-group均为可重复参数并归入 Hook selection 帮助分组。注意prek run --stage的帮助文档也明确写道不指定 stage 且无 group 过滤器时默认先取pre-commit阶段 Hook若命中了命令指定的 Hook ID 再回退匹配manual而使用任一 group 选项时省略 stage 则允许任意阶段匹配。选择模型过滤器的作用顺序分组过滤与现有的项目/Hook 选择器叠加生效有效选择顺序如下从选中的项目加载 Hook应用位置参数的 Hook 或项目包含选择应用--skip选择器与 skip 环境变量应用分组的包含include、交集require与排除exclude过滤若提供了显式--stage应用 stage 过滤若既无 group 过滤器也无显式--stage沿用现有默认pre-commitstage 过滤与 Hook 目标的manual回退应用现有的文件匹配与运行输入逻辑。例如prek run frontend/ --group ci --no-group slow会运行frontend/目录下所有标记了ci、但未标记slow的 Hook。在 crates/prek/src/cli/run/run.rs 中可以看到该顺序的落地先解析GroupFilters再在 Hook 初始化后依次经过selectors.select_hookinclude/skip与group_filters.matches_hook分组过滤双重要求才进入selected_hooks。被分组过滤排除的 Hook 绝不会被安装或执行——这一点对依赖大型工具链、本地不支持的依赖或用户刻意回避的生态的 Hook 尤其重要。从源码看分组过滤甚至在克隆远程仓库之前就生效HookInitFilters::keeps_configured_hook与keeps_remote_repo会基于配置信息先行排除不可能匹配的远程仓库crates/prek/src/workspace.rsProjectInitPlan::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_modecrates/prek/src/cli/run/run.rs正是实现存在 group 过滤器且无显式 stage 时返回(None, RunInputMode::Files)随后uses_only_message_file_inputcrates/prek/src/cli/run/run.rs配合stage_uses_message_file_inputcrates/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 行为完全不变files、exclude、types、types_or、exclude_types仍负责过滤文件always_run仅在 Hook 通过分组过滤后才生效pass_filenames: false仍是 Hook 执行设置而非分组选择设置priority继续调度剩余的 Hookfail_fast、require_serial、diff 检测、修改文件报告、输出处理与 Hook 结果语义均不变语言支持检查仍适用于被选中的 Hook既有的 Hook 缓存与安装行为只考虑通过分组过滤的 Hook。如果分组过滤后没有任何 Hook 可运行prek run应报告没有 Hook 匹配请求的选择器并返回失败——与显式选择器写错时的行为一致。运行管线中select_runnable_env_hookscrates/prek/src/cli/run/run.rs也印证了这一点只有既通过分组过滤、又有匹配文件或always_run的 Hook 才会进入环境安装阶段。工作区行为在 workspace 模式下分组名是跨所有选中项目应用的 CLI 选择器prek run --group ci选中每个选中项目中所有标记ci的 Hook项目选择器可以先用项目前缀收窄 workspace 范围再执行分组过滤分组名无需全局声明分组名不跨项目配置文件协调调度。分组匹配按 Hook 逐一评估。没有匹配 Hook 的项目会直接退出本次运行。源码中run对 workspace 内每个项目独立执行init_hooks并传递同一组GroupFilterscrates/prek/src/cli/run/run.rs随后统一经过selectors.select_hook与group_filters.matches_hook双重过滤。边界情况全览未分组 Hookungrouped hooks省略groups或groups: []的 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_hook对GroupSelector::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字段是BTreeSetStringcrates/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]除保留的命名空间外不应强制任何固定词汇表。ci、agent、slow、format、lint都只是示例不是保留名称。项目配置的 schema 正则^[^\s]\S*$见 crates/prek/src/config/hook.rs正是这一约束的机器可读表达。大小写敏感分组匹配大小写敏感CI与ci是不同分组。这避免了平台相关的归一化差异也与大多数既有配置键/值的匹配行为一致。未知 CLI 分组如果某个分组选择器没有匹配到任何 Hook运行应失败报错方式与未匹配的 Hook 选择器一致如果请求了多个分组且至少一个有匹配未匹配的分组名应产生警告而非让整个运行失败这与现有选择器报告风格一致。例如prek run --group ci --group does-not-exist应运行ci分组的 Hook并警告does-not-exist未匹配任何 Hook。GroupFilters::report_unusedcrates/prek/src/cli/run/selector.rs通过FilterUsage记录每个 include/require/exclude 是否被实际使用未使用的以warn_user!输出单个时或列表形式多个时。Skip 选择器--skip持续生效即使 Hook 匹配了分组也会被移除prek run --group ci --skip ruffruffHook 会被跳过。环境变量 Skip既有的 skip 环境变量继续生效且本提案不为其新增分组语法。是否需要PREK_GROUP、PREK_NO_GROUP之类的环境级分组选择可另行讨论详见 crates/prek-consts/src/env_vars.rs 中既有 skip 变量如SKIP、PREK_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。其实现复用同一GroupFilterscrates/prek/src/cli/list.rs并对selectors、group_filters、stage、language做联合过滤。该功能不向--output-formatjson添加分组元数据暴露该数据是另一项独立变更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-formatgrouped默认禁用default-disabled的 Hook用于分组选择的环境变量prek install --groupprek try-repo --group、prek try-repo --require-group、prek try-repo --no-group全局分组声明或针对根级列表的校验。向后兼容性不使用groups、--group、--require-group、--no-group时行为完全不变既有stages行为在普通prek run与已安装 Git Hook 执行中不变唯一新增行为是显式分组选择模式以及该模式与显式 stage 求交的能力以开头的分组名被拒绝防止特殊选择器与配置的分组成员冲突现有配置中未知的groups键目前会被忽略本提案实现后该键开始有意义。这是可接受的它是增量特性未知键不属于保证稳定的行为契约。实现落地与测试验证虽然文档以 proposal 形式呈现当前仓库中该功能已完整实现实现要点与提案一一对应groups已加入配置 Hook 类型HookWire/RemoteHook/LocalHook见 crates/prek/src/config/hook.rs与构建后的Hook类型crates/prek/src/hook.rs分组名在配置解析期通过deserialize_groups校验crates/prek/src/config/priority.rsprek run已加入可重复的--group/--require-group/--no-group参数crates/prek/src/cli/mod.rs分组过滤发生在显式 stage 过滤与安装选择之前crates/prek/src/cli/run/run.rsgroup 模式激活且未显式--stage时跳过默认 stage 过滤infer_stage_and_input_modecrates/prek/src/cli/run/run.rs分组选择器在prek try-repo中不可用TryRepoArgs无对应字段schema 与正式文档已同步更新groups的机器可读约束体现在 schema 正则^[^\s]\S*$正式使用文档见 docs/reference/configuration.mdCLI 参考见 docs/reference/cli.md--group、--require-group、--no-group条目测试覆盖 include、require 交集、exclude、混合分组过滤器、未分组 Hook、显式 stage 交集、workspace 选择与被排除 Hook 不安装等场景——非法名称拒绝测试见 crates/prek/src/config/mod.rsHook 构建测试中亦包含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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进