Skip to content

Commit f1135d2

Browse files
authored
Merge pull request #366 from Kobzol/pr-approval-sanity-checks
Do not allow approving WIP PRs
2 parents 78f8362 + b47cc09 commit f1135d2

File tree

5 files changed

+62
-7
lines changed

5 files changed

+62
-7
lines changed

src/bors/comment.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ pub fn approve_non_open_pr_comment() -> Comment {
218218
Comment::new("Only open, non-draft PRs can be approved".to_string())
219219
}
220220

221+
pub fn approve_wip_title(keyword: &str) -> Comment {
222+
Comment::new(format!(
223+
r":clipboard: Looks like this PR is still in progress, ignoring approval.
224+
225+
Hint: Remove **{keyword}** from this PR's title when it is ready for review.
226+
"
227+
))
228+
}
229+
221230
pub fn delegate_try_builds_comment(delegatee: &str, bot_prefix: &CommandPrefix) -> Comment {
222231
Comment::new(format!(
223232
r":v: @{delegatee}, you can now perform try builds on this pull request!

src/bors/handlers/review.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::bors::RepositoryState;
55
use crate::bors::command::RollupMode;
66
use crate::bors::command::{Approver, CommandPrefix};
77
use crate::bors::comment::{
8-
approve_non_open_pr_comment, delegate_comment, delegate_try_builds_comment,
8+
approve_non_open_pr_comment, approve_wip_title, delegate_comment, delegate_try_builds_comment,
99
};
1010
use crate::bors::handlers::has_permission;
1111
use crate::bors::handlers::labels::handle_label_trigger;
@@ -38,10 +38,10 @@ pub(super) async fn command_approve(
3838
return Ok(());
3939
};
4040

41-
if !matches!(pr.github.status, PullRequestStatus::Open) {
41+
if let Some(error_comment) = check_pr_approval_validity(pr).await? {
4242
repo_state
4343
.client
44-
.post_comment(pr.number(), approve_non_open_pr_comment())
44+
.post_comment(pr.number(), error_comment)
4545
.await?;
4646
return Ok(());
4747
}
@@ -62,6 +62,28 @@ pub(super) async fn command_approve(
6262
notify_of_approval(&repo_state, pr, approver.as_str()).await
6363
}
6464

65+
/// Keywords that will prevent an approval if they appear in the PR's title.
66+
/// They are checked in a case-insensitive manner.
67+
const WIP_KEYWORDS: &[&str] = &["wip", "[do not merge]"];
68+
69+
/// Check that the given PR can be approved in its current state.
70+
/// Returns `Ok(Some(comment))` if it **cannot** be approved; the comment should be sent to the
71+
/// pull request.
72+
async fn check_pr_approval_validity(pr: &PullRequestData) -> anyhow::Result<Option<Comment>> {
73+
// Check PR status
74+
if !matches!(pr.github.status, PullRequestStatus::Open) {
75+
return Ok(Some(approve_non_open_pr_comment()));
76+
}
77+
78+
// Check WIP title
79+
let title = pr.github.title.to_lowercase();
80+
if let Some(wip_kw) = WIP_KEYWORDS.iter().find(|kw| title.contains(*kw)) {
81+
return Ok(Some(approve_wip_title(wip_kw)));
82+
}
83+
84+
Ok(None)
85+
}
86+
6587
/// Unapprove a pull request.
6688
/// Pull request's author can also unapprove the pull request.
6789
pub(super) async fn command_unapprove(
@@ -288,6 +310,7 @@ async fn notify_of_delegation(
288310
#[cfg(test)]
289311
mod tests {
290312
use crate::database::{DelegatedPermission, TreeState};
313+
use crate::tests::BorsTester;
291314
use crate::{
292315
bors::{
293316
RollupMode,
@@ -1113,4 +1136,24 @@ mod tests {
11131136
})
11141137
.await;
11151138
}
1139+
1140+
#[sqlx::test]
1141+
async fn approve_wip_pr(pool: sqlx::PgPool) {
1142+
run_test(pool, async |tester: &mut BorsTester| {
1143+
tester
1144+
.edit_pr(default_repo_name(), default_pr_number(), |pr| {
1145+
pr.title = "[do not merge] CI experiments".to_string();
1146+
})
1147+
.await?;
1148+
tester.post_comment("@bors r+").await?;
1149+
insta::assert_snapshot!(tester.get_comment().await?, @r"
1150+
:clipboard: Looks like this PR is still in progress, ignoring approval.
1151+
1152+
Hint: Remove **[do not merge]** from this PR's title when it is ready for review.
1153+
");
1154+
tester.default_pr().await.expect_unapproved();
1155+
Ok(())
1156+
})
1157+
.await;
1158+
}
11161159
}

src/bors/handlers/trybuild.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ It fixes so many issues, sir."
562562

563563
insta::assert_snapshot!(tester.get_branch_commit_message(&tester.try_branch()), @r"
564564
Auto merge of rust-lang/borstest#1 - pr-1, r=<try>
565-
PR #1
565+
Title of PR 1
566566
");
567567
Ok(())
568568
})
@@ -591,7 +591,7 @@ try-job: Bar
591591

592592
insta::assert_snapshot!(tester.get_branch_commit_message(&tester.try_branch()), @r"
593593
Auto merge of rust-lang/borstest#1 - pr-1, r=<try>
594-
PR #1
594+
Title of PR 1
595595
596596
try-job: Foo
597597
try-job: Bar
@@ -620,7 +620,7 @@ try-job: Bar
620620

621621
insta::assert_snapshot!(tester.get_branch_commit_message(&tester.try_branch()), @r"
622622
Auto merge of rust-lang/borstest#1 - pr-1, r=<try>
623-
PR #1
623+
Title of PR 1
624624
625625
626626
try-job: Baz

src/tests/mocks/pull_request.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,13 @@ impl From<PullRequest> for GitHubPullRequest {
214214
closed_at,
215215
assignees,
216216
description,
217+
title,
217218
} = pr;
218219
GitHubPullRequest {
219220
user: author.clone().into(),
220221
url: "https://test.com".to_string(),
221222
id: number.0 + 1000,
222-
title: format!("PR #{number}"),
223+
title,
223224
body: description,
224225
mergeable_state,
225226
draft: status == PullRequestStatus::Draft,

src/tests/mocks/repository.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub struct PullRequest {
5555
pub closed_at: Option<DateTime<Utc>>,
5656
pub assignees: Vec<User>,
5757
pub description: String,
58+
pub title: String,
5859
}
5960

6061
impl PullRequest {
@@ -78,6 +79,7 @@ impl PullRequest {
7879
closed_at: None,
7980
assignees: Vec::new(),
8081
description: format!("Description of PR {number}"),
82+
title: format!("Title of PR {number}"),
8183
}
8284
}
8385
}

0 commit comments

Comments
 (0)