Skip to content

Commit d1992a3

Browse files
committed
Add no GitHub mentions in commits handler
1 parent cc054e0 commit d1992a3

File tree

3 files changed

+171
-1
lines changed

3 files changed

+171
-1
lines changed

src/config.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub(crate) struct Config {
4747
pub(crate) bot_pull_requests: Option<BotPullRequests>,
4848
pub(crate) rendered_link: Option<RenderedLinkConfig>,
4949
pub(crate) canonicalize_issue_links: Option<CanonicalizeIssueLinksConfig>,
50+
pub(crate) no_mentions: Option<NoMentionsConfig>,
5051
}
5152

5253
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -420,6 +421,11 @@ pub(crate) struct RenderedLinkConfig {
420421
#[serde(deny_unknown_fields)]
421422
pub(crate) struct CanonicalizeIssueLinksConfig {}
422423

424+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
425+
#[serde(rename_all = "kebab-case")]
426+
#[serde(deny_unknown_fields)]
427+
pub(crate) struct NoMentionsConfig {}
428+
423429
fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
424430
let cache = CONFIG_CACHE.read().unwrap();
425431
cache.get(repo).and_then(|(config, fetch_time)| {
@@ -545,6 +551,8 @@ mod tests {
545551
546552
[rendered-link]
547553
trigger-files = ["posts/"]
554+
555+
[no-mentions]
548556
"#;
549557
let config = toml::from_str::<Config>(&config).unwrap();
550558
let mut ping_teams = HashMap::new();
@@ -608,6 +616,7 @@ mod tests {
608616
trigger_files: vec!["posts/".to_string()]
609617
}),
610618
canonicalize_issue_links: Some(CanonicalizeIssueLinksConfig {}),
619+
no_mentions: Some(NoMentionsConfig {}),
611620
}
612621
);
613622
}
@@ -671,7 +680,8 @@ mod tests {
671680
merge_conflicts: None,
672681
bot_pull_requests: None,
673682
rendered_link: None,
674-
canonicalize_issue_links: None
683+
canonicalize_issue_links: None,
684+
no_mentions: None,
675685
}
676686
);
677687
}

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mod major_change;
3737
mod mentions;
3838
mod merge_conflicts;
3939
mod milestone_prs;
40+
mod no_mentions;
4041
mod no_merges;
4142
mod nominate;
4243
mod note;
@@ -233,6 +234,7 @@ issue_handlers! {
233234
review_requested,
234235
pr_tracking,
235236
validate_config,
237+
no_mentions,
236238
}
237239

