Skip to content

Commit 5ac0d1f

Browse files
committed
feat: Prevent spam from GitHub mentions in merge commits
1 parent 811e09a commit 5ac0d1f

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ chrono = "0.4"
5454

5555
itertools = "0.14"
5656

57+
# Text processing
58+
regex = "1"
59+
5760
[dev-dependencies]
5861
insta = "1.26"
5962
wiremock = "0.6"

src/bors/handlers/trybuild.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::github::{
2020
CommitSha, GithubUser, LabelTrigger, MergeError, PullRequest, PullRequestNumber,
2121
};
2222
use crate::permissions::PermissionType;
23+
use crate::utils::suppress_github_mentions;
2324

2425
use super::deny_request;
2526
use super::has_permission;
@@ -286,7 +287,7 @@ fn auto_merge_commit_message(
286287
{pr_message}"#,
287288
pr_label = pr.head_label,
288289
pr_title = pr.title,
289-
pr_message = pr.message,
290+
pr_message = suppress_github_mentions(&pr.message),
290291
repo_owner = name.owner(),
291292
repo_name = name.name()
292293
);

src/utils/mod.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,100 @@
11
pub mod logging;
22
pub mod timing;
3+
4+
/// Converts GitHub @mentions to markdown-backticked text to prevent auto-linking.
5+
/// For example, "@user" is transformed to "`user`" (with backticks) to suppress notifications.
6+
/// Processes only valid GitHub mentions, excluding email addresses and other @ symbols.
7+
pub fn suppress_github_mentions(text: &str) -> String {
8+
if text.is_empty() || !text.contains('@') {
9+
return text.to_string();
10+
}
11+
12+
let pattern = r"@[A-Za-z0-9][A-Za-z0-9\-]{0,38}(/[A-Za-z0-9][A-Za-z0-9\-]{0,38}(/[A-Za-z0-9][A-Za-z0-9\-]{0,38})?)?";
13+
14+
let re = regex::Regex::new(pattern).unwrap();
15+
re.replace_all(text, |caps: &regex::Captures| {
16+
let mention = &caps[0];
17+
let position = caps.get(0).unwrap().start();
18+
19+
if !is_github_mention(text, mention, position) {
20+
return mention.to_string();
21+
}
22+
23+
let name = &mention[1..]; // Drop the @ symbol
24+
format!("`{}`", name)
25+
})
26+
.to_string()
27+
}
28+
29+
// Determines if a potential mention would actually trigger a notification
30+
fn is_github_mention(text: &str, mention: &str, pos: usize) -> bool {
31+
// Not a valid mention if preceded by alphanumeric or underscore (email)
32+
if pos > 0 {
33+
let c = text.chars().nth(pos - 1).unwrap();
34+
if c.is_alphanumeric() || c == '_' {
35+
return false;
36+
}
37+
}
38+
39+
// Check if followed by invalid character
40+
let end = pos + mention.len();
41+
if end < text.len() {
42+
let next_char = text.chars().nth(end).unwrap();
43+
if next_char.is_alphanumeric() || next_char == '_' || next_char == '-' {
44+
return false;
45+
}
46+
}
47+
48+
true
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
55+
#[test]
56+
fn test_suppress_github_mentions() {
57+
// User mentions
58+
assert_eq!(suppress_github_mentions("Hello @user"), "Hello `user`");
59+
assert_eq!(
60+
suppress_github_mentions("CC @user1 @user2"),
61+
"CC `user1` `user2`"
62+
);
63+
64+
// Org team mentions
65+
assert_eq!(suppress_github_mentions("@org/user"), "`org/user`");
66+
assert_eq!(
67+
suppress_github_mentions("Thanks @rust-lang/lib-team!"),
68+
"Thanks `rust-lang/lib-team`!"
69+
);
70+
71+
// Org nested team mentions
72+
assert_eq!(
73+
suppress_github_mentions("@org/team/subteam"),
74+
"`org/team/subteam`"
75+
);
76+
assert_eq!(
77+
suppress_github_mentions("CC @github/docs/content"),
78+
"CC `github/docs/content`"
79+
);
80+
81+
// Non mentions
82+
assert_eq!(suppress_github_mentions(""), "");
83+
assert_eq!(suppress_github_mentions("@"), "@");
84+
assert_eq!(
85+
suppress_github_mentions("[email protected]"),
86+
87+
);
88+
assert_eq!(suppress_github_mentions("@user_test"), "@user_test");
89+
assert_eq!(
90+
suppress_github_mentions("No mentions here"),
91+
"No mentions here"
92+
);
93+
94+
// Combination of user mentions, org team mentions and nested team mentions
95+
assert_eq!(
96+
suppress_github_mentions("Thanks @user, @rust-lang/libs and @github/docs/content!"),
97+
"Thanks `user`, `rust-lang/libs` and `github/docs/content`!"
98+
);
99+
}
100+
}

0 commit comments

Comments
 (0)