Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/prek/src/cli/hook_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub(crate) async fn hook_impl(
skips,
vec![],
vec![],
vec![],
Some(hook_type.into()),
file_selection,
false,
Expand Down
3 changes: 2 additions & 1 deletion crates/prek/src/cli/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub(crate) async fn list(
includes: Vec<String>,
skips: Vec<String>,
groups: Vec<String>,
required_groups: Vec<String>,
no_groups: Vec<String>,
hook_stage: Option<Stage>,
language: Option<Language>,
Expand All @@ -41,7 +42,7 @@ pub(crate) async fn list(
) -> anyhow::Result<ExitStatus> {
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);
Expand Down
19 changes: 16 additions & 3 deletions crates/prek/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Stage>,

Expand All @@ -658,6 +659,12 @@ pub(crate) struct RunArgs {
#[arg(long = "group", value_name = "GROUP")]
pub(crate) groups: Vec<String>,

/// 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<String>,

/// Do not run hooks belonging to the specified group.
///
/// Can be specified multiple times. Exclusion wins over inclusion.
Expand Down Expand Up @@ -751,6 +758,12 @@ pub(crate) struct ListArgs {
#[arg(long = "group", value_name = "GROUP")]
pub(crate) groups: Vec<String>,

/// 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<String>,

/// Do not show hooks belonging to the specified group.
///
/// Can be specified multiple times. Exclusion wins over inclusion.
Expand Down
3 changes: 2 additions & 1 deletion crates/prek/src/cli/run/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub(crate) async fn run(
includes: Vec<String>,
skips: Vec<String>,
groups: Vec<String>,
required_groups: Vec<String>,
no_groups: Vec<String>,
hook_stage: Option<Stage>,
selection: FileSelection,
Expand Down Expand Up @@ -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)?;

Expand Down
116 changes: 64 additions & 52 deletions crates/prek/src/cli/run/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,18 @@ impl Selectors {

#[derive(Debug, Clone, Default)]
pub(crate) struct GroupFilters {
includes: Vec<String>,
excludes: Vec<String>,
include_any: Vec<String>,
require_all: Vec<String>,
exclude_any: Vec<String>,
usage: Arc<Mutex<FilterUsage>>,
}

impl GroupFilters {
pub(crate) fn parse(includes: &[String], excludes: &[String]) -> Result<Self, Error> {
pub(crate) fn parse(
include_any: &[String],
require_all: &[String],
exclude_any: &[String],
) -> Result<Self, Error> {
let parse_groups = |flag: &'static str, groups: &[String]| {
let mut seen = FxHashSet::default();
let mut names = Vec::new();
Expand All @@ -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::<Vec<_>>();
Expand All @@ -533,17 +530,22 @@ impl GroupFilters {

#[derive(Default, Debug)]
struct FilterUsage {
used_includes: FxHashSet<usize>,
used_excludes: FxHashSet<usize>,
includes: FxHashSet<usize>,
requirements: FxHashSet<usize>,
excludes: FxHashSet<usize>,
}

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>(
Expand All @@ -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>(
Expand All @@ -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<Item = (usize, &'a T)> + 'a {
values
.iter()
.enumerate()
.filter(|(idx, _)| !self.requirements.contains(idx))
}

fn report_unused(&self, selectors: &Selectors) {
Expand Down
1 change: 1 addition & 0 deletions crates/prek/src/cli/try_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ pub(crate) async fn try_repo(
vec![],
vec![],
vec![],
vec![],
stage,
file_selection,
run_args.show_diff_on_failure,
Expand Down
2 changes: 2 additions & 0 deletions crates/prek/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
options.includes,
options.skips,
args.groups,
args.required_groups,
args.no_groups,
args.stage,
file_selection,
Expand All @@ -306,6 +307,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.includes,
args.skips,
args.groups,
args.required_groups,
args.no_groups,
args.hook_stage,
args.language,
Expand Down
9 changes: 9 additions & 0 deletions crates/prek/tests/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading