@@ -5,7 +5,7 @@ use crate::bors::RepositoryState;
55use crate :: bors:: command:: RollupMode ;
66use crate :: bors:: command:: { Approver , CommandPrefix } ;
77use 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} ;
1010use crate :: bors:: handlers:: has_permission;
1111use 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.
6789pub ( super ) async fn command_unapprove (
@@ -288,6 +310,7 @@ async fn notify_of_delegation(
288310#[ cfg( test) ]
289311mod 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}
0 commit comments