Repository: rust-lang/triagebot
Pull request #1917: Add no GitHub mentions in commits handler
Author: login: Urgau
Created: 2025-03-07T22:35:24Z | Merged: 2025-04-09T16:51:37Z

This PR implements a no-mentions in commits handler. This is so that people are not spammed by GitHub when someone pushes a commit with their GitHub handle in it.

This is coming from https://github.com/rust-lang/rust/issues/137990. I should note that I also have been annoyed by that in the past.

The implementation is heavily inspired by the `[no-merges]` one.

The warning is gated behind the `[no-mentions]` config and produces such warning:

> There are mentions (`@mention`) in your commits. We have a no mention policy so these commits will need to be removed for this pull request to be merged.
>
> The following commits have mentions is them:
>  - commit1
>  - commit2

r? @ehuss


Diff:
diff --git a/src/config.rs b/src/config.rs
index 7fbb16703..ddf431fe6 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -47,6 +47,7 @@ pub(crate) struct Config {
     pub(crate) bot_pull_requests: Option<BotPullRequests>,
     pub(crate) rendered_link: Option<RenderedLinkConfig>,
     pub(crate) canonicalize_issue_links: Option<CanonicalizeIssueLinksConfig>,
+    pub(crate) no_mentions: Option<NoMentionsConfig>,
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -420,6 +421,11 @@ pub(crate) struct RenderedLinkConfig {
 #[serde(deny_unknown_fields)]
 pub(crate) struct CanonicalizeIssueLinksConfig {}
 
+#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "kebab-case")]
+#[serde(deny_unknown_fields)]
+pub(crate) struct NoMentionsConfig {}
+
 fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
     let cache = CONFIG_CACHE.read().unwrap();
     cache.get(repo).and_then(|(config, fetch_time)| {
@@ -545,6 +551,8 @@ mod tests {
 
             [rendered-link]
             trigger-files = ["posts/"]
+
+            [no-mentions]
         "#;
         let config = toml::from_str::<Config>(&config).unwrap();
         let mut ping_teams = HashMap::new();
@@ -608,6 +616,7 @@ mod tests {
                     trigger_files: vec!["posts/".to_string()]
                 }),
                 canonicalize_issue_links: Some(CanonicalizeIssueLinksConfig {}),
+                no_mentions: Some(NoMentionsConfig {}),
             }
         );
     }
@@ -671,7 +680,8 @@ mod tests {
                 merge_conflicts: None,
                 bot_pull_requests: None,
                 rendered_link: None,
-                canonicalize_issue_links: None
+                canonicalize_issue_links: None,
+                no_mentions: None,
             }
         );
     }
diff --git a/src/handlers/check_commits.rs b/src/handlers/check_commits.rs
index 719214626..a13e150ce 100644
--- a/src/handlers/check_commits.rs
+++ b/src/handlers/check_commits.rs
@@ -8,6 +8,7 @@ use crate::{
 };
 
 mod modified_submodule;
+mod no_mentions;
 mod non_default_branch;
 
 /// Key for the state in the database
@@ -41,6 +42,7 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
             event.issue.number
         )
     };
+    let commits = event.issue.commits(&ctx.github).await?;
 
     let mut warnings = Vec::new();
 
@@ -58,6 +60,10 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
         warnings.extend(modified_submodule::modifies_submodule(diff));
     }
 
+    if let Some(no_mentions) = &config.no_mentions {
+        warnings.extend(no_mentions::mentions_in_commits(no_mentions, &commits));
+    }
+
     handle_warnings(ctx, event, warnings).await
 }
 
@@ -88,12 +94,7 @@ async fn handle_warnings(
                 .await?;
         }
 
-        // Format the warnings for user consumption on Github
-        let warnings: Vec<_> = warnings
-            .iter()
-            .map(|warning| format!("* {warning}"))
-            .collect();
-        let warning = format!(":warning: **Warning** :warning:\n\n{}", warnings.join("\n"));
+        let warning = warning_from_warnings(&warnings);
         let comment = event.issue.post_comment(&ctx.github, &warning).await?;
 
         // Save new state in the database
