Skip to content

Commit ebf5466

Browse files
committed
feat: Prevent spam from GitHub mentions in merge commits
1 parent 680a300 commit ebf5466

File tree

3 files changed

+102
-1
lines changed

3 files changed

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

0 commit comments

Comments
 (0)