From 287e997443b75af4a0bdd6541a4f775e41bd3688 Mon Sep 17 00:00:00 2001 From: Jo <10510431+j178@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:59:38 +0800 Subject: [PATCH] Fix mixed workspace selectors --- crates/prek/src/cli/run/selector.rs | 32 ++++++++---- crates/prek/tests/workspace.rs | 80 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/crates/prek/src/cli/run/selector.rs b/crates/prek/src/cli/run/selector.rs index 6831f3ea0..e254a910d 100644 --- a/crates/prek/src/cli/run/selector.rs +++ b/crates/prek/src/cli/run/selector.rs @@ -364,22 +364,36 @@ impl Selectors { return false; } - // If no project prefix selectors are present, all paths are included - if !self - .includes - .iter() - .any(|include| matches!(include.expr, SelectorExpr::ProjectPrefix(_))) - { + if self.includes.is_empty() { return true; } let mut included = false; + // This is a project-discovery prefilter, not final hook selection. + // Keep scanning after a match so project-prefix selectors still get + // usage recorded for accurate unused-selector warnings. for (idx, include) in self.includes.iter().enumerate() { - if let SelectorExpr::ProjectPrefix(project_path) = &include.expr { - if path.starts_with(project_path) { - usage.use_include(idx); + match &include.expr { + SelectorExpr::HookId(_) => { + // A bare hook id can match any project, but we cannot mark + // it used until final hook filtering proves a hook exists. included = true; } + SelectorExpr::ProjectPrefix(project_path) => { + // Project-prefix selectors select projects directly, so a + // matching discovered project is enough to mark them used. + if path.starts_with(project_path) { + usage.use_include(idx); + included = true; + } + } + SelectorExpr::ProjectHook { project_path, .. } => { + // `project:hook` only needs this exact project initialized; + // usage is still decided by the final hook-id match. + if path == project_path { + included = true; + } + } } } included diff --git a/crates/prek/tests/workspace.rs b/crates/prek/tests/workspace.rs index 60bca5416..04b12a34e 100644 --- a/crates/prek/tests/workspace.rs +++ b/crates/prek/tests/workspace.rs @@ -718,6 +718,86 @@ fn run_with_selectors() -> Result<()> { Ok(()) } +#[test] +fn run_with_mixed_project_and_hook_selectors() -> Result<()> { + let context = TestContext::new(); + context.init_project(); + + context.write_pre_commit_config(indoc! {r" + repos: + - repo: local + hooks: + - id: root-hook + name: root hook + entry: echo root + language: system + pass_filenames: false + "}); + + let sub = context.work_dir().child("sub"); + sub.create_dir_all()?; + sub.child(".pre-commit-config.yaml").write_str(indoc! {r" + repos: + - repo: local + hooks: + - id: sub-hook + name: sub hook + entry: echo sub + language: system + pass_filenames: false + "})?; + sub.child("file.txt").write_str("")?; + + let empty = context.work_dir().child("empty"); + empty.create_dir_all()?; + empty + .child(".pre-commit-config.yaml") + .write_str("repos: []\n")?; + + let unselected = context.work_dir().child("unselected"); + unselected.create_dir_all()?; + unselected + .child(".pre-commit-config.yaml") + .write_str("invalid: config\n")?; + + context.git_add("."); + + cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("sub/").arg(".:root-hook"), @r" + success: true + exit_code: 0 + ----- stdout ----- + ✓ sub + sub hook...............................................................Passed + ✓ + root hook..............................................................Passed + + ----- stderr ----- + "); + + cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("sub/").arg("root-hook"), @r" + success: true + exit_code: 0 + ----- stdout ----- + ✓ sub + sub hook...............................................................Passed + ✓ + root hook..............................................................Passed + + ----- stderr ----- + "); + + cmd_snapshot!(context.filters(), context.run().arg("--all-files").arg("empty/").arg("root-hook"), @r" + success: true + exit_code: 0 + ----- stdout ----- + root hook................................................................Passed + + ----- stderr ----- + "); + + Ok(()) +} + #[test] fn skips() -> Result<()> { let context = TestContext::new();