Skip to content

Commit 346ecd6

Browse files
authored
Merge pull request #2171 from Urgau/mentions-content-changes
Add way to do mentions of content changes
2 parents 7ab36ca + 291d78e commit 346ecd6

File tree

2 files changed

+167
-22
lines changed

2 files changed

+167
-22
lines changed

src/config.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,23 @@ pub(crate) struct ConcernConfig {
225225
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
226226
pub(crate) struct MentionsConfig {
227227
#[serde(flatten)]
228-
pub(crate) paths: HashMap<String, MentionsPathConfig>,
228+
pub(crate) entries: HashMap<String, MentionsEntryConfig>,
229+
}
230+
231+
#[derive(PartialEq, Eq, Debug, Default, serde::Deserialize)]
232+
#[serde(rename_all = "kebab-case")]
233+
pub(crate) enum MentionsEntryType {
234+
#[default]
235+
Filename,
236+
Content,
229237
}
230238

231239
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
232240
#[serde(deny_unknown_fields)]
233-
pub(crate) struct MentionsPathConfig {
241+
pub(crate) struct MentionsEntryConfig {
242+
#[serde(alias = "type")]
243+
#[serde(default)]
244+
pub(crate) type_: MentionsEntryType,
234245
pub(crate) message: Option<String>,
235246
#[serde(default)]
236247
pub(crate) cc: Vec<String>,
@@ -661,7 +672,7 @@ mod tests {
661672

662673
#[test]
663674
fn sample() {
664-
let config = r#"
675+
let config = r##"
665676
[relabel]
666677
allow-unauthenticated = [
667678
"C-*"
@@ -692,6 +703,18 @@ mod tests {
692703
core = "T-core"
693704
infra = "T-infra"
694705
706+
[mentions."src/"]
707+
cc = ["@someone"]
708+
709+
[mentions."target/"]
710+
message = "This is a message."
711+
cc = ["@someone"]
712+
713+
[mentions."#[rustc_attr]"]
714+
type = "content"
715+
message = "This is a message."
716+
cc = ["@someone"]
717+
695718
[shortcut]
696719
697720
[issue-links]
@@ -713,7 +736,7 @@ mod tests {
713736
[range-diff]
714737
715738
[review-changes-since]
716-
"#;
739+
"##;
717740
let config = toml::from_str::<Config>(&config).unwrap();
718741
let mut ping_teams = HashMap::new();
719742
ping_teams.insert(
@@ -780,7 +803,34 @@ mod tests {
780803
github_releases: None,
781804
review_submitted: None,
782805
review_requested: None,
783-
mentions: None,
806+
mentions: Some(MentionsConfig {
807+
entries: HashMap::from([
808+
(
809+
"src/".to_string(),
810+
MentionsEntryConfig {
811+
type_: MentionsEntryType::Filename,
812+
message: None,
813+
cc: vec!["@someone".to_string()]
814+
}
815+
),
816+
(
817+
"target/".to_string(),
818+
MentionsEntryConfig {
819+
type_: MentionsEntryType::Filename,
820+
message: Some("This is a message.".to_string()),
821+
cc: vec!["@someone".to_string()]
822+
}
823+
),
824+
(
825+
"#[rustc_attr]".to_string(),
826+
MentionsEntryConfig {
827+
type_: MentionsEntryType::Content,
828+
message: Some("This is a message.".to_string()),
829+
cc: vec!["@someone".to_string()]
830+
}
831+
)
832+
])
833+
}),
784834
no_merges: None,
785835
pr_tracking: None,
786836
transfer: None,

src/handlers/mentions.rs

Lines changed: 112 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,28 @@
33
//! interested people.
44
55
use crate::{
6-
config::{MentionsConfig, MentionsPathConfig},
6+
config::{MentionsConfig, MentionsEntryConfig, MentionsEntryType},
77
db::issue_data::IssueData,
88
github::{IssuesAction, IssuesEvent},
99
handlers::Context,
1010
};
1111
use anyhow::Context as _;
12+
use itertools::Itertools;
1213
use serde::{Deserialize, Serialize};
13-
use std::fmt::Write;
1414
use std::path::Path;
15+
use std::{fmt::Write, path::PathBuf};
1516
use tracing as log;
1617

1718
const MENTIONS_KEY: &str = "mentions";
1819

1920
pub(super) struct MentionsInput {
20-
paths: Vec<String>,
21+
to_mention: Vec<(String, Vec<PathBuf>)>,
2122
}
2223

2324
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
2425
struct MentionState {
25-
paths: Vec<String>,
26+
#[serde(alias = "paths")]
27+
entries: Vec<String>,
2628
}
2729

2830
pub(super) async fn parse_input(
@@ -61,23 +63,42 @@ pub(super) async fn parse_input(
6163
{
6264
let file_paths: Vec<_> = files.iter().map(|fd| Path::new(&fd.filename)).collect();
6365
let to_mention: Vec<_> = config
64-
.paths
66+
.entries
6567
.iter()
66-
.filter(|(path, MentionsPathConfig { cc, .. })| {
67-
let path = Path::new(path);
68-
// Only mention matching paths.
69-
let touches_relevant_files = file_paths.iter().any(|p| p.starts_with(path));
68+
.filter_map(|(entry, MentionsEntryConfig { cc, type_, .. })| {
69+
let relevant_file_paths: Vec<PathBuf> = match type_ {
70+
MentionsEntryType::Filename => {
71+
let path = Path::new(entry);
72+
// Only mention matching paths.
73+
file_paths
74+
.iter()
75+
.filter(|p| p.starts_with(path))
76+
.map(|p| PathBuf::from(p))
77+
.collect()
78+
}
79+
MentionsEntryType::Content => {
80+
// Only mentions byte-for-byte matching content inside the patch.
81+
files
82+
.iter()
83+
.filter(|f| patch_contains(&f.patch, &**entry))
84+
.map(|f| PathBuf::from(&f.filename))
85+
.collect()
86+
}
87+
};
7088
// Don't mention if only the author is in the list.
7189
let pings_non_author = match &cc[..] {
7290
[only_cc] => only_cc.trim_start_matches('@') != &event.issue.user.login,
7391
_ => true,
7492
};
75-
touches_relevant_files && pings_non_author
93+
if !relevant_file_paths.is_empty() && pings_non_author {
94+
Some((entry.to_string(), relevant_file_paths))
95+
} else {
96+
None
97+
}
7698
})
77-
.map(|(key, _mention)| key.to_string())
7899
.collect();
79100
if !to_mention.is_empty() {
80-
return Ok(Some(MentionsInput { paths: to_mention }));
101+
return Ok(Some(MentionsInput { to_mention }));
81102
}
82103
}
83104
Ok(None)
@@ -94,23 +115,36 @@ pub(super) async fn handle_input(
94115
IssueData::load(&mut client, &event.issue, MENTIONS_KEY).await?;
95116
// Build the message to post to the issue.
96117
let mut result = String::new();
97-
for to_mention in &input.paths {
98-
if state.data.paths.iter().any(|p| p == to_mention) {
118+
for (entry, relevant_file_paths) in input.to_mention {
119+
if state.data.entries.iter().any(|e| e == &entry) {
99120
// Avoid duplicate mentions.
100121
continue;
101122
}
102-
let MentionsPathConfig { message, cc } = &config.paths[to_mention];
123+
let MentionsEntryConfig { message, cc, type_ } = &config.entries[&entry];
103124
if !result.is_empty() {
104125
result.push_str("\n\n");
105126
}
106127
match message {
107128
Some(m) => result.push_str(m),
108-
None => write!(result, "Some changes occurred in {to_mention}").unwrap(),
129+
None => match type_ {
130+
MentionsEntryType::Filename => {
131+
write!(result, "Some changes occurred in {entry}").unwrap()
132+
}
133+
MentionsEntryType::Content => write!(
134+
result,
135+
"Some changes regarding `{entry}` occurred in {}",
136+
relevant_file_paths
137+
.iter()
138+
.map(|f| f.to_string_lossy())
139+
.join(", ")
140+
)
141+
.unwrap(),
142+
},
109143
}
110144
if !cc.is_empty() {
111145
write!(result, "\n\ncc {}", cc.join(", ")).unwrap();
112146
}
113-
state.data.paths.push(to_mention.to_string());
147+
state.data.entries.push(entry);
114148
}
115149
if !result.is_empty() {
116150
event
@@ -122,3 +156,64 @@ pub(super) async fn handle_input(
122156
}
123157
Ok(())
124158
}
159+
160+
fn patch_contains(patch: &str, needle: &str) -> bool {
161+
for line in patch.lines() {
162+
if (!line.starts_with("+++") && line.starts_with('+'))
163+
|| (!line.starts_with("---") && line.starts_with('-'))
164+
{
165+
if line.contains(needle) {
166+
return true;
167+
}
168+
}
169+
}
170+
171+
false
172+
}
173+
174+
#[cfg(test)]
175+
mod tests {
176+
use super::*;
177+
178+
#[test]
179+
fn finds_added_line() {
180+
let patch = "\
181+
--- a/file.txt
182+
+++ b/file.txt
183+
+hello world
184+
context line
185+
";
186+
assert!(patch_contains(patch, "hello"));
187+
}
188+
189+
#[test]
190+
fn finds_removed_line() {
191+
let patch = "\
192+
--- a/file.txt
193+
+++ b/file.txt
194+
-old value
195+
+new value
196+
";
197+
assert!(patch_contains(patch, "old value"));
198+
}
199+
200+
#[test]
201+
fn ignores_diff_headers() {
202+
let patch = "\
203+
--- a/file.txt
204+
+++ b/file.txt
205+
context line
206+
";
207+
assert!(!patch_contains(patch, "file.txt")); // should *not* match header
208+
}
209+
210+
#[test]
211+
fn needle_not_present() {
212+
let patch = "\
213+
--- a/file.txt
214+
+++ b/file.txt
215+
+added line
216+
";
217+
assert!(!patch_contains(patch, "missing"));
218+
}
219+
}

0 commit comments

Comments
 (0)