diff --git a/crates/prek/src/cli/hook_impl.rs b/crates/prek/src/cli/hook_impl.rs index fbf12c9a0..46c5c2f61 100644 --- a/crates/prek/src/cli/hook_impl.rs +++ b/crates/prek/src/cli/hook_impl.rs @@ -127,6 +127,7 @@ pub(crate) async fn hook_impl( skips, vec![], vec![], + vec![], Some(hook_type.into()), file_selection, false, diff --git a/crates/prek/src/cli/list.rs b/crates/prek/src/cli/list.rs index d4fc66eb4..13f8f930d 100644 --- a/crates/prek/src/cli/list.rs +++ b/crates/prek/src/cli/list.rs @@ -31,6 +31,7 @@ pub(crate) async fn list( includes: Vec, skips: Vec, groups: Vec, + required_groups: Vec, no_groups: Vec, hook_stage: Option, language: Option, @@ -41,7 +42,7 @@ pub(crate) async fn list( ) -> anyhow::Result { let workspace_root = Workspace::find_root(config.as_deref(), &CWD)?; let selectors = Selectors::load(&includes, &skips, &workspace_root)?; - let group_filters = GroupFilters::parse(&groups, &no_groups)?; + let group_filters = GroupFilters::parse(&groups, &required_groups, &no_groups)?; let workspace = Workspace::discover(store, workspace_root, config, Some(&selectors), refresh)?; let reporter = HookInitReporter::new(printer); diff --git a/crates/prek/src/cli/mod.rs b/crates/prek/src/cli/mod.rs index 688ba10b0..7d1080dae 100644 --- a/crates/prek/src/cli/mod.rs +++ b/crates/prek/src/cli/mod.rs @@ -646,9 +646,10 @@ pub(crate) struct RunArgs { /// When not specified and no group filter is active, this command starts with /// hooks eligible for `pre-commit`. If no hook is selected and the command /// named hook IDs, those same IDs are matched again against hooks configured - /// for `manual`. With `--group` or `--no-group`, omitting the stage lets - /// hooks from any configured stage match, using the default file input mode; - /// hooks that only run at `commit-msg` or `prepare-commit-msg` are ignored. + /// for `manual`. With `--group`, `--require-group`, or `--no-group`, omitting + /// the stage lets hooks from any configured stage match, using the default file + /// input mode; hooks that only run at `commit-msg` or `prepare-commit-msg` are + /// ignored. #[arg(long, value_enum, alias = "hook-stage")] pub(crate) stage: Option, @@ -658,6 +659,12 @@ pub(crate) struct RunArgs { #[arg(long = "group", value_name = "GROUP")] pub(crate) groups: Vec, + /// Run hooks belonging to every specified group. + /// + /// Can be specified multiple times. Composes with `--group` and `--no-group`. + #[arg(long = "require-group", value_name = "GROUP")] + pub(crate) required_groups: Vec, + /// Do not run hooks belonging to the specified group. /// /// Can be specified multiple times. Exclusion wins over inclusion. @@ -751,6 +758,12 @@ pub(crate) struct ListArgs { #[arg(long = "group", value_name = "GROUP")] pub(crate) groups: Vec, + /// Show hooks belonging to every specified group. + /// + /// Can be specified multiple times. Composes with `--group` and `--no-group`. + #[arg(long = "require-group", value_name = "GROUP")] + pub(crate) required_groups: Vec, + /// Do not show hooks belonging to the specified group. /// /// Can be specified multiple times. Exclusion wins over inclusion. diff --git a/crates/prek/src/cli/run/run.rs b/crates/prek/src/cli/run/run.rs index 48769fc9e..ffb400699 100644 --- a/crates/prek/src/cli/run/run.rs +++ b/crates/prek/src/cli/run/run.rs @@ -44,6 +44,7 @@ pub(crate) async fn run( includes: Vec, skips: Vec, groups: Vec, + required_groups: Vec, no_groups: Vec, hook_stage: Option, selection: FileSelection, @@ -76,7 +77,7 @@ pub(crate) async fn run( let workspace_root = Workspace::find_root(config.as_deref(), &CWD)?; let selectors = Selectors::load(&includes, &skips, &workspace_root)?; - let group_filters = GroupFilters::parse(&groups, &no_groups)?; + let group_filters = GroupFilters::parse(&groups, &required_groups, &no_groups)?; let has_group_filters = group_filters.has_filters(); let workspace = Workspace::discover(store, workspace_root, config, Some(&selectors), refresh)?; diff --git a/crates/prek/src/cli/run/selector.rs b/crates/prek/src/cli/run/selector.rs index 574900ee9..ec5d9491c 100644 --- a/crates/prek/src/cli/run/selector.rs +++ b/crates/prek/src/cli/run/selector.rs @@ -407,13 +407,18 @@ impl Selectors { #[derive(Debug, Clone, Default)] pub(crate) struct GroupFilters { - includes: Vec, - excludes: Vec, + include_any: Vec, + require_all: Vec, + exclude_any: Vec, usage: Arc>, } impl GroupFilters { - pub(crate) fn parse(includes: &[String], excludes: &[String]) -> Result { + pub(crate) fn parse( + include_any: &[String], + require_all: &[String], + exclude_any: &[String], + ) -> Result { let parse_groups = |flag: &'static str, groups: &[String]| { let mut seen = FxHashSet::default(); let mut names = Vec::new(); @@ -434,80 +439,72 @@ impl GroupFilters { }; Ok(Self { - includes: parse_groups("--group", includes)?, - excludes: parse_groups("--no-group", excludes)?, + include_any: parse_groups("--group", include_any)?, + require_all: parse_groups("--require-group", require_all)?, + exclude_any: parse_groups("--no-group", exclude_any)?, usage: Arc::default(), }) } pub(crate) fn has_filters(&self) -> bool { - !self.includes.is_empty() || !self.excludes.is_empty() + !self.include_any.is_empty() || !self.require_all.is_empty() || !self.exclude_any.is_empty() } - pub(crate) fn matches_hook(&self, hook: &Hook) -> bool { + fn matches_groups(&self, contains_group: impl Fn(&str) -> bool) -> bool { let mut usage = self.usage.lock().unwrap(); - let mut excluded = false; - for (idx, exclude) in self.excludes.iter().enumerate() { - if hook.groups.contains(exclude) { + let mut matches_any_excluded = false; + for (idx, exclude) in self.exclude_any.iter().enumerate() { + if contains_group(exclude) { usage.use_exclude(idx); - excluded = true; + matches_any_excluded = true; } } - if self.includes.is_empty() { - return !excluded; + let mut matches_any_included = self.include_any.is_empty(); + for (idx, include) in self.include_any.iter().enumerate() { + if contains_group(include) { + usage.use_include(idx); + matches_any_included = true; + } } - let mut included = false; - for (idx, include) in self.includes.iter().enumerate() { - if hook.groups.contains(include) { - usage.use_include(idx); - included = true; + let mut matches_all_required = true; + for (idx, requirement) in self.require_all.iter().enumerate() { + if contains_group(requirement) { + usage.use_requirement(idx); + } else { + matches_all_required = false; } } - included && !excluded + matches_any_included && matches_all_required && !matches_any_excluded + } + + pub(crate) fn matches_hook(&self, hook: &Hook) -> bool { + self.matches_groups(|group| hook.groups.contains(group)) } pub(crate) fn matches_configured_hook(&self, hook: &ConfiguredHook<'_>) -> bool { - let mut usage = self.usage.lock().unwrap(); - let contains_group = |group: &str| { + self.matches_groups(|group| { hook.groups .is_some_and(|groups| groups.iter().any(|hook_group| hook_group == group)) - }; - - let mut excluded = false; - for (idx, exclude) in self.excludes.iter().enumerate() { - if contains_group(exclude) { - usage.use_exclude(idx); - excluded = true; - } - } - - if self.includes.is_empty() { - return !excluded; - } - - let mut included = false; - for (idx, include) in self.includes.iter().enumerate() { - if contains_group(include) { - usage.use_include(idx); - included = true; - } - } - - included && !excluded + }) } pub(crate) fn report_unused(&self) { let usage = self.usage.lock().unwrap(); let unused = usage - .unused_includes(&self.includes) + .unused_includes(&self.include_any) .map(|(_, group)| format!("--group={group}")) .chain( usage - .unused_excludes(&self.excludes) + .unused_requirements(&self.require_all) + .map(|(_, group)| format!("--require-group={group}")), + ) + .chain( + usage + .unused_excludes(&self.exclude_any) .map(|(_, group)| format!("--no-group={group}")), ) .collect::>(); @@ -533,17 +530,22 @@ impl GroupFilters { #[derive(Default, Debug)] struct FilterUsage { - used_includes: FxHashSet, - used_excludes: FxHashSet, + includes: FxHashSet, + requirements: FxHashSet, + excludes: FxHashSet, } impl FilterUsage { fn use_include(&mut self, idx: usize) { - self.used_includes.insert(idx); + self.includes.insert(idx); + } + + fn use_requirement(&mut self, idx: usize) { + self.requirements.insert(idx); } fn use_exclude(&mut self, idx: usize) { - self.used_excludes.insert(idx); + self.excludes.insert(idx); } fn unused_includes<'a, T>( @@ -553,7 +555,7 @@ impl FilterUsage { values .iter() .enumerate() - .filter(|(idx, _)| !self.used_includes.contains(idx)) + .filter(|(idx, _)| !self.includes.contains(idx)) } fn unused_excludes<'a, T>( @@ -563,7 +565,17 @@ impl FilterUsage { values .iter() .enumerate() - .filter(|(idx, _)| !self.used_excludes.contains(idx)) + .filter(|(idx, _)| !self.excludes.contains(idx)) + } + + fn unused_requirements<'a, T>( + &'a self, + values: &'a [T], + ) -> impl Iterator + 'a { + values + .iter() + .enumerate() + .filter(|(idx, _)| !self.requirements.contains(idx)) } fn report_unused(&self, selectors: &Selectors) { diff --git a/crates/prek/src/cli/try_repo.rs b/crates/prek/src/cli/try_repo.rs index 2b71c4d64..de2f55124 100644 --- a/crates/prek/src/cli/try_repo.rs +++ b/crates/prek/src/cli/try_repo.rs @@ -281,6 +281,7 @@ pub(crate) async fn try_repo( vec![], vec![], vec![], + vec![], stage, file_selection, run_args.show_diff_on_failure, diff --git a/crates/prek/src/main.rs b/crates/prek/src/main.rs index 76befbc75..5dfb7ea11 100644 --- a/crates/prek/src/main.rs +++ b/crates/prek/src/main.rs @@ -284,6 +284,7 @@ async fn run(cli: Cli) -> Result { options.includes, options.skips, args.groups, + args.required_groups, args.no_groups, args.stage, file_selection, @@ -306,6 +307,7 @@ async fn run(cli: Cli) -> Result { args.includes, args.skips, args.groups, + args.required_groups, args.no_groups, args.hook_stage, args.language, diff --git a/crates/prek/tests/list.rs b/crates/prek/tests/list.rs index 4d67f6053..d133364b7 100644 --- a/crates/prek/tests/list.rs +++ b/crates/prek/tests/list.rs @@ -340,6 +340,15 @@ fn list_with_group_filter() { warning: group selector `--group=missing` did not match any hooks "); + cmd_snapshot!(context.filters(), context.list().arg("--require-group").arg("ci").arg("--require-group").arg("slow"), @r" + success: true + exit_code: 0 + ----- stdout ----- + .:slow-lint + + ----- stderr ----- + "); + cmd_snapshot!(context.filters(), context.list().arg("--group").arg("ci slow"), @r" success: false exit_code: 2 diff --git a/crates/prek/tests/run.rs b/crates/prek/tests/run.rs index 10a9b506c..bf208f913 100644 --- a/crates/prek/tests/run.rs +++ b/crates/prek/tests/run.rs @@ -758,7 +758,7 @@ fn run_group_without_stage_selects_hooks_across_stages() { } #[test] -fn run_group_without_stage_warns_when_only_message_file_hooks_match() { +fn run_required_group_without_stage_warns_when_only_message_file_hooks_match() { let context = TestContext::new(); context.init_project(); @@ -784,7 +784,7 @@ fn run_group_without_stage_warns_when_only_message_file_hooks_match() { context.git_add("."); - cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--group").arg("ci"), @r#" + cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--require-group").arg("ci"), @r#" success: false exit_code: 1 ----- stdout ----- @@ -864,7 +864,97 @@ fn run_group_exclusion_wins_over_inclusion() { } #[test] -fn run_unknown_group_warns_and_empty_selection_fails() { +fn run_required_groups_intersect_and_compose_with_other_group_filters() { + let context = TestContext::new(); + context.init_project(); + + context.write_pre_commit_config(indoc::indoc! {r#" + repos: + - repo: local + hooks: + - id: ty + name: Ty + language: system + entry: python3 -c "print('ty')" + always_run: true + groups: [lint, fast, local] + - id: ruff-format + name: Ruff Format + language: system + entry: python3 -c "print('ruff-format')" + always_run: true + groups: [format, fast, local] + - id: mypy + name: Mypy + language: system + entry: python3 -c "print('mypy')" + always_run: true + groups: [lint, slow, ci] + - id: black + name: Black + language: system + entry: python3 -c "print('black')" + always_run: true + groups: [format, slow, ci] + - id: black-fast + name: Black Fast + language: system + entry: python3 -c "print('black-fast')" + always_run: true + groups: [format, slow, ci, fast] + "#}); + + context.git_add("."); + + cmd_snapshot!( + context.filters(), + context + .run() + .arg("--all-files") + .arg("--no-group") + .arg("fast") + .arg("--require-group") + .arg("format") + .arg("--group") + .arg("lint") + .arg("--no-group") + .arg("local") + .arg("--require-group") + .arg("slow") + .arg("--group") + .arg("ci"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + Black....................................................................Passed + + ----- stderr ----- + " + ); + + cmd_snapshot!( + context.filters(), + context + .run() + .arg("--all-files") + .arg("--require-group") + .arg("lint") + .arg("--require-group") + .arg("format"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: No hooks found after filtering with the given selectors + " + ); +} + +#[test] +fn run_unknown_group_selectors_warn_and_empty_selection_fails() { let context = TestContext::new(); context.init_project(); @@ -891,6 +981,16 @@ fn run_unknown_group_warns_and_empty_selection_fails() { warning: group selector `--group=missing` did not match any hooks error: No hooks found after filtering with the given selectors "#); + + cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--require-group").arg("missing"), @r#" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + warning: group selector `--require-group=missing` did not match any hooks + error: No hooks found after filtering with the given selectors + "#); } #[test] @@ -934,7 +1034,7 @@ fn run_group_selectors_reject_whitespace() { } #[test] -fn run_group_and_stage_filters_intersect() { +fn run_required_group_and_stage_filters_intersect() { let context = TestContext::new(); context.init_project(); @@ -967,7 +1067,7 @@ fn run_group_and_stage_filters_intersect() { context.git_add("."); - cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--group").arg("ci").arg("--stage").arg("pre-push"), @r#" + cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("--require-group").arg("ci").arg("--stage").arg("pre-push"), @r#" success: true exit_code: 0 ----- stdout ----- @@ -1836,6 +1936,7 @@ fn global_path_options_expand_tilde() -> Result<()> { }, stage: None, groups: [], + required_groups: [], no_groups: [], } @@ -2366,7 +2467,7 @@ fn skipped_same_key_remote_repo_entry_is_not_initialized() -> Result<()> { } #[test] -fn group_excluded_remote_repo_is_not_cloned() { +fn required_group_excluded_remote_repo_is_not_cloned() { let context = TestContext::new(); context.init_project(); context.write_pre_commit_config(indoc::indoc! {r" @@ -2374,19 +2475,25 @@ fn group_excluded_remote_repo_is_not_cloned() { - repo: builtin hooks: - id: end-of-file-fixer - groups: [ci] + groups: [ci, fast] - repo: https://notexistentatallnevergonnahappen.com/nonexistent/repo rev: v1.0.0 hooks: - id: ruff-check - groups: [local] + groups: [ci] "}); context.git_add("."); cmd_snapshot!( context.filters(), - context.run().arg("--all-files").arg("--group").arg("ci"), + context + .run() + .arg("--all-files") + .arg("--require-group") + .arg("ci") + .arg("--require-group") + .arg("fast"), @r" success: true exit_code: 0 @@ -3278,6 +3385,7 @@ fn selectors_completion() -> Result<()> { --dry-run Do not run the hooks, but print the hooks that would have been run --stage The stage during which the hook is fired --group Run hooks belonging to the specified group + --require-group Run hooks belonging to every specified group --no-group Do not run hooks belonging to the specified group --config Path to alternate config file --cd Change to directory before running diff --git a/docs/proposals/hook-groups.md b/docs/proposals/hook-groups.md index a573f17a3..00be72a0a 100644 --- a/docs/proposals/hook-groups.md +++ b/docs/proposals/hook-groups.md @@ -88,31 +88,37 @@ hooks = [ ## CLI -Add two repeatable options to `prek run`: +Add three repeatable options to `prek run`: ```text prek run --group +prek run --require-group prek run --no-group ``` `--group ` is an include filter. When one or more groups are requested, a hook is selected if its `groups` contains at least one requested group. +`--require-group ` is an intersection filter. A hook is selected only if +its `groups` contains every required group. + `--no-group ` is an exclude filter. A hook is removed if its `groups` contains any excluded group. -If both options are provided, exclusion wins: +The three options compose independently of argument order, and exclusion wins: 1. Select hooks matching `--group`, if any `--group` values were provided. -2. Remove hooks matching `--no-group`, if any `--no-group` values were provided. +2. Keep only hooks matching every `--require-group` value, if any were provided. +3. Remove hooks matching `--no-group`, if any `--no-group` values were provided. Examples: ```bash 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 --no-group slow +prek run --all-files --group ci --require-group lint --no-group slow prek run --all-files --group ci --stage pre-push ``` @@ -124,7 +130,7 @@ selection order should be: 1. Load hooks from selected projects. 2. Apply positional hook or project includes. 3. Apply `--skip` selectors and skip environment variables. -4. Apply group include and exclude filters. +4. Apply group include, intersection, and exclude filters. 5. Apply explicit `--stage` filtering, if provided. 6. If no group filter and no explicit `--stage` were provided, apply the existing default `pre-commit` stage filtering and hook-target `manual` @@ -148,13 +154,14 @@ dependencies, or ecosystems the user intentionally does not want to invoke. Groups are independent from Git hook stages. -When `--group` and `--no-group` are not used, the existing stage behavior is -unchanged: omitting `--stage`/`--hook-stage` first selects hooks eligible for -`pre-commit`. If no hook is selected and the command named hook IDs, those same -IDs are matched again against hooks configured for `manual`. +When `--group`, `--require-group`, and `--no-group` are not used, the existing +stage behavior is unchanged: omitting `--stage`/`--hook-stage` first selects +hooks eligible for `pre-commit`. If no hook is selected and the command named +hook IDs, those same IDs are matched again against hooks configured for +`manual`. -When `--group` or `--no-group` is used without an explicit `--stage`, `prek run` -enters group selection mode: +When any group selector is used without an explicit `--stage`, `prek run` enters +group selection mode: - Hooks from any configured stage can match. - The special second pass that checks named hook IDs against `manual` is not @@ -174,8 +181,8 @@ according to each hook's normal filters and `pass_filenames` setting; only the message-file stages need input that cannot be inferred and are not selected by stage-less group runs. -When `--group` or `--no-group` is combined with explicit stage selection, the -filters compose by intersection: +When a group selector is combined with explicit stage selection, the filters +compose by intersection: ```bash prek run --group ci --stage pre-push @@ -234,6 +241,8 @@ If no group options are passed, ungrouped hooks run as they do today. If `--group ` is passed, ungrouped hooks do not match and are not run. This proposal does not add a virtual `ungrouped` group. +If `--require-group ` is passed, ungrouped hooks also do not match. + If only `--no-group ` is passed, ungrouped hooks remain selected, because they do not belong to the excluded group. @@ -246,9 +255,12 @@ A hook may belong to multiple groups: groups: ["lint", "python", "ci"] ``` -The hook matches any include group and is excluded by any exclude group: +The hook matches any include group, every listed required group, and is excluded +by any exclude group: - `--group lint` selects it. +- `--require-group lint --require-group ci` selects it. +- `--require-group lint --require-group format` does not select it. - `--group ci --no-group python` excludes it. - `--no-group format` does not exclude it. @@ -285,8 +297,8 @@ configuration key/value behavior. ### Unknown CLI Groups -If `--group does-not-exist` matches no hooks, the run should fail with the same -kind of explicit-selection error used for unmatched hook selectors. +If a group selector matches no hooks, the run should fail with the same kind of +explicit-selection error used for unmatched hook selectors. If multiple groups are requested and at least one matches, unmatched group names should produce a warning rather than failing the entire run, consistent with @@ -345,7 +357,7 @@ have made obviously cannot occur, and `prek` should not install or execute it. ### Try Repo -`prek try-repo` should not accept `--group` or `--no-group`. +`prek try-repo` should not accept `--group`, `--require-group`, or `--no-group`. `try-repo` builds a temporary project configuration from the remote hook manifest. That generated configuration contains hook ids, but it does not have @@ -355,10 +367,12 @@ ungrouped. Instead, these flags should be rejected by the CLI. ### List Command -This proposal does not require changes to `prek list`. +`prek list` accepts the same `--group`, `--require-group`, and `--no-group` +filters as `prek run`. This lets users preview the hooks selected by a group +expression without running them. -It would be useful for `prek list --output-format=json` to include `groups` once -the field exists, but filtering `prek list` by group can be a follow-up. +This does not add group metadata to `--output-format=json`; exposing that data +remains a separate change. ## Install Behavior @@ -386,12 +400,14 @@ This proposal does not add: - Default-disabled hooks. - Environment variables for group selection. - `prek install --group`. -- `prek try-repo --group` or `prek try-repo --no-group`. +- `prek try-repo --group`, `prek try-repo --require-group`, or + `prek try-repo --no-group`. - Global group declarations or validation against a root-level list. ## Backward Compatibility -If no `groups`, `--group`, or `--no-group` is used, behavior is unchanged. +If no `groups`, `--group`, `--require-group`, or `--no-group` is used, behavior +is unchanged. Existing `stages` behavior is unchanged for normal `prek run` and installed Git hook execution. The only new behavior is explicit group selection mode and the @@ -408,13 +424,14 @@ At a high level, implementation should: 1. Add `groups` to config hook types and the built `Hook` type. 2. Validate group names during config parsing or hook construction. -3. Add repeatable `--group` and `--no-group` arguments to `prek run`. +3. Add repeatable `--group`, `--require-group`, and `--no-group` arguments to + `prek run`. 4. Apply group filtering before explicit stage filtering and before install selection. 5. Skip default stage filtering when group mode is active and no explicit `--stage` was provided. -6. Keep `--group` and `--no-group` unsupported for `prek try-repo`. +6. Keep all group selectors unsupported for `prek try-repo`. 7. Add schema, documentation, and CLI reference updates. -8. Add integration tests covering include, exclude, include-plus-exclude, - ungrouped hooks, explicit stage intersections, workspace selection, and - install avoidance for excluded hooks. +8. Add integration tests covering include, required intersections, exclude, + mixed group filters, ungrouped hooks, explicit stage intersections, + workspace selection, and install avoidance for excluded hooks. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 36a3ad0c5..e2b3ae597 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -243,6 +243,8 @@ prek run [OPTIONS] [HOOK|PROJECT]...
--quiet, -q

Use quiet output.

Repeating this option, e.g., -qq, will enable a silent mode in which prek will write no output to stdout.

May also be set with the PREK_QUIET environment variable.

--refresh

Refresh all cached data

+
--require-group group

Run hooks belonging to every specified group.

+

Can be specified multiple times. Composes with --group and --no-group.

--show-diff-on-failure

When hooks fail, run git diff directly afterward

--skip hook|project

Skip the specified hooks or projects.

Supports flexible selector syntax:

@@ -259,7 +261,7 @@ prek run [OPTIONS] [HOOK|PROJECT]...

Can be specified multiple times. Also accepts PREK_SKIP or SKIP environment variables (comma-delimited).

--stage, --hook-stage stage

The stage during which the hook is fired.

-

When specified, only hooks configured for that stage (for example manual, pre-commit, or pre-push) will run. When not specified and no group filter is active, this command starts with hooks eligible for pre-commit. If no hook is selected and the command named hook IDs, those same IDs are matched again against hooks configured for manual. With --group or --no-group, omitting the stage lets hooks from any configured stage match, using the default file input mode; hooks that only run at commit-msg or prepare-commit-msg are ignored.

+

When specified, only hooks configured for that stage (for example manual, pre-commit, or pre-push) will run. When not specified and no group filter is active, this command starts with hooks eligible for pre-commit. If no hook is selected and the command named hook IDs, those same IDs are matched again against hooks configured for manual. With --group, --require-group, or --no-group, omitting the stage lets hooks from any configured stage match, using the default file input mode; hooks that only run at commit-msg or prepare-commit-msg are ignored.

Possible values:

  • manual
  • @@ -373,6 +375,8 @@ prek list [OPTIONS] [HOOK|PROJECT]...
--quiet, -q

Use quiet output.

Repeating this option, e.g., -qq, will enable a silent mode in which prek will write no output to stdout.

May also be set with the PREK_QUIET environment variable.

--refresh

Refresh all cached data

+
--require-group group

Show hooks belonging to every specified group.

+

Can be specified multiple times. Composes with --group and --no-group.

--skip hook|project

Skip the specified hooks or projects.

Supports flexible selector syntax:

    diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 54c623822..35ee1299f 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1143,7 +1143,9 @@ Tag a hook with user-defined run groups. - Type: list of strings - Default: `[]` -Groups are arbitrary labels used by [`prek run --group `](cli.md#prek-run--group) and [`prek run --no-group `](cli.md#prek-run--no-group). +Groups are arbitrary labels used by [`prek run --group `](cli.md#prek-run--group), +[`prek run --require-group `](cli.md#prek-run--require-group), and +[`prek run --no-group `](cli.md#prek-run--no-group). Group names cannot be empty or contain whitespace. `groups` is a project configuration field. If it appears in a remote @@ -1199,6 +1201,12 @@ Run only the `ci` group: prek run --all-files --group ci ``` +Run only hooks belonging to both `lint` and `ci`: + +```bash +prek run --all-files --require-group lint --require-group ci +``` + Run everything except formatters: ```bash @@ -1207,13 +1215,16 @@ prek run --all-files --no-group format If a hook matches both `--group` and `--no-group`, `--no-group` wins. -When `--group` or `--no-group` is used without `--stage`, group filtering is -not constrained by hook stage. `prek run` collects normal file input for the -manual command and runs every matching hook that can use that input. Hooks -configured only for `commit-msg` and/or `prepare-commit-msg` require Git's -message file argument, so they are ignored unless run in the corresponding -hook stage. If every matching hook is ignored this way, `prek run` warns and -fails. +Repeated `--group` values use union semantics, while repeated +`--require-group` values use intersection semantics. When combined, a hook must +match at least one `--group`, every `--require-group`, and no `--no-group`. + +When any group selector is used without `--stage`, group filtering is not +constrained by hook stage. `prek run` collects normal file input for the manual +command and runs every matching hook that can use that input. Hooks configured +only for `commit-msg` and/or `prepare-commit-msg` require Git's message file +argument, so they are ignored unless run in the corresponding hook stage. If +every matching hook is ignored this way, `prek run` warns and fails. ### `require_serial`