diff --git a/crates/prek/src/cli/run/filter.rs b/crates/prek/src/cli/run/filter.rs index bdf516f60..f2518b8d3 100644 --- a/crates/prek/src/cli/run/filter.rs +++ b/crates/prek/src/cli/run/filter.rs @@ -764,15 +764,23 @@ mod tests { #[cfg(unix)] #[test] - fn filename_filter_skips_non_utf8_paths_with_regex_include() { + fn filename_filter_regex_matches_valid_suffix_of_non_utf8_path() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt as _; - let include = regex_pattern(r".*\.py$"); + let include = regex_pattern(r"\.py$"); let path = Path::new(OsStr::from_bytes(b"bad-\xff.py")); let filter = FilenameFilter::new(Some(&include), None); - assert!(!filter.matches(path)); + assert!(filter.matches(path)); + } + + #[test] + fn filename_filter_regex_keeps_unicode_character_classes() { + let include = regex_pattern(r"^\w+\.py$"); + let filter = FilenameFilter::new(Some(&include), None); + + assert!(filter.matches(Path::new("café.py"))); } #[test] diff --git a/crates/prek/src/config.rs b/crates/prek/src/config.rs index c1df7a3fa..4e55af571 100644 --- a/crates/prek/src/config.rs +++ b/crates/prek/src/config.rs @@ -4,7 +4,7 @@ use std::fmt::Display; use std::path::Path; use anyhow::Result; -use fancy_regex::Regex; +use fancy_regex::{BytesMode, Regex, RegexBuilder}; use globset::{Glob, GlobSet}; use itertools::Itertools; use owo_colors::OwoColorize; @@ -327,16 +327,18 @@ impl FilePattern { } pub(crate) fn regex(pattern: &str) -> Result { - Ok(Self::Regex(Regex::new(pattern)?)) + let regex = RegexBuilder::new(pattern) + .bytes_mode(BytesMode::UnicodeBytes) + .build()?; + Ok(Self::Regex(regex)) } pub(crate) fn is_match(&self, path: &Path) -> bool { match self { FilePattern::Never => false, - // Regex patterns are text matchers; globs can match OS paths directly. - FilePattern::Regex(regex) => path - .to_str() - .is_some_and(|path| regex.is_match(path).unwrap_or(false)), + FilePattern::Regex(regex) => regex + .is_match(path.as_os_str().as_encoded_bytes()) + .unwrap_or(false), FilePattern::Glob(globs) => globs.is_match(path), } } @@ -360,9 +362,9 @@ impl TryFrom for FilePattern { fn try_from(value: FilePatternWire) -> Result { match value { - FilePatternWire::Glob { glob } => Ok(Self::Glob(GlobPatterns::new(vec![glob])?)), - FilePatternWire::GlobList { glob } => Ok(Self::Glob(GlobPatterns::new(glob)?)), - FilePatternWire::Regex(pattern) => Ok(Self::Regex(Regex::new(&pattern)?)), + FilePatternWire::Glob { glob } => Ok(Self::glob(vec![glob])?), + FilePatternWire::GlobList { glob } => Ok(Self::glob(glob)?), + FilePatternWire::Regex(pattern) => Ok(Self::regex(&pattern)?), } } } diff --git a/crates/prek/src/hooks/pre_commit_hooks/no_commit_to_branch.rs b/crates/prek/src/hooks/pre_commit_hooks/no_commit_to_branch.rs index 6f4cb4f97..07e669b77 100644 --- a/crates/prek/src/hooks/pre_commit_hooks/no_commit_to_branch.rs +++ b/crates/prek/src/hooks/pre_commit_hooks/no_commit_to_branch.rs @@ -31,8 +31,11 @@ impl Args { } for pattern in &self.patterns { - let pattern = Regex::new(pattern).context("Failed to compile regex patterns")?; - if pattern.is_match(branch).unwrap_or(false) { + let regex = Regex::new(pattern) + .with_context(|| format!("Failed to compile regex pattern `{pattern}`"))?; + if regex.is_match(branch).with_context(|| { + format!("Failed to match branch against regex pattern `{pattern}`") + })? { return Ok(true); } } @@ -67,3 +70,23 @@ pub(crate) async fn run(hook: &Hook) -> Result { Ok(HookOutput::unchanged(0, Vec::new())) } } + +#[cfg(test)] +mod tests { + use super::Args; + + #[test] + fn pattern_runtime_errors_include_pattern() { + let args = Args { + branches: Vec::new(), + patterns: vec![r"^(a|aa)+\1$".to_string()], + }; + let branch = format!("{}b", "a".repeat(40)); + + let err = args.check_protected(&branch).unwrap_err(); + assert_eq!( + err.to_string(), + r"Failed to match branch against regex pattern `^(a|aa)+\1$`" + ); + } +} diff --git a/crates/prek/tests/builtin_hooks.rs b/crates/prek/tests/builtin_hooks.rs index 9a1bca70d..3e4b55005 100644 --- a/crates/prek/tests/builtin_hooks.rs +++ b/crates/prek/tests/builtin_hooks.rs @@ -2928,7 +2928,7 @@ fn no_commit_to_branch_hook_with_patterns() -> Result<()> { ----- stderr ----- error: Failed to run hook `no-commit-to-branch` - caused by: Failed to compile regex patterns + caused by: Failed to compile regex pattern `*invalid-pattern*` caused by: Parsing error at position 0: Target of repeat operator is invalid ");