6
6
//! - Adds the PR to the workqueue of one team member (after the PR has been assigned or reopened)
7
7
//! - Removes the PR from the workqueue of one team member (after the PR has been unassigned or closed)
8
8
9
- use crate :: github:: PullRequestNumber ;
9
+ use crate :: github:: { Label , PullRequestNumber } ;
10
10
use crate :: github:: { User , UserId } ;
11
11
use crate :: {
12
12
config:: ReviewPrefsConfig ,
13
13
github:: { IssuesAction , IssuesEvent } ,
14
14
handlers:: Context ,
15
15
} ;
16
16
use futures:: TryStreamExt ;
17
+ use octocrab:: models:: IssueState ;
17
18
use octocrab:: params:: pulls:: Sort ;
18
19
use octocrab:: params:: { Direction , State } ;
19
20
use octocrab:: Octocrab ;
20
21
use std:: collections:: { HashMap , HashSet } ;
22
+ use tokio:: sync:: RwLockWriteGuard ;
21
23
use tracing as log;
22
24
23
25
/// Maps users to a set of currently assigned open non-draft pull requests.
@@ -39,8 +41,7 @@ impl ReviewerWorkqueue {
39
41
pub ( super ) enum ReviewPrefsInput {
40
42
Assigned { assignee : User } ,
41
43
Unassigned { assignee : User } ,
42
- Reopened ,
43
- Closed ,
44
+ OtherChange ,
44
45
}
45
46
46
47
pub ( super ) async fn parse_input (
@@ -68,10 +69,12 @@ pub(super) async fn parse_input(
68
69
assignee : assignee. clone ( ) ,
69
70
} ) ) ,
70
71
// We don't need to handle Opened explicitly, because that will trigger the Assigned event
71
- IssuesAction :: Reopened => Ok ( Some ( ReviewPrefsInput :: Reopened ) ) ,
72
- IssuesAction :: Closed | IssuesAction :: Deleted | IssuesAction :: Transferred => {
73
- Ok ( Some ( ReviewPrefsInput :: Closed ) )
74
- }
72
+ IssuesAction :: Reopened
73
+ | IssuesAction :: Closed
74
+ | IssuesAction :: Deleted
75
+ | IssuesAction :: Transferred
76
+ | IssuesAction :: Labeled { .. }
77
+ | IssuesAction :: Unlabeled { .. } => Ok ( Some ( ReviewPrefsInput :: OtherChange ) ) ,
75
78
_ => Ok ( None ) ,
76
79
}
77
80
}
@@ -84,45 +87,52 @@ pub(super) async fn handle_input<'a>(
84
87
) -> anyhow:: Result < ( ) > {
85
88
log:: info!( "Handling event action {:?} in PR tracking" , event. action) ;
86
89
90
+ let pr = & event. issue ;
91
+ let pr_number = event. issue . number ;
92
+
93
+ let mut workqueue = ctx. workqueue . write ( ) . await ;
94
+
95
+ // If the PR doesn't wait for a review, remove it from the workqueue completely.
96
+ // This handles situations such as labels being modified, which make the PR no longer to be
97
+ // in the "waiting for a review" state, or the PR being closed/merged.
98
+ if !waits_for_a_review ( & pr. labels , pr. is_open ( ) , pr. draft ) {
99
+ log:: info!(
100
+ "Removing PR {pr_number} from workqueue, because it is not waiting for a review." ,
101
+ ) ;
102
+ delete_pr_from_all_queues ( & mut workqueue, pr_number) ;
103
+ return Ok ( ( ) ) ;
104
+ }
105
+
87
106
match input {
88
- // This handler is reached also when assigning a PR using the Github UI
89
- // (i.e. from the "Assignees" dropdown menu).
90
- // We need to also check assignee availability here.
107
+ // The PR was assigned to a specific user, and it is waiting for a review.
91
108
ReviewPrefsInput :: Assigned { assignee } => {
92
- let pr_number = event. issue . number ;
93
109
log:: info!(
94
- "Adding PR {pr_number} from workqueue of {} because they were assigned." ,
110
+ "Adding PR {pr_number} to workqueue of {} because they were assigned." ,
95
111
assignee. login
96
112
) ;
97
113
98
- upsert_pr_into_workqueue ( ctx , assignee. id , pr_number) . await ;
114
+ upsert_pr_into_user_queue ( & mut workqueue , assignee. id , pr_number) ;
99
115
}
100
116
ReviewPrefsInput :: Unassigned { assignee } => {
101
- let pr_number = event. issue . number ;
102
117
log:: info!(
103
118
"Removing PR {pr_number} from workqueue of {} because they were unassigned." ,
104
119
assignee. login
105
120
) ;
106
- delete_pr_from_workqueue ( ctx , assignee. id , pr_number) . await ;
121
+ delete_pr_from_user_queue ( & mut workqueue , assignee. id , pr_number) ;
107
122
}
108
- ReviewPrefsInput :: Closed => {
123
+ // Some other change has happened (e.g. labels changed or the PR being reopened).
124
+ // Make sure that all assigned users have the PR in their queue.
125
+ // When a PR is opened, it might not yet contain all the information needed to determine
126
+ // whether it waits for a reviewer or not. For example, when you open a PR,
127
+ // triagebot might apply the "S-waiting-on-review" (or similar) label to it, which we
128
+ // currently use to determine whether a PR is truly assigned to someone or not.
129
+ // We thus need to refresh the queue state after every relevant state change that we
130
+ // receive.
131
+ ReviewPrefsInput :: OtherChange => {
109
132
for assignee in & event. issue . assignees {
110
- let pr_number = event. issue . number ;
111
- log:: info!(
112
- "Removing PR {pr_number} from workqueue of {} because it was closed or merged." ,
113
- assignee. login
114
- ) ;
115
- delete_pr_from_workqueue ( ctx, assignee. id , pr_number) . await ;
116
- }
117
- }
118
- ReviewPrefsInput :: Reopened => {
119
- for assignee in & event. issue . assignees {
120
- let pr_number = event. issue . number ;
121
- log:: info!(
122
- "Re-adding PR {pr_number} to workqueue of {} because it was (re)opened." ,
123
- assignee. login
124
- ) ;
125
- upsert_pr_into_workqueue ( ctx, assignee. id , pr_number) . await ;
133
+ if upsert_pr_into_user_queue ( & mut workqueue, assignee. id , pr_number) {
134
+ log:: info!( "Adding PR {pr_number} to workqueue of {}." , assignee. login) ;
135
+ }
126
136
}
127
137
}
128
138
}
@@ -147,8 +157,10 @@ pub async fn load_workqueue(client: &Octocrab) -> anyhow::Result<ReviewerWorkque
147
157
}
148
158
149
159
/// Retrieve tuples of (user, PR number) where
150
- /// the given user is assigned as a reviewer for that PR.
151
- /// Only non-draft, non-rollup and open PRs are taken into account.
160
+ /// the given user is assigned as a reviewer for that PR
161
+ /// and the PR is considered to be "waiting for a review", according to the semantics
162
+ /// of the reviewer workqueue.
163
+ /// See the [`waits_for_a_review`] function.
152
164
pub async fn retrieve_pull_request_assignments (
153
165
owner : & str ,
154
166
repository : & str ,
@@ -170,26 +182,26 @@ pub async fn retrieve_pull_request_assignments(
170
182
. into_stream ( client) ;
171
183
let mut stream = std:: pin:: pin!( stream) ;
172
184
while let Some ( pr) = stream. try_next ( ) . await ? {
173
- if pr. draft == Some ( true ) {
174
- continue ;
175
- }
176
- // exclude rollup PRs
177
- if pr
185
+ let labels = pr
178
186
. labels
179
187
. unwrap_or_default ( )
180
- . iter ( )
181
- . any ( |label| label. name == "rollup" )
182
- {
183
- continue ;
184
- }
185
- for user in pr. assignees . unwrap_or_default ( ) {
186
- assignments. push ( (
187
- User {
188
- login : user. login ,
189
- id : ( * user. id ) . into ( ) ,
190
- } ,
191
- pr. number ,
192
- ) ) ;
188
+ . into_iter ( )
189
+ . map ( |l| Label { name : l. name } )
190
+ . collect :: < Vec < Label > > ( ) ;
191
+ if waits_for_a_review (
192
+ & labels,
193
+ pr. state == Some ( IssueState :: Open ) ,
194
+ pr. draft . unwrap_or_default ( ) ,
195
+ ) {
196
+ for user in pr. assignees . unwrap_or_default ( ) {
197
+ assignments. push ( (
198
+ User {
199
+ login : user. login ,
200
+ id : ( * user. id ) . into ( ) ,
201
+ } ,
202
+ pr. number ,
203
+ ) ) ;
204
+ }
193
205
}
194
206
}
195
207
assignments. sort_by ( |a, b| a. 0 . id . cmp ( & b. 0 . id ) ) ;
@@ -210,30 +222,57 @@ pub async fn get_assigned_prs(ctx: &Context, user_id: UserId) -> HashSet<PullReq
210
222
211
223
/// Add a PR to the workqueue of a team member.
212
224
/// Ensures no accidental PR duplicates.
213
- async fn upsert_pr_into_workqueue ( ctx : & Context , user_id : UserId , pr : PullRequestNumber ) {
214
- ctx . workqueue
215
- . write ( )
216
- . await
217
- . reviewers
218
- . entry ( user_id )
219
- . or_default ( )
220
- . insert ( pr) ;
225
+ ///
226
+ /// Returns true if the PR was actually inserted.
227
+ fn upsert_pr_into_user_queue (
228
+ workqueue : & mut RwLockWriteGuard < ReviewerWorkqueue > ,
229
+ user_id : UserId ,
230
+ pr : PullRequestNumber ,
231
+ ) -> bool {
232
+ workqueue . reviewers . entry ( user_id ) . or_default ( ) . insert ( pr)
221
233
}
222
234
223
- /// Delete a PR from the workqueue of a team member
224
- async fn delete_pr_from_workqueue ( ctx : & Context , user_id : UserId , pr : PullRequestNumber ) {
225
- let mut queue = ctx. workqueue . write ( ) . await ;
226
- if let Some ( reviewer) = queue. reviewers . get_mut ( & user_id) {
227
- reviewer. remove ( & pr) ;
235
+ /// Delete a PR from the workqueue of a team member.
236
+ fn delete_pr_from_user_queue (
237
+ workqueue : & mut ReviewerWorkqueue ,
238
+ user_id : UserId ,
239
+ pr : PullRequestNumber ,
240
+ ) {
241
+ if let Some ( queue) = workqueue. reviewers . get_mut ( & user_id) {
242
+ queue. remove ( & pr) ;
228
243
}
229
244
}
230
245
246
+ /// Delete a PR from the workqueue completely.
247
+ fn delete_pr_from_all_queues ( workqueue : & mut ReviewerWorkqueue , pr : PullRequestNumber ) {
248
+ for queue in workqueue. reviewers . values_mut ( ) {
249
+ queue. retain ( |pr_number| * pr_number != pr) ;
250
+ }
251
+ }
252
+
253
+ /// Returns true if the workqueue should assume that this PR is actually waiting for a reviewer.
254
+ /// The function receives atomic attributes so that it is compatible both with triagebot's
255
+ /// `Issue` struct (used for incremental updates) and octocrab's `PullRequest` struct (used for
256
+ /// batch PR loads).
257
+ ///
258
+ /// Note: this functionality is currently hardcoded for rust-lang/rust, other repos might use
259
+ /// different labels.
260
+ fn waits_for_a_review ( labels : & [ Label ] , is_open : bool , is_draft : bool ) -> bool {
261
+ let is_blocked = labels
262
+ . iter ( )
263
+ . any ( |l| l. name == "S-blocked" || l. name == "S-inactive" ) ;
264
+ let is_rollup = labels. iter ( ) . any ( |l| l. name == "rollup" ) ;
265
+ let is_waiting_for_reviewer = labels. iter ( ) . any ( |l| l. name == "S-waiting-on-review" ) ;
266
+
267
+ is_open && !is_draft && !is_blocked && !is_rollup && is_waiting_for_reviewer
268
+ }
269
+
231
270
#[ cfg( test) ]
232
271
mod tests {
233
272
use crate :: config:: Config ;
234
- use crate :: github:: PullRequestNumber ;
235
273
use crate :: github:: { Issue , IssuesAction , IssuesEvent , Repository , User } ;
236
- use crate :: handlers:: pr_tracking:: { handle_input, parse_input, upsert_pr_into_workqueue} ;
274
+ use crate :: github:: { Label , PullRequestNumber } ;
275
+ use crate :: handlers:: pr_tracking:: { handle_input, parse_input, upsert_pr_into_user_queue} ;
237
276
use crate :: tests:: github:: { default_test_user, issue, pull_request, user} ;
238
277
use crate :: tests:: { run_test, TestContext } ;
239
278
@@ -247,7 +286,10 @@ mod tests {
247
286
IssuesAction :: Assigned {
248
287
assignee : user. clone ( ) ,
249
288
} ,
250
- pull_request ( ) . number ( 10 ) . call ( ) ,
289
+ pull_request ( )
290
+ . number ( 10 )
291
+ . labels ( vec ! [ "S-waiting-on-review" ] )
292
+ . call ( ) ,
251
293
)
252
294
. await ;
253
295
@@ -258,6 +300,29 @@ mod tests {
258
300
. await ;
259
301
}
260
302
303
+ #[ tokio:: test]
304
+ async fn ignore_blocked_pr ( ) {
305
+ run_test ( |ctx| async move {
306
+ let user = user ( "Martin" , 2 ) ;
307
+
308
+ run_handler (
309
+ & ctx,
310
+ IssuesAction :: Assigned {
311
+ assignee : user. clone ( ) ,
312
+ } ,
313
+ pull_request ( )
314
+ . labels ( vec ! [ "S-waiting-on-review" , "S-blocked" ] )
315
+ . call ( ) ,
316
+ )
317
+ . await ;
318
+
319
+ check_assigned_prs ( & ctx, & user, & [ ] ) . await ;
320
+
321
+ Ok ( ctx)
322
+ } )
323
+ . await ;
324
+ }
325
+
261
326
#[ tokio:: test]
262
327
async fn remove_pr_from_workqueue_on_unassign ( ) {
263
328
run_test ( |ctx| async move {
@@ -269,7 +334,10 @@ mod tests {
269
334
IssuesAction :: Unassigned {
270
335
assignee : user. clone ( ) ,
271
336
} ,
272
- pull_request ( ) . number ( 10 ) . call ( ) ,
337
+ pull_request ( )
338
+ . number ( 10 )
339
+ . labels ( vec ! [ "S-waiting-on-review" ] )
340
+ . call ( ) ,
273
341
)
274
342
. await ;
275
343
@@ -280,6 +348,43 @@ mod tests {
280
348
. await ;
281
349
}
282
350
351
+ #[ tokio:: test]
352
+ async fn add_pr_to_workqueue_on_label ( ) {
353
+ run_test ( |ctx| async move {
354
+ let user = user ( "Martin" , 2 ) ;
355
+
356
+ run_handler (
357
+ & ctx,
358
+ IssuesAction :: Assigned {
359
+ assignee : user. clone ( ) ,
360
+ } ,
361
+ pull_request ( ) . number ( 10 ) . call ( ) ,
362
+ )
363
+ . await ;
364
+ check_assigned_prs ( & ctx, & user, & [ ] ) . await ;
365
+
366
+ run_handler (
367
+ & ctx,
368
+ IssuesAction :: Labeled {
369
+ label : Label {
370
+ name : "S-waiting-on-review" . to_string ( ) ,
371
+ } ,
372
+ } ,
373
+ pull_request ( )
374
+ . number ( 10 )
375
+ . labels ( vec ! [ "S-waiting-on-review" ] )
376
+ . assignees ( vec ! [ user. clone( ) ] )
377
+ . call ( ) ,
378
+ )
379
+ . await ;
380
+
381
+ check_assigned_prs ( & ctx, & user, & [ 10 ] ) . await ;
382
+
383
+ Ok ( ctx)
384
+ } )
385
+ . await ;
386
+ }
387
+
283
388
#[ tokio:: test]
284
389
async fn remove_pr_from_workqueue_on_pr_closed ( ) {
285
390
run_test ( |ctx| async move {
@@ -314,6 +419,7 @@ mod tests {
314
419
IssuesAction :: Reopened ,
315
420
pull_request ( )
316
421
. number ( 10 )
422
+ . labels ( vec ! [ "S-waiting-on-review" ] )
317
423
. assignees ( vec ! [ user. clone( ) ] )
318
424
. call ( ) ,
319
425
)
@@ -369,8 +475,11 @@ mod tests {
369
475
}
370
476
371
477
async fn set_assigned_prs ( ctx : & TestContext , user : & User , prs : & [ PullRequestNumber ] ) {
372
- for & pr in prs {
373
- upsert_pr_into_workqueue ( ctx. handler_ctx ( ) , user. id , pr) . await ;
478
+ {
479
+ let mut workqueue = ctx. handler_ctx ( ) . workqueue . write ( ) . await ;
480
+ for & pr in prs {
481
+ upsert_pr_into_user_queue ( & mut workqueue, user. id , pr) ;
482
+ }
374
483
}
375
484
check_assigned_prs ( & ctx, user, prs) . await ;
376
485
}
0 commit comments