Skip to content

Commit 542f03d

Browse files
committed
handlers: add no merge policy notifications
Add a handler for issue events that checks whether a merge commit has been added to the pull request and informs the user of the project's no merge policy. Signed-off-by: David Wood <david.wood@huawei.com>
1 parent 7594315 commit 542f03d

File tree

4 files changed

+166
-0
lines changed

4 files changed

+166
-0
lines changed

src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub(crate) struct Config {
3333
pub(crate) shortcut: Option<ShortcutConfig>,
3434
pub(crate) note: Option<NoteConfig>,
3535
pub(crate) mentions: Option<MentionsConfig>,
36+
pub(crate) no_merges: Option<NoMergesConfig>,
3637
}
3738

3839
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -79,6 +80,12 @@ pub(crate) struct AssignConfig {
7980
_empty: (),
8081
}
8182

83+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
84+
pub(crate) struct NoMergesConfig {
85+
#[serde(default)]
86+
_empty: (),
87+
}
88+
8289
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
8390
pub(crate) struct NoteConfig {
8491
#[serde(default)]
@@ -365,6 +372,7 @@ mod tests {
365372
github_releases: None,
366373
review_submitted: None,
367374
mentions: None,
375+
no_merges: None,
368376
}
369377
);
370378
}

src/github.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,33 @@ impl Issue {
749749
Ok(Some(String::from(String::from_utf8_lossy(&diff))))
750750
}
751751

752+
/// Returns the commits from this pull request (no commits are returned if this `Issue` is not
753+
/// a pull request).
754+
pub async fn commits(&self, client: &GithubClient) -> anyhow::Result<Vec<GithubCommit>> {
755+
if !self.is_pr() {
756+
return Ok(vec![]);
757+
}
758+
759+
let mut commits = Vec::new();
760+
let mut page = 1;
761+
loop {
762+
let req = client.get(&format!(
763+
"{}/pulls/{}/commits?page={page}&per_page=100",
764+
self.repository().url(),
765+
self.number
766+
));
767+
768+
let new: Vec<_> = client.json(req).await?;
769+
if new.is_empty() {
770+
break;
771+
}
772+
commits.extend(new);
773+
774+
page += 1;
775+
}
776+
Ok(commits)
777+
}
778+
752779
pub async fn files(&self, client: &GithubClient) -> anyhow::Result<Vec<PullRequestFile>> {
753780
if !self.is_pr() {
754781
return Ok(vec![]);

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod glacier;
3131
mod major_change;
3232
mod mentions;
3333
mod milestone_prs;
34+
mod no_merges;
3435
mod nominate;
3536
mod note;
3637
mod notification;
@@ -155,6 +156,7 @@ issue_handlers! {
155156
autolabel,
156157
major_change,
157158
mentions,
159+
no_merges,
158160
notify_zulip,
159161
}
160162

src/handlers/no_merges.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//! Purpose: When opening a PR, or pushing new changes, check for merge commits
2+
//! and notify the user of our no-merge policy.
3+
4+
use crate::{
5+
config::NoMergesConfig,
6+
db::issue_data::IssueData,
7+
github::{IssuesAction, IssuesEvent},
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_MERGES_KEY: &str = "no_merges";
17+
18+
pub(super) struct NoMergesInput {
19+
/// Hashes of merge commits in the pull request.
20+
merge_commits: HashSet<String>,
21+
}
22+
23+
#[derive(Debug, Default, Deserialize, Serialize)]
24+
struct NoMergesState {
25+
/// Hashes of merge commits that have already been mentioned by triagebot in a comment.
26+
mentioned_merge_commits: HashSet<String>,
27+
}
28+
29+
pub(super) async fn parse_input(
30+
ctx: &Context,
31+
event: &IssuesEvent,
32+
config: Option<&NoMergesConfig>,
33+
) -> Result<Option<NoMergesInput>, String> {
34+
if !matches!(
35+
event.action,
36+
IssuesAction::Opened | IssuesAction::Synchronize | IssuesAction::ReadyForReview
37+
) {
38+
return Ok(None);
39+
}
40+
41+
// Require an empty configuration block to enable no-merges notifications.
42+
if config.is_none() {
43+
return Ok(None);
44+
}
45+
46+
// Don't ping on rollups or draft PRs.
47+
if event.issue.title.starts_with("Rollup of") || event.issue.draft {
48+
return Ok(None);
49+
}
50+
51+
let mut merge_commits = HashSet::new();
52+
let commits = event
53+
.issue
54+
.commits(&ctx.github)
55+
.await
56+
.map_err(|e| {
57+
log::error!("failed to fetch commits: {:?}", e);
58+
})
59+
.unwrap_or_default();
60+
for commit in commits {
61+
if commit.parents.len() > 1 {
62+
merge_commits.insert(commit.sha.clone());
63+
}
64+
}
65+
66+
let input = NoMergesInput { merge_commits };
67+
Ok(if input.merge_commits.is_empty() {
68+
None
69+
} else {
70+
Some(input)
71+
})
72+
}
73+
74+
pub(super) async fn handle_input(
75+
ctx: &Context,
76+
_config: &NoMergesConfig,
77+
event: &IssuesEvent,
78+
input: NoMergesInput,
79+
) -> anyhow::Result<()> {
80+
let mut client = ctx.db.get().await;
81+
let mut state: IssueData<'_, NoMergesState> =
82+
IssueData::load(&mut client, &event.issue, NO_MERGES_KEY).await?;
83+
84+
let since_last_posted = if state.data.mentioned_merge_commits.is_empty() {
85+
""
86+
} else {
87+
" (since this message was last posted)"
88+
};
89+
90+
let mut should_send = false;
91+
let mut message = format!(
92+
"
93+
There are merge commits (commits with multiple parents) in your changes. We have a
94+
[no merge policy](https://rustc-dev-guide.rust-lang.org/git.html#no-merge-policy) so
95+
these commits will need to be removed for this pull request to be merged.
96+
97+
You can start a rebase with the following commands:
98+
99+
```shell-session
100+
$ # rebase
101+
$ git rebase -i master
102+
$ # delete any merge commits in the editor that appears
103+
$ git push --force-with-lease
104+
```
105+
106+
The following commits are merge commits{since_last_posted}:
107+
108+
"
109+
);
110+
for commit in &input.merge_commits {
111+
if state.data.mentioned_merge_commits.contains(commit) {
112+
continue;
113+
}
114+
115+
should_send = true;
116+
state.data.mentioned_merge_commits.insert((*commit).clone());
117+
write!(message, "- {commit}").unwrap();
118+
}
119+
120+
if should_send {
121+
event
122+
.issue
123+
.post_comment(&ctx.github, &message)
124+
.await
125+
.context("failed to post no_merges comment")?;
126+
state.save().await?;
127+
}
128+
Ok(())
129+
}

0 commit comments

Comments
 (0)