diff --git a/src/config.rs b/src/config.rs index 41e0cb748..d99b84250 100644 --- a/src/config.rs +++ b/src/config.rs @@ -299,6 +299,9 @@ pub(crate) struct MentionsEntryConfig { #[serde(alias = "type")] #[serde(default)] pub(crate) type_: MentionsEntryType, + /// Additional path globs to filter; primarily useful for `type = "content"` + #[serde(default)] + pub(crate) trigger_files: Vec, pub(crate) message: Option, #[serde(default)] pub(crate) cc: Vec, @@ -951,6 +954,12 @@ mod tests { message = "This is a message." cc = ["@someone"] + [mentions."miri"] + type = "content" + trigger_files = ["library/1", "library/2"] + message = "This is a message." + cc = ["@someone"] + [shortcut] [issue-links] @@ -1048,6 +1057,7 @@ mod tests { "src/".to_string(), MentionsEntryConfig { type_: MentionsEntryType::Filename, + trigger_files: vec![], message: None, cc: vec!["@someone".to_string()] } @@ -1056,6 +1066,7 @@ mod tests { "target/".to_string(), MentionsEntryConfig { type_: MentionsEntryType::Filename, + trigger_files: vec![], message: Some("This is a message.".to_string()), cc: vec!["@someone".to_string()] } @@ -1064,10 +1075,23 @@ mod tests { "#[rustc_attr]".to_string(), MentionsEntryConfig { type_: MentionsEntryType::Content, + trigger_files: vec![], + message: Some("This is a message.".to_string()), + cc: vec!["@someone".to_string()] + } + ), + ( + "miri".to_string(), + MentionsEntryConfig { + type_: MentionsEntryType::Content, + trigger_files: vec![ + "library/1".to_string(), + "library/2".to_string() + ], message: Some("This is a message.".to_string()), cc: vec!["@someone".to_string()] } - ) + ), ]) }), no_merges: None, diff --git a/src/handlers/check_commits/validate_config.rs b/src/handlers/check_commits/validate_config.rs index 41de673d3..0c235ff4f 100644 --- a/src/handlers/check_commits/validate_config.rs +++ b/src/handlers/check_commits/validate_config.rs @@ -96,9 +96,22 @@ fn validate_parsed_config(config: &Config) -> Result<(), String> { }; if let Some(mentions) = &config.mentions { - for (entry, MentionsEntryConfig { type_, .. }) in &mentions.entries { + for (entry, cfg @ MentionsEntryConfig { type_, .. }) in &mentions.entries { if type_ == &MentionsEntryType::Filename { validate_pattern(entry, &format!("`[mentions.\"{entry}\"]`"))?; + if !cfg.trigger_files.is_empty() { + return Err(format!( + "Invalid `triagebot.toml`:\n\ + `[mentions.\"{entry}\"]` has `trigger_files` \ + which is only valid for `type = \"content\"`." + )); + } + } + for file in &cfg.trigger_files { + validate_pattern( + file, + &format!("`[mentions.\"{entry}\"] trigger_files \"{file}\"`"), + )?; } } } diff --git a/src/handlers/mentions.rs b/src/handlers/mentions.rs index 2dc7b6154..7a6ef13d2 100644 --- a/src/handlers/mentions.rs +++ b/src/handlers/mentions.rs @@ -85,7 +85,7 @@ pub(super) async fn parse_input( let to_mention: Vec<_> = config .entries .iter() - .filter_map(|(entry, MentionsEntryConfig { cc, type_, .. })| { + .filter_map(|(entry, cfg @ MentionsEntryConfig { cc, type_, .. })| { let relevant_file_paths: Vec = match type_ { MentionsEntryType::Filename => { // Only mention matching paths. @@ -98,10 +98,17 @@ pub(super) async fn parse_input( .collect() } MentionsEntryType::Content => { + let file_matcher = ModifiedPathMatcher::new(&cfg.trigger_files); + let file_match = |f| file_matcher.is_match(Path::new(f)); // Only mentions byte-for-byte matching content inside the patch. modified_files .iter() - .filter(|f| patch_adds(&f.patch, entry)) + .filter(|f| { + patch_adds(&f.patch, entry) + && (cfg.trigger_files.is_empty() + || file_match(&f.filename) + || f.previous_filename.as_ref().is_some_and(|p| file_match(p))) + }) .map(|f| PathBuf::from(&f.filename)) .collect() }