Skip to content

Commit

Permalink
handlers: add no merge policy notifications
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
davidtwco committed Aug 31, 2022
1 parent 7594315 commit 0d61be8
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/config.rs
Expand Up @@ -33,6 +33,7 @@ pub(crate) struct Config {
pub(crate) shortcut: Option<ShortcutConfig>,
pub(crate) note: Option<NoteConfig>,
pub(crate) mentions: Option<MentionsConfig>,
pub(crate) no_merges: Option<NoMergesConfig>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
Expand Down Expand Up @@ -79,6 +80,12 @@ pub(crate) struct AssignConfig {
_empty: (),
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
pub(crate) struct NoMergesConfig {
#[serde(default)]
_empty: (),
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
pub(crate) struct NoteConfig {
#[serde(default)]
Expand Down Expand Up @@ -365,6 +372,7 @@ mod tests {
github_releases: None,
review_submitted: None,
mentions: None,
no_merges: None,
}
);
}
Expand Down
21 changes: 21 additions & 0 deletions src/github.rs
Expand Up @@ -749,6 +749,21 @@ impl Issue {
Ok(Some(String::from(String::from_utf8_lossy(&diff))))
}

/// Returns the commits from this pull request (no commits are returned if this `Issue` is not
/// a pull request).
pub async fn commits(&self, client: &GithubClient) -> anyhow::Result<Vec<Commit>> {
if !self.is_pr() {
return Ok(vec![]);
}

let req = client.get(&format!(
"{}/pulls/{}/commits",
self.repository().url(),
self.number
));
Ok(client.json(req).await?)
}

pub async fn files(&self, client: &GithubClient) -> anyhow::Result<Vec<PullRequestFile>> {
if !self.is_pr() {
return Ok(vec![]);
Expand All @@ -763,6 +778,12 @@ impl Issue {
}
}

#[derive(Debug, serde::Deserialize)]
pub struct Commit {
pub sha: String,
pub parents: Vec<Parent>,
}

#[derive(Debug, serde::Deserialize)]
pub struct PullRequestFile {
pub sha: String,
Expand Down
2 changes: 2 additions & 0 deletions src/handlers.rs
Expand Up @@ -31,6 +31,7 @@ mod glacier;
mod major_change;
mod mentions;
mod milestone_prs;
mod no_merges;
mod nominate;
mod note;
mod notification;
Expand Down Expand Up @@ -155,6 +156,7 @@ issue_handlers! {
autolabel,
major_change,
mentions,
no_merges,
notify_zulip,
}

Expand Down
141 changes: 141 additions & 0 deletions src/handlers/no_merges.rs
@@ -0,0 +1,141 @@
//! Purpose: When opening a PR, or pushing new changes, check for merge commits
//! and notify the user of our no-merge policy.

use crate::{
config::NoMergesConfig,
db::issue_data::IssueData,
github::{IssuesAction, IssuesEvent},
handlers::Context,
};
use anyhow::Context as _;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Write;
use tracing as log;

const NO_MERGES_KEY: &str = "no_merges";

pub(super) struct NoMergesInput {
/// Hashes of merge commits in the pull request.
merge_commits: HashSet<String>,
/// Hash of the commit before the first merge commit.
before_first_merge_commit: String,
}

#[derive(Debug, Default, Deserialize, Serialize)]
struct NoMergesState {
/// Hashes of merge commits that have already been mentioned by triagebot in a comment.
mentioned_merge_commits: HashSet<String>,
}

pub(super) async fn parse_input(
ctx: &Context,
event: &IssuesEvent,
config: Option<&NoMergesConfig>,
) -> Result<Option<NoMergesInput>, String> {
if !matches!(
event.action,
IssuesAction::Opened | IssuesAction::Synchronize | IssuesAction::ReadyForReview
) {
return Ok(None);
}

// Require an empty configuration block to enable no-merges notifications.
if config.is_none() {
return Ok(None);
}

// Don't ping on rollups or draft PRs.
if event.issue.title.starts_with("Rollup of") || event.issue.draft {
return Ok(None);
}

let mut merge_commits = HashSet::new();
let mut before_first_merge_commit = None;
let commits = event
.issue
.commits(&ctx.github)
.await
.map_err(|e| {
log::error!("failed to fetch commits: {:?}", e);
})
.unwrap_or_default();
for commit in commits {
if commit.parents.len() > 1 {
merge_commits.insert(commit.sha.clone());
}

if merge_commits.is_empty() {
before_first_merge_commit = Some(commit.sha);
}
}

let input = NoMergesInput {
merge_commits,
before_first_merge_commit: before_first_merge_commit
.unwrap_or_else(|| "master".to_string()),
};
Ok(if input.merge_commits.is_empty() {
None
} else {
Some(input)
})
}

pub(super) async fn handle_input(
ctx: &Context,
_config: &NoMergesConfig,
event: &IssuesEvent,
input: NoMergesInput,
) -> anyhow::Result<()> {
let mut client = ctx.db.get().await;
let mut state: IssueData<'_, NoMergesState> =
IssueData::load(&mut client, &event.issue, NO_MERGES_KEY).await?;

let since_last_posted = if state.data.mentioned_merge_commits.is_empty() {
""
} else {
" (since this message was last posted)"
};
let before_first_merge_commit = input.before_first_merge_commit;

let mut should_send = false;
let mut message = format!(
"
There are merge commits (commits with multiple parents) in your changes. We have a
[no merge policy](https://rustc-dev-guide.rust-lang.org/git.html#no-merge-policy) so
these commits will need to be removed for this pull request to be merged.
You can start a rebase with the following commands:
```shell-session
$ # rebase
$ git rebase -i {before_first_merge_commit}
$ # delete any merge commits in the editor that appears
$ git push --force-with-lease
```
The following commits are merge commits{since_last_posted}:
"
);
for commit in &input.merge_commits {
if state.data.mentioned_merge_commits.contains(commit) {
continue;
}

should_send = true;
state.data.mentioned_merge_commits.insert((*commit).clone());
write!(message, "- {commit}").unwrap();
}

if should_send {
event
.issue
.post_comment(&ctx.github, &message)
.await
.context("failed to post no_merges comment")?;
state.save().await?;
}
Ok(())
}

0 comments on commit 0d61be8

Please sign in to comment.