@@ -120,3 +121,12 @@ async fn handle_warnings(
 
     Ok(())
 }
+
+// Format the warnings for user consumption on Github
+fn warning_from_warnings(warnings: &[String]) -> String {
+    let warnings: Vec<_> = warnings
+        .iter()
+        .map(|warning| format!("* {warning}"))
+        .collect();
+    format!(":warning: **Warning** :warning:\n\n{}", warnings.join("\n"))
+}
diff --git a/src/handlers/check_commits/modified_submodule.rs b/src/handlers/check_commits/modified_submodule.rs
index f0e63b5af..dcd3d7867 100644
--- a/src/handlers/check_commits/modified_submodule.rs
+++ b/src/handlers/check_commits/modified_submodule.rs
@@ -1,6 +1,6 @@
 use crate::github::FileDiff;
 
-const SUBMODULE_WARNING_MSG: &str = "These commits modify **submodules**.";
+const SUBMODULE_WARNING_MSG: &str = "Some commits in this PR modify **submodules**.";
 
 /// Returns a message if the PR modifies a git submodule.
 pub(super) fn modifies_submodule(diff: &[FileDiff]) -> Option<String> {
diff --git a/src/handlers/check_commits/no_mentions.rs b/src/handlers/check_commits/no_mentions.rs
new file mode 100644
index 000000000..cefa9d464
--- /dev/null
+++ b/src/handlers/check_commits/no_mentions.rs
@@ -0,0 +1,78 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use std::fmt::Write;
+
+use crate::{config::NoMentionsConfig, github::GithubCommit};
+
+pub(super) fn mentions_in_commits(
+    _conf: &NoMentionsConfig,
+    commits: &[GithubCommit],
+) -> Option<String> {
+    let mut mentions_commits = Vec::new();
+
+    for commit in commits {
+        if !parser::get_mentions(&commit.commit.message).is_empty() {
+            mentions_commits.push(&*commit.sha);
+        }
+    }
+
+    if mentions_commits.is_empty() {
+        None
+    } else {
+        Some(mentions_in_commits_warn(mentions_commits))
+    }
+}
+
+fn mentions_in_commits_warn(commits: Vec<&str>) -> String {
+    let mut warning = format!("There are username mentions (such as `@user`) in the commit messages of the following commits.\n  *Please remove the mentions to avoid spamming these users.*\n");
+
+    for commit in commits {
+        let _ = writeln!(warning, "    - {commit}");
+    }
+
+    warning
+}
+
+#[test]
+fn test_mentions_in_commits() {
+    fn dummy_commit_from_body(sha: &str, body: &str) -> GithubCommit {
+        use chrono::{DateTime, FixedOffset};
+
+        GithubCommit {
+            sha: sha.to_string(),
+            commit: crate::github::GithubCommitCommitField {
+                author: crate::github::GitUser {
+                    date: DateTime::<FixedOffset>::MIN_UTC.into(),
+                },
+                message: body.to_string(),
+                tree: crate::github::GitCommitTree {
+                    sha: "60ff73dfdd81aa1e6737eb3dacdfd4a141f6e14d".to_string(),
+                },
+            },
+            parents: vec![],
+        }
+    }
+
+    let mut commits = vec![dummy_commit_from_body(
+        "d1992a392617dfb10518c3e56446b6c9efae38b0",
+        "This is simple without mentions!",
+    )];
+
+    assert_eq!(mentions_in_commits(&NoMentionsConfig {}, &commits), None);
+
+    commits.push(dummy_commit_from_body(
+        "d7daa17bc97df9377640b0d33cbd0bbeed703c3a",
+        "This is a body with a @mention!",
+    ));
+
+    assert_eq!(
+        mentions_in_commits(&NoMentionsConfig {}, &commits),
+        Some(
+            r#"There are username mentions (such as `@user`) in the commit messages of the following commits.
+  *Please remove the mentions to avoid spamming these users.*
+    - d7daa17bc97df9377640b0d33cbd0bbeed703c3a
+"#.to_string()
+        )
+    );
+}


Reviews:
user: login: Kobzol
body: Looks good, but left a few comments.

When multiple warnings are included together, it looks a bit [confusing](https://github.com/Kobzol/bors-kindergarten/pull/26#issuecomment-2785792618), because the submodules handler doesn't actually list the actual commits with submodules. Maybe we could reword it from "These commits modify submodules" to "Some commits modify submodules".
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T09:23:05Z
user: login: Urgau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T17:11:25Z
user: login: Urgau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T17:12:13Z
user: login: Kobzol
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T11:28:33Z
user: login: Kobzol
body: Variable shadowing sometimes is a bit dangerous! :laughing: Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-09T16:41:45Z

Review comments:
diff_hunk: @@ -0,0 +1,158 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use crate::{
+    config::NoMentionsConfig,
+    db::issue_data::IssueData,
+    github::{IssuesAction, IssuesEvent, ReportedContentClassifiers},
+    handlers::Context,
+};
+use anyhow::Context as _;
+use serde::{Deserialize, Serialize};
+use std::collections::HashSet;
+use std::fmt::Write;
+use tracing as log;
+
+const NO_MENTIONS_KEY: &str = "no_mentions";
+
+pub(super) struct NoMentionsInput {
+    /// Hashes of commits that have mentions in the pull request.
+    mentions_commits: HashSet<String>,
+}
+
+#[derive(Debug, Default, Deserialize, Serialize)]
+struct NoMentionsState {
+    /// Hashes of mention commits that have already been mentioned by triagebot in a comment.
path: src/handlers/no_mentions.rs
user: login: Kobzol
body: Too many `mention`s :joy:
created_at: 2025-04-08T09:16:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use std::fmt::Write;
+
+use crate::{config::NoMentionsConfig, github::GithubCommit};
+
+pub(super) fn mentions_in_commits(
+    _conf: &NoMentionsConfig,
+    commits: &[GithubCommit],
+) -> Option<String> {
+    let mut mentions_commits = Vec::new();
+
+    for commit in commits {
+        if !parser::get_mentions(&commit.commit.message).is_empty() {
+            mentions_commits.push(&*commit.sha);
+        }
+    }
+
+    if mentions_commits.is_empty() {
+        None
+    } else {
+        Some(mentions_in_commits_warn(mentions_commits))
+    }
+}
+
+fn mentions_in_commits_warn(commits: Vec<&str>) -> String {
+    let mut warning =
+        format!("There are mentions (`@user`) in the following commits, please remove them.\n");
path: src/handlers/check_commits/no_mentions.rs
user: login: Kobzol
body: ```suggestion
        format!("There are username mentions (such as `@user`) in the commit messages of the following commits. Please remove the mentions to avoid spamming these users.\n");
```
created_at: 2025-04-08T09:17:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use std::fmt::Write;
+
+use crate::{config::NoMentionsConfig, github::GithubCommit};
+
+pub(super) fn mentions_in_commits(
+    _conf: &NoMentionsConfig,
+    commits: &[GithubCommit],
+) -> Option<String> {
+    let mut mentions_commits = Vec::new();
+
+    for commit in commits {
+        if !parser::get_mentions(&commit.commit.message).is_empty() {
+            mentions_commits.push(&*commit.sha);
+        }
+    }
+
+    if mentions_commits.is_empty() {
+        None
+    } else {
+        Some(mentions_in_commits_warn(mentions_commits))
+    }
+}
+
+fn mentions_in_commits_warn(commits: Vec<&str>) -> String {
+    let mut warning =
+        format!("There are mentions (`@user`) in the following commits, please remove them.\n");
+
+    for commit in commits {
+        let _ = writeln!(warning, "    - {commit}");
+    }
+
+    warning
+}
+
+#[test]
+fn test_warning_printing() {
path: src/handlers/check_commits/no_mentions.rs
user: login: Kobzol
body: It shouldn't be that hard to test the actual functionality more end-to-end. Could you please test `mentions_in_commits` instead of just `mentions_in_commits_warn`? Should be enough to construct fake `GitHubCommit`s, we can add helper functions for doing that into `src/tests`, it will surely be useful in other places.

I want to add tests to new functionality or modifications if possible, and here I think it's a low-hanging fruit.
created_at: 2025-04-08T09:19:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use std::fmt::Write;
+
+use crate::{config::NoMentionsConfig, github::GithubCommit};
+
+pub(super) fn mentions_in_commits(
+    _conf: &NoMentionsConfig,
+    commits: &[GithubCommit],
+) -> Option<String> {
+    let mut mentions_commits = Vec::new();
+
+    for commit in commits {
+        if !parser::get_mentions(&commit.commit.message).is_empty() {
+            mentions_commits.push(&*commit.sha);
+        }
+    }
+
+    if mentions_commits.is_empty() {
+        None
+    } else {
+        Some(mentions_in_commits_warn(mentions_commits))
+    }
+}
+
+fn mentions_in_commits_warn(commits: Vec<&str>) -> String {
+    let mut warning =
+        format!("There are mentions (`@user`) in the following commits, please remove them.\n");
path: src/handlers/check_commits/no_mentions.rs
user: login: Urgau
body: I was trying to avoid a too long line compared to the other warnings, but going to the line between the two sentences makes it okay. Changed.
created_at: 2025-04-08T17:11:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use std::fmt::Write;
+
+use crate::{config::NoMentionsConfig, github::GithubCommit};
+
+pub(super) fn mentions_in_commits(
+    _conf: &NoMentionsConfig,
+    commits: &[GithubCommit],
+) -> Option<String> {
+    let mut mentions_commits = Vec::new();
+
+    for commit in commits {
+        if !parser::get_mentions(&commit.commit.message).is_empty() {
+            mentions_commits.push(&*commit.sha);
+        }
+    }
+
+    if mentions_commits.is_empty() {
+        None
+    } else {
+        Some(mentions_in_commits_warn(mentions_commits))
+    }
+}
+
+fn mentions_in_commits_warn(commits: Vec<&str>) -> String {
+    let mut warning =
+        format!("There are mentions (`@user`) in the following commits, please remove them.\n");
+
+    for commit in commits {
+        let _ = writeln!(warning, "    - {commit}");
+    }
+
+    warning
+}
+
+#[test]
+fn test_warning_printing() {
path: src/handlers/check_commits/no_mentions.rs
user: login: Urgau
body: Sure, added an end-to-end test, not sure about the quality of it, but works for our use-case.
created_at: 2025-04-08T17:12:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,56 @@
+//! Purpose: When opening a PR, or pushing new changes, check for github mentions
+//! in commits and notify the user of our no-mentions in commits policy.
+
+use std::fmt::Write;
+
+use crate::{config::NoMentionsConfig, github::GithubCommit};
+
+pub(super) fn mentions_in_commits(
+    _conf: &NoMentionsConfig,
+    commits: &[GithubCommit],
+) -> Option<String> {
+    let mut mentions_commits = Vec::new();
+
+    for commit in commits {
+        if !parser::get_mentions(&commit.commit.message).is_empty() {
+            mentions_commits.push(&*commit.sha);
+        }
+    }
+
+    if mentions_commits.is_empty() {
+        None
+    } else {
+        Some(mentions_in_commits_warn(mentions_commits))
+    }
+}
+
+fn mentions_in_commits_warn(commits: Vec<&str>) -> String {
+    let mut warning =
+        format!("There are mentions (`@user`) in the following commits, please remove them.\n");
+
+    for commit in commits {
+        let _ = writeln!(warning, "    - {commit}");
+    }
+
+    warning
+}
+
+#[test]
+fn test_warning_printing() {
path: src/handlers/check_commits/no_mentions.rs
user: login: Kobzol
body: Thanks, much better than nothing!
created_at: 2025-04-09T11:28:33Z
author_association: MEMBER

PR comments:
user: login: ehuss
created_at: 2025-03-26T19:22:46Z
body: If this is intended to go in rust-lang/rust, then I think this would need approval from the primary teams that would be affected there (mainly compiler and libs I'm thinking), but I imagine would transitively also be required for all subtree teams, or otherwise it seems like it would be quite noisy. It's also not clear to me that "need to be removed" is not true, as I don't see anything that would prevent it.

I'd also say that this doesn't seem to cover the PR description which I think is the more common culprit. However, that will conflict with `r?` commands or general comments people leave to copy other people.

I have a slight worry that this will be too overbearing.

author_association: CONTRIBUTOR
user: login: Urgau
created_at: 2025-03-26T19:53:55Z
body: Sure, this would need buying from subtree first.

> I'd also say that this doesn't seem to cover the PR description which I think is the more common culprit. However, that will conflict with r? commands or general comments people leave to copy other people.

I think this is in part already handled by mentions being escaped in homu: [rust-lang/homu#100](https://github.com/rust-lang/homu/pull/100)

Non-homu handled repositories would still have this issue, but I think PR description are not as bad as commit messages. I have rarely see people link merge commit (where the PR description end up), while it's much more common for individual commits IMO to be copied. 

> It's also not clear to me that "need to be removed" is not true, as I don't see anything that would prevent it.

It's the same generic message as the one regarding merge commits not being allowed, which is also not enforced, particularly for subtrees.

> I have a slight worry that this will be too overbearing.

As proposed it's limited it to commit message, which hopefully will make it near invisible to most people.
author_association: MEMBER
user: login: Kobzol
created_at: 2025-03-27T09:40:44Z
body: The implementation looks good to me. It omits some functionality of `no_merges` though, notably ignoring drafts and `exclude_titles`. I'm a bit worried about duplicating the commit loading between these two handlers. After a push to a PR, two handlers will not attempt to load all commits of a PR, which seems a bit wasteful. But since they don't run concurrently, it's probably fine? I wonder if we should perhaps merge these two handlers, into something like "check_commits" handler. They share a lot of code anyway, and in this way we could scan the commits just once and then check merge commits and mentions together.

Same as with `no_merges`, I think that it's fine that this warns, but doesn't otherwise prevent the PR from being merged.
I'm not sure if the subtrees are such a problem? On sync PRs, this might send a notice if the subtree uses a mention in its commits, but that doesn't seem like such an issue.
author_association: MEMBER
user: login: Urgau
created_at: 2025-03-27T17:44:36Z
body: We should probably ignore the draft state. That's a good observation. Excluding some titles seems a bit much, we can add it later if necessary.

Regarding the multiple load of commits, see my response in https://github.com/rust-lang/triagebot/pull/1901#issuecomment-2758928121.

> I'm not sure if the subtrees are such a problem?

Agree with you, it's only a warning, it's not blocking in anyway. I also want to deploy the handler in subtrees first, so it shouldn't be a problem in practice.
author_association: MEMBER
user: login: Urgau
created_at: 2025-04-07T19:23:05Z
body: Contrary to what I laid out in https://github.com/rust-lang/triagebot/pull/1901#issuecomment-2770008801, I rebased in this PR first since it's much much simpler than moving the merge commits handler.

Should be ready for review.
author_association: MEMBER
user: login: Kobzol
created_at: 2025-04-09T11:32:32Z
body: Looks good! However, while testing, I found out that the detection of "same warnings" in `check_commits` is broken :sweat_smile:
[Here](https://github.com/rust-lang/triagebot/blob/596f0c5f60cd0b87175994393325b14f9615334e/src/handlers/check_commits.rs#L101) we overwrite the `warnings` variable by adding an asterisk to them, so the bot always thinks that the warnings are different, even though they are in fact the same.

Could you fix that in this PR please? Since it touches these files anyway.
author_association: MEMBER
user: login: Urgau
created_at: 2025-04-09T16:40:16Z
body: That's embarrassing. Fixed as a separate commit.
author_association: MEMBER