1
+ use std:: collections:: HashSet ;
2
+
1
3
use anyhow:: bail;
4
+ use anyhow:: Context as _;
2
5
3
6
use super :: Context ;
4
7
use crate :: {
5
8
config:: Config ,
6
9
db:: issue_data:: IssueData ,
7
- github:: { Event , IssuesAction , IssuesEvent , ReportedContentClassifiers } ,
10
+ github:: { Event , IssuesAction , IssuesEvent , Label , ReportedContentClassifiers } ,
8
11
} ;
9
12
10
13
#[ cfg( test) ]
@@ -13,6 +16,7 @@ use crate::github::GithubCommit;
13
16
mod issue_links;
14
17
mod modified_submodule;
15
18
mod no_mentions;
19
+ mod no_merges;
16
20
mod non_default_branch;
17
21
18
22
/// Key for the state in the database
@@ -25,6 +29,8 @@ struct CheckCommitsWarningsState {
25
29
last_warnings : Vec < String > ,
26
30
/// ID of the most recent warning comment.
27
31
last_warned_comment : Option < String > ,
32
+ /// List of the last labels added.
33
+ last_labels : Vec < String > ,
28
34
}
29
35
30
36
pub ( super ) async fn handle ( ctx : & Context , event : & Event , config : & Config ) -> anyhow:: Result < ( ) > {
@@ -34,12 +40,17 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
34
40
35
41
if !matches ! (
36
42
event. action,
37
- IssuesAction :: Opened | IssuesAction :: Synchronize
43
+ IssuesAction :: Opened | IssuesAction :: Synchronize | IssuesAction :: ReadyForReview
38
44
) || !event. issue . is_pr ( )
39
45
{
40
46
return Ok ( ( ) ) ;
41
47
}
42
48
49
+ // Don't ping on rollups or draft PRs.
50
+ if event. issue . title . starts_with ( "Rollup of" ) || event. issue . draft {
51
+ return Ok ( ( ) ) ;
52
+ }
53
+
43
54
let Some ( diff) = event. issue . diff ( & ctx. github ) . await ? else {
44
55
bail ! (
45
56
"expected issue {} to be a PR, but the diff could not be determined" ,
@@ -49,6 +60,7 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
49
60
let commits = event. issue . commits ( & ctx. github ) . await ?;
50
61
51
62
let mut warnings = Vec :: new ( ) ;
63
+ let mut labels = Vec :: new ( ) ;
52
64
53
65
// Compute the warnings
54
66
if let Some ( assign_config) = & config. assign {
@@ -72,14 +84,24 @@ pub(super) async fn handle(ctx: &Context, event: &Event, config: &Config) -> any
72
84
warnings. extend ( issue_links:: issue_links_in_commits ( issue_links, & commits) ) ;
73
85
}
74
86
75
- handle_warnings ( ctx, event, warnings) . await
87
+ if let Some ( no_merges) = & config. no_merges {
88
+ if let Some ( warn) =
89
+ no_merges:: merges_in_commits ( & event. issue . title , & event. repository , no_merges, & commits)
90
+ {
91
+ warnings. push ( warn. 0 ) ;
92
+ labels. extend ( warn. 1 ) ;
93
+ }
94
+ }
95
+
96
+ handle_warnings_and_labels ( ctx, event, warnings, labels) . await
76
97
}
77
98
78
99
// Add, hide or hide&add a comment with the warnings.
79
- async fn handle_warnings (
100
+ async fn handle_warnings_and_labels (
80
101
ctx : & Context ,
81
102
event : & IssuesEvent ,
82
103
warnings : Vec < String > ,
104
+ labels : Vec < String > ,
83
105
) -> anyhow:: Result < ( ) > {
84
106
// Get the state of the warnings for this PR in the database.
85
107
let mut db = ctx. db . get ( ) . await ;
@@ -105,10 +127,8 @@ async fn handle_warnings(
105
127
let warning = warning_from_warnings ( & warnings) ;
106
128
let comment = event. issue . post_comment ( & ctx. github , & warning) . await ?;
107
129
108
- // Save new state in the database
109
130
state. data . last_warnings = warnings;
110
131
state. data . last_warned_comment = Some ( comment. node_id ) ;
111
- state. save ( ) . await ?;
112
132
} else if warnings. is_empty ( ) {
113
133
// No warnings to be shown, let's resolve a previous warnings comment, if there was one.
114
134
if let Some ( last_warned_comment_id) = state. data . last_warned_comment {
@@ -123,10 +143,46 @@ async fn handle_warnings(
123
143
124
144
state. data . last_warnings = Vec :: new ( ) ;
125
145
state. data . last_warned_comment = None ;
126
- state. save ( ) . await ?;
127
146
}
128
147
}
129
148
149
+ // Handle the labels, add the new ones, remove the one no longer required, or don't do anything
150
+ if !state. data . last_labels . is_empty ( ) || !labels. is_empty ( ) {
151
+ let ( labels_to_remove, labels_to_add) =
152
+ calculate_label_changes ( & state. data . last_labels , & labels) ;
153
+
154
+ // Remove the labels no longer required
155
+ if !labels_to_remove. is_empty ( ) {
156
+ for label in labels_to_remove {
157
+ event
158
+ . issue
159
+ . remove_label ( & ctx. github , & label)
160
+ . await
161
+ . context ( "failed to remove a label in check_commits" ) ?;
162
+ }
163
+ }
164
+
165
+ // Add the labels that are now required
166
+ if !labels_to_add. is_empty ( ) {
167
+ event
168
+ . issue
169
+ . add_labels (
170
+ & ctx. github ,
171
+ labels_to_add
172
+ . into_iter ( )
173
+ . map ( |name| Label { name } )
174
+ . collect ( ) ,
175
+ )
176
+ . await
177
+ . context ( "failed to add labels in check_commits" ) ?;
178
+ }
179
+
180
+ state. data . last_labels = labels;
181
+ }
182
+
183
+ // Save new state in the database
184
+ state. save ( ) . await ?;
185
+
130
186
Ok ( ( ) )
131
187
}
132
188
@@ -139,6 +195,20 @@ fn warning_from_warnings(warnings: &[String]) -> String {
139
195
format ! ( ":warning: **Warning** :warning:\n \n {}" , warnings. join( "\n " ) )
140
196
}
141
197
198
+ // Calculate the label changes
199
+ fn calculate_label_changes (
200
+ previous : & Vec < String > ,
201
+ current : & Vec < String > ,
202
+ ) -> ( Vec < String > , Vec < String > ) {
203
+ let previous_set: HashSet < String > = previous. into_iter ( ) . cloned ( ) . collect ( ) ;
204
+ let current_set: HashSet < String > = current. into_iter ( ) . cloned ( ) . collect ( ) ;
205
+
206
+ let removals = previous_set. difference ( & current_set) . cloned ( ) . collect ( ) ;
207
+ let additions = current_set. difference ( & previous_set) . cloned ( ) . collect ( ) ;
208
+
209
+ ( removals, additions)
210
+ }
211
+
142
212
#[ cfg( test) ]
143
213
fn dummy_commit_from_body ( sha : & str , body : & str ) -> GithubCommit {
144
214
use chrono:: { DateTime , FixedOffset } ;
0 commit comments