238240
macro_rules! command_handlers {

src/handlers/no_mentions.rs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//! Purpose: When opening a PR, or pushing new changes, check for github mentions
2+
//! in commits and notify the user of our no-mentions in commits policy.
3+
4+
use crate::{
5+
config::NoMentionsConfig,
6+
db::issue_data::IssueData,
7+
github::{IssuesAction, IssuesEvent, ReportedContentClassifiers},
8+
handlers::Context,
9+
};
10+
use anyhow::Context as _;
11+
use serde::{Deserialize, Serialize};
12+
use std::collections::HashSet;
13+
use std::fmt::Write;
14+
use tracing as log;
15+
16+
const NO_MENTIONS_KEY: &str = "no_mentions";
17+
18+
pub(super) struct NoMentionsInput {
19+
/// Hashes of commits that have mentions in the pull request.
20+
mentions_commits: HashSet<String>,
21+
}
22+
23+
#[derive(Debug, Default, Deserialize, Serialize)]
24+
struct NoMentionsState {
25+
/// Hashes of mention commits that have already been mentioned by triagebot in a comment.
26+
mentioned_mention_commits: HashSet<String>,
27+
/// List of all the no_mention comments as GitHub GraphQL NodeId.
28+
#[serde(default)]
29+
no_mention_comments: Vec<String>,
30+
}
31+
32+
pub(super) async fn parse_input(
33+
ctx: &Context,
34+
event: &IssuesEvent,
35+
config: Option<&NoMentionsConfig>,
36+
) -> Result<Option<NoMentionsInput>, String> {
37+
if !matches!(
38+
event.action,
39+
IssuesAction::Opened | IssuesAction::Synchronize | IssuesAction::ReadyForReview
40+
) {
41+
return Ok(None);
42+
}
43+
44+
// Require a `[no_mentions]` configuration block to enable no-mentions notifications.
45+
let Some(_config) = config else {
46+
return Ok(None);
47+
};
48+
49+
let mut mentions_commits = HashSet::new();
50+
let commits = event
51+
.issue
52+
.commits(&ctx.github)
53+
.await
54+
.map_err(|e| {
55+
log::error!("failed to fetch commits: {:?}", e);
56+
})
57+
.unwrap_or_default();
58+
59+
for commit in commits {
60+
if !parser::get_mentions(&commit.commit.message).is_empty() {
61+
mentions_commits.insert(commit.sha.clone());
62+
}
63+
}
64+
65+
// Run the handler even if we have no mentions,
66+
// so we can take an action if some were removed.
67+
Ok(Some(NoMentionsInput { mentions_commits }))
68+
}
69+
70+
fn get_default_message() -> String {
71+
format!(
72+
"
73+
There are mentions (`@mention`) in your commits. We have a no mention policy
74+
so these commits will need to be removed for this pull request to be merged.
75+
76+
"
77+
)
78+
}
79+
80+
pub(super) async fn handle_input(
81+
ctx: &Context,
82+
_config: &NoMentionsConfig,
83+
event: &IssuesEvent,
84+
input: NoMentionsInput,
85+
) -> anyhow::Result<()> {
86+
let mut client = ctx.db.get().await;
87+
let mut state: IssueData<'_, NoMentionsState> =
88+
IssueData::load(&mut client, &event.issue, NO_MENTIONS_KEY).await?;
89+
90+
// No commits with mentions.
91+
if input.mentions_commits.is_empty() {
92+
if state.data.mentioned_mention_commits.is_empty() {
93+
// No commits with mentions from before, so do nothing.
94+
return Ok(());
95+
}
96+
97+
// Minimize prior no mention comments.
98+
for node_id in state.data.no_mention_comments.iter() {
99+
event
100+
.issue
101+
.hide_comment(
102+
&ctx.github,
103+
node_id.as_str(),
104+
ReportedContentClassifiers::Resolved,
105+
)
106+
.await
107+
.context("failed to hide previous no-mention comment")?;
108+
}
109+
110+
// Clear from state.
111+
state.data.mentioned_mention_commits.clear();
112+
state.data.no_mention_comments.clear();
113+
state.save().await?;
114+
return Ok(());
115+
}
116+
117+
let first_time = state.data.mentioned_mention_commits.is_empty();
118+
119+
let mut message = get_default_message();
120+
121+
let since_last_posted = if first_time {
122+
""
123+
} else {
124+
" (since this message was last posted)"
125+
};
126+
writeln!(
127+
message,
128+
"The following commits have mentions is them{since_last_posted}:"
129+
)
130+
.unwrap();
131+
132+
let mut should_send = false;
133+
for commit in &input.mentions_commits {
134+
if state.data.mentioned_mention_commits.contains(commit) {
135+
continue;
136+
}
137+
138+
should_send = true;
139+
state
140+
.data
141+
.mentioned_mention_commits
142+
.insert((*commit).clone());
143+
writeln!(message, "- {commit}").unwrap();
144+
}
145+
146+
if should_send {
147+
// Post comment
148+
let comment = event
149+
.issue
150+
.post_comment(&ctx.github, &message)
151+
.await
152+
.context("failed to post no_mentions comment")?;
153+
154+
state.data.no_mention_comments.push(comment.node_id);
155+
state.save().await?;
156+
}
157+
Ok(())
158+
}

0 commit comments

Comments
 (0)