Skip to content

Commit 14e674a

Browse files
committed
Add no GitHub mentions in commits handler
1 parent d45a79f commit 14e674a

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
@@ -46,6 +46,7 @@ pub(crate) struct Config {
4646
pub(crate) merge_conflicts: Option<MergeConflictConfig>,
4747
pub(crate) bot_pull_requests: Option<BotPullRequests>,
4848
pub(crate) rendered_link: Option<RenderedLinkConfig>,
49+
pub(crate) no_mentions: Option<NoMentionsConfig>,
4950
}
5051

5152
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -414,6 +415,11 @@ pub(crate) struct RenderedLinkConfig {
414415
pub(crate) trigger_files: Vec<String>,
415416
}
416417

418+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
419+
#[serde(rename_all = "kebab-case")]
420+
#[serde(deny_unknown_fields)]
421+
pub(crate) struct NoMentionsConfig {}
422+
417423
fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
418424
let cache = CONFIG_CACHE.read().unwrap();
419425
cache.get(repo).and_then(|(config, fetch_time)| {
@@ -537,6 +543,8 @@ mod tests {
537543
538544
[rendered-link]
539545
trigger-files = ["posts/"]
546+
547+
[no-mentions]
540548
"#;
541549
let config = toml::from_str::<Config>(&config).unwrap();
542550
let mut ping_teams = HashMap::new();
@@ -598,7 +606,8 @@ mod tests {
598606
bot_pull_requests: None,
599607
rendered_link: Some(RenderedLinkConfig {
600608
trigger_files: vec!["posts/".to_string()]
601-
})
609+
}),
610+
no_mentions: Some(NoMentionsConfig {}),
602611
}
603612
);
604613
}
@@ -662,6 +671,7 @@ mod tests {
662671
merge_conflicts: None,
663672
bot_pull_requests: None,
664673
rendered_link: None,
674+
no_mentions: None,
665675
}
666676
);
667677
}

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod major_change;
3535
mod mentions;
3636
mod merge_conflicts;
3737
mod milestone_prs;
38+
mod no_mentions;
3839
mod no_merges;
3940
mod nominate;
4041
mod note;
@@ -220,6 +221,7 @@ issue_handlers! {
220221
review_requested,
221222
pr_tracking,
222223
validate_config,
224+
no_mentions,
223225
}
224226

225227
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)