19
19
20
20
use crate :: {
21
21
config:: AssignConfig ,
22
- github:: { self , Event , Issue , IssuesAction , Selection } ,
22
+ github:: { self , Event , FileDiff , Issue , IssuesAction , Selection } ,
23
23
handlers:: { Context , GithubClient , IssuesEvent } ,
24
24
interactions:: EditIssueBody ,
25
25
} ;
@@ -80,13 +80,11 @@ struct AssignData {
80
80
}
81
81
82
82
/// Input for auto-assignment when a PR is created.
83
- pub ( super ) struct AssignInput {
84
- git_diff : String ,
85
- }
83
+ pub ( super ) struct AssignInput { }
86
84
87
85
/// Prepares the input when a new PR is opened.
88
86
pub ( super ) async fn parse_input (
89
- ctx : & Context ,
87
+ _ctx : & Context ,
90
88
event : & IssuesEvent ,
91
89
config : Option < & AssignConfig > ,
92
90
) -> Result < Option < AssignInput > , String > {
@@ -100,15 +98,7 @@ pub(super) async fn parse_input(
100
98
{
101
99
return Ok ( None ) ;
102
100
}
103
- let git_diff = match event. issue . diff ( & ctx. github ) . await {
104
- Ok ( None ) => return Ok ( None ) ,
105
- Err ( e) => {
106
- log:: error!( "failed to fetch diff: {:?}" , e) ;
107
- return Ok ( None ) ;
108
- }
109
- Ok ( Some ( diff) ) => diff,
110
- } ;
111
- Ok ( Some ( AssignInput { git_diff } ) )
101
+ Ok ( Some ( AssignInput { } ) )
112
102
}
113
103
114
104
/// Handles the work of setting an assignment for a new PR and posting a
@@ -117,11 +107,18 @@ pub(super) async fn handle_input(
117
107
ctx : & Context ,
118
108
config : & AssignConfig ,
119
109
event : & IssuesEvent ,
120
- input : AssignInput ,
110
+ _input : AssignInput ,
121
111
) -> anyhow:: Result < ( ) > {
112
+ let Some ( diff) = event. issue . diff ( & ctx. github ) . await ? else {
113
+ bail ! (
114
+ "expected issue {} to be a PR, but the diff could not be determined" ,
115
+ event. issue. number
116
+ )
117
+ } ;
118
+
122
119
// Don't auto-assign or welcome if the user manually set the assignee when opening.
123
120
if event. issue . assignees . is_empty ( ) {
124
- let ( assignee, from_comment) = determine_assignee ( ctx, event, config, & input ) . await ?;
121
+ let ( assignee, from_comment) = determine_assignee ( ctx, event, config, & diff ) . await ?;
125
122
if assignee. as_deref ( ) == Some ( "ghost" ) {
126
123
// "ghost" is GitHub's placeholder account for deleted accounts.
127
124
// It is used here as a convenient way to prevent assignment. This
@@ -180,7 +177,7 @@ pub(super) async fn handle_input(
180
177
if config. warn_non_default_branch {
181
178
warnings. extend ( non_default_branch ( event) ) ;
182
179
}
183
- warnings. extend ( modifies_submodule ( & input . git_diff ) ) ;
180
+ warnings. extend ( modifies_submodule ( diff ) ) ;
184
181
if !warnings. is_empty ( ) {
185
182
let warnings: Vec < _ > = warnings
186
183
. iter ( )
@@ -222,9 +219,9 @@ fn non_default_branch(event: &IssuesEvent) -> Option<String> {
222
219
}
223
220
224
221
/// Returns a message if the PR modifies a git submodule.
225
- fn modifies_submodule ( diff : & str ) -> Option < String > {
222
+ fn modifies_submodule ( diff : & [ FileDiff ] ) -> Option < String > {
226
223
let re = regex:: Regex :: new ( r"\+Subproject\scommit\s" ) . unwrap ( ) ;
227
- if re. is_match ( diff) {
224
+ if diff . iter ( ) . any ( |fd| re. is_match ( & fd . diff ) ) {
228
225
Some ( SUBMODULE_WARNING_MSG . to_string ( ) )
229
226
} else {
230
227
None
@@ -278,7 +275,7 @@ async fn determine_assignee(
278
275
ctx : & Context ,
279
276
event : & IssuesEvent ,
280
277
config : & AssignConfig ,
281
- input : & AssignInput ,
278
+ diff : & [ FileDiff ] ,
282
279
) -> anyhow:: Result < ( Option < String > , bool ) > {
283
280
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
284
281
if let Some ( name) = find_assign_command ( ctx, event) {
@@ -298,7 +295,7 @@ async fn determine_assignee(
298
295
}
299
296
}
300
297
// Errors fall-through to try fallback group.
301
- match find_reviewers_from_diff ( config, & input . git_diff ) {
298
+ match find_reviewers_from_diff ( config, diff ) {
302
299
Ok ( candidates) if !candidates. is_empty ( ) => {
303
300
match find_reviewer_from_names ( & teams, config, & event. issue , & candidates) {
304
301
Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
@@ -346,60 +343,61 @@ async fn determine_assignee(
346
343
/// May return an error if the owners map is misconfigured.
347
344
///
348
345
/// Beware this may return an empty list if nothing matches.
349
- fn find_reviewers_from_diff ( config : & AssignConfig , diff : & str ) -> anyhow:: Result < Vec < String > > {
346
+ fn find_reviewers_from_diff (
347
+ config : & AssignConfig ,
348
+ diff : & [ FileDiff ] ,
349
+ ) -> anyhow:: Result < Vec < String > > {
350
350
// Map of `owners` path to the number of changes found in that path.
351
351
// This weights the reviewer choice towards places where the most edits are done.
352
352
let mut counts: HashMap < & str , u32 > = HashMap :: new ( ) ;
353
- // List of the longest `owners` patterns that match the current path. This
354
- // prefers choosing reviewers from deeply nested paths over those defined
355
- // for top-level paths, under the assumption that they are more
356
- // specialized.
357
- //
358
- // This is a list to handle the situation if multiple paths of the same
359
- // length match.
360
- let mut longest_owner_patterns = Vec :: new ( ) ;
361
- // Iterate over the diff, finding the start of each file. After each file
362
- // is found, it counts the number of modified lines in that file, and
363
- // tracks those in the `counts` map.
364
- for line in diff. split ( '\n' ) {
365
- if line. starts_with ( "diff --git " ) {
366
- // Start of a new file.
367
- longest_owner_patterns. clear ( ) ;
368
- let path = line[ line. find ( " b/" ) . unwrap ( ) ..]
369
- . strip_prefix ( " b/" )
370
- . unwrap ( ) ;
371
- // Find the longest `owners` entries that match this path.
372
- let mut longest = HashMap :: new ( ) ;
373
- for owner_pattern in config. owners . keys ( ) {
374
- let ignore = ignore:: gitignore:: GitignoreBuilder :: new ( "/" )
375
- . add_line ( None , owner_pattern)
376
- . with_context ( || format ! ( "owner file pattern `{owner_pattern}` is not valid" ) ) ?
377
- . build ( ) ?;
378
- if ignore. matched_path_or_any_parents ( path, false ) . is_ignore ( ) {
379
- let owner_len = owner_pattern. split ( '/' ) . count ( ) ;
380
- longest. insert ( owner_pattern, owner_len) ;
381
- }
382
- }
383
- let max_count = longest. values ( ) . copied ( ) . max ( ) . unwrap_or ( 0 ) ;
384
- longest_owner_patterns. extend (
385
- longest
386
- . iter ( )
387
- . filter ( |( _, count) | * * count == max_count)
388
- . map ( |x| * x. 0 ) ,
389
- ) ;
390
- // Give some weight to these patterns to start. This helps with
391
- // files modified without any lines changed.
392
- for owner_pattern in & longest_owner_patterns {
393
- * counts. entry ( owner_pattern) . or_default ( ) += 1 ;
353
+ // Iterate over the diff, counting the number of modified lines in each
354
+ // file, and tracks those in the `counts` map.
355
+ for file_diff in diff {
356
+ // List of the longest `owners` patterns that match the current path. This
357
+ // prefers choosing reviewers from deeply nested paths over those defined
358
+ // for top-level paths, under the assumption that they are more
359
+ // specialized.
360
+ //
361
+ // This is a list to handle the situation if multiple paths of the same
362
+ // length match.
363
+ let mut longest_owner_patterns = Vec :: new ( ) ;
364
+
365
+ // Find the longest `owners` entries that match this path.
366
+ let mut longest = HashMap :: new ( ) ;
367
+ for owner_pattern in config. owners . keys ( ) {
368
+ let ignore = ignore:: gitignore:: GitignoreBuilder :: new ( "/" )
369
+ . add_line ( None , owner_pattern)
370
+ . with_context ( || format ! ( "owner file pattern `{owner_pattern}` is not valid" ) ) ?
371
+ . build ( ) ?;
372
+ if ignore
373
+ . matched_path_or_any_parents ( & file_diff. path , false )
374
+ . is_ignore ( )
375
+ {
376
+ let owner_len = owner_pattern. split ( '/' ) . count ( ) ;
377
+ longest. insert ( owner_pattern, owner_len) ;
394
378
}
395
- continue ;
396
379
}
397
- // Check for a modified line.
398
- if ( !line. starts_with ( "+++" ) && line. starts_with ( '+' ) )
399
- || ( !line. starts_with ( "---" ) && line. starts_with ( '-' ) )
400
- {
401
- for owner_path in & longest_owner_patterns {
402
- * counts. entry ( owner_path) . or_default ( ) += 1 ;
380
+ let max_count = longest. values ( ) . copied ( ) . max ( ) . unwrap_or ( 0 ) ;
381
+ longest_owner_patterns. extend (
382
+ longest
383
+ . iter ( )
384
+ . filter ( |( _, count) | * * count == max_count)
385
+ . map ( |x| * x. 0 ) ,
386
+ ) ;
387
+ // Give some weight to these patterns to start. This helps with
388
+ // files modified without any lines changed.
389
+ for owner_pattern in & longest_owner_patterns {
390
+ * counts. entry ( owner_pattern) . or_default ( ) += 1 ;
391
+ }
392
+
393
+ // Count the modified lines.
394
+ for line in file_diff. diff . lines ( ) {
395
+ if ( !line. starts_with ( "+++" ) && line. starts_with ( '+' ) )
396
+ || ( !line. starts_with ( "---" ) && line. starts_with ( '-' ) )
397
+ {
398
+ for owner_path in & longest_owner_patterns {
399
+ * counts. entry ( owner_path) . or_default ( ) += 1 ;
400
+ }
403
401
}
404
402
}
405
403
}
0 commit comments