1919
2020use crate :: {
2121 config:: AssignConfig ,
22- github:: { self , Event , Issue , IssuesAction , Selection } ,
22+ github:: { self , Event , FileDiff , Issue , IssuesAction , Selection } ,
2323 handlers:: { Context , GithubClient , IssuesEvent } ,
2424 interactions:: EditIssueBody ,
2525} ;
@@ -80,13 +80,11 @@ struct AssignData {
8080}
8181
8282/// Input for auto-assignment when a PR is created.
83- pub ( super ) struct AssignInput {
84- git_diff : String ,
85- }
83+ pub ( super ) struct AssignInput { }
8684
8785/// Prepares the input when a new PR is opened.
8886pub ( super ) async fn parse_input (
89- ctx : & Context ,
87+ _ctx : & Context ,
9088 event : & IssuesEvent ,
9189 config : Option < & AssignConfig > ,
9290) -> Result < Option < AssignInput > , String > {
@@ -100,15 +98,7 @@ pub(super) async fn parse_input(
10098 {
10199 return Ok ( None ) ;
102100 }
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 { } ) )
112102}
113103
114104/// Handles the work of setting an assignment for a new PR and posting a
@@ -117,11 +107,18 @@ pub(super) async fn handle_input(
117107 ctx : & Context ,
118108 config : & AssignConfig ,
119109 event : & IssuesEvent ,
120- input : AssignInput ,
110+ _input : AssignInput ,
121111) -> 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+
122119 // Don't auto-assign or welcome if the user manually set the assignee when opening.
123120 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 ?;
125122 if assignee. as_deref ( ) == Some ( "ghost" ) {
126123 // "ghost" is GitHub's placeholder account for deleted accounts.
127124 // It is used here as a convenient way to prevent assignment. This
@@ -180,7 +177,7 @@ pub(super) async fn handle_input(
180177 if config. warn_non_default_branch {
181178 warnings. extend ( non_default_branch ( event) ) ;
182179 }
183- warnings. extend ( modifies_submodule ( & input . git_diff ) ) ;
180+ warnings. extend ( modifies_submodule ( diff ) ) ;
184181 if !warnings. is_empty ( ) {
185182 let warnings: Vec < _ > = warnings
186183 . iter ( )
@@ -222,9 +219,9 @@ fn non_default_branch(event: &IssuesEvent) -> Option<String> {
222219}
223220
224221/// 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 > {
226223 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 ) ) {
228225 Some ( SUBMODULE_WARNING_MSG . to_string ( ) )
229226 } else {
230227 None
@@ -278,7 +275,7 @@ async fn determine_assignee(
278275 ctx : & Context ,
279276 event : & IssuesEvent ,
280277 config : & AssignConfig ,
281- input : & AssignInput ,
278+ diff : & [ FileDiff ] ,
282279) -> anyhow:: Result < ( Option < String > , bool ) > {
283280 let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
284281 if let Some ( name) = find_assign_command ( ctx, event) {
@@ -298,7 +295,7 @@ async fn determine_assignee(
298295 }
299296 }
300297 // 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 ) {
302299 Ok ( candidates) if !candidates. is_empty ( ) => {
303300 match find_reviewer_from_names ( & teams, config, & event. issue , & candidates) {
304301 Ok ( assignee) => return Ok ( ( Some ( assignee) , false ) ) ,
@@ -346,60 +343,61 @@ async fn determine_assignee(
346343/// May return an error if the owners map is misconfigured.
347344///
348345/// 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 > > {
350350 // Map of `owners` path to the number of changes found in that path.
351351 // This weights the reviewer choice towards places where the most edits are done.
352352 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) ;
394378 }
395- continue ;
396379 }
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+ }
403401 }
404402 }
405403 }
0 commit comments