@@ -8,6 +8,7 @@ use std::{
88 sync:: { Arc , Mutex , MutexGuard } ,
99} ;
1010
11+ use gix_imara_diff:: Diff ;
1112use log:: Level ;
1213
1314// project-specific crates/modules
@@ -145,41 +146,20 @@ pub fn run_clang_format(
145146 if let Some ( patched_path) = & file. patched_path
146147 && patched_path. exists ( )
147148 {
148- let tidy_patch_contents =
149- fs:: read_to_string ( patched_path) . map_err ( |e| ClangCaptureError :: ReadFileFailed {
150- file_name : patched_path. to_string_lossy ( ) . to_string ( ) ,
151- source : e,
152- } ) ?;
153- let ( tidy_diff, _) = make_patch ( & tidy_patch_contents, & original_contents) ;
154149 let mut cmd = Command :: new ( cmd_path) ;
155150 cmd. current_dir ( & cache_path) ;
156151 // edit the clang-tody patched file in-place (`-i`)
157152 cmd. args ( [ "--style" , & clang_params. style , "-i" ] ) ;
158153 // if ranges is empty, then we're just formatting the entire file.
159154 if !ranges. is_empty ( ) {
160- // We're concerned about formatting what clang-tidy changed (tidy_diff.hunks().before),
161- // but we also want to include any clang-format changes that do not overlap clang-tidy fixes.
162- let mut joint_ranges = tidy_diff
163- . hunks ( )
164- // hunk is partially inclusive: [start, end),
165- // but clang-format expects fully inclusive line ranges.
166- // subtract 1 from hunk.before.end
167- . map ( |hunk| {
168- RangeInclusive :: new ( hunk. before . start , hunk. before . end . saturating_sub ( 1 ) )
169- } )
170- . collect :: < Vec < _ > > ( ) ;
171- for range in & ranges {
172- let mut contained = false ;
173- for hunk in tidy_diff. hunks ( ) {
174- if hunk. before . contains ( range. start ( ) ) && hunk. before . contains ( range. end ( ) ) {
175- contained = true ;
176- break ;
177- }
178- }
179- if !contained {
180- joint_ranges. push ( range. clone ( ) ) ;
155+ let tidy_patch_contents = fs:: read_to_string ( patched_path) . map_err ( |e| {
156+ ClangCaptureError :: ReadFileFailed {
157+ file_name : patched_path. to_string_lossy ( ) . to_string ( ) ,
158+ source : e,
181159 }
182- }
160+ } ) ?;
161+ let ( tidy_diff, _) = make_patch ( & tidy_patch_contents, & original_contents) ;
162+ let joint_ranges = three_way_diff ( & ranges, tidy_diff) ;
183163 for range in & joint_ranges {
184164 cmd. arg ( format ! ( "--lines={}:{}" , range. start( ) , range. end( ) ) . as_str ( ) ) ;
185165 }
@@ -223,11 +203,119 @@ pub fn run_clang_format(
223203 Ok ( logs)
224204}
225205
206+ /// Essentially does a three way diff without the original source that generated the given `ranges` (simplified hunks).
207+ ///
208+ /// The returned list of ranges are lines that need formatting in the clang-tidy patched file,
209+ /// provided by the `tidy_diff`. The given `ranges` are the line numbers in the original file
210+ /// that clang-tidy patched.
211+ fn three_way_diff ( ranges : & [ RangeInclusive < u32 > ] , tidy_diff : Diff ) -> Vec < RangeInclusive < u32 > > {
212+ // We're concerned about the formatting cases:
213+ //
214+ // 1. changes that clang-tidy made: `tidy_diff.hunks().after`
215+ // 2. changes in the current CI event's diff (`ranges`)
216+ // that clang-tidy did not touch (`tidy_diff.hunks().before`)
217+ // 3. changes that do not overlap clang-tidy fixes: `ranges` - `tidy_diff.hunks().before`
218+ // 4. changes that overlap with clang-tidy fixes. This one is complex because
219+ // - tidy fixes can prefix an og range
220+ // - tidy fixes can suffix an og range
221+ // - tidy fixes can be contained within an og range
222+ // - multiple tidy fixes can (in order) suffix, be contained within, and prefix an og range
223+ let mut joint_ranges = vec ! [ ] ;
224+ let mut tidy_iter = tidy_diff. hunks ( ) . peekable ( ) ;
225+ let mut line_shift = 0i32 ;
226+
227+ /// Prevent pure removals from causing invalid inclusive ranges.
228+ fn maybe_push_range ( joint_ranges : & mut Vec < RangeInclusive < u32 > > , start : u32 , end : u32 ) {
229+ if start <= end {
230+ joint_ranges. push ( RangeInclusive :: new ( start, end) ) ;
231+ }
232+ }
233+
234+ for og_range in ranges {
235+ let og_start = * og_range. start ( ) ;
236+ let og_end = * og_range. end ( ) ;
237+
238+ // track the start and end of a merged range that gets pushed into joint_ranges.
239+ let mut merged_start = ( og_start as i32 + line_shift) as u32 ;
240+ let mut merged_end = ( og_end as i32 + line_shift) as u32 ;
241+
242+ while let Some ( tidy_hunk) = tidy_iter. peek ( ) {
243+ // alias for readability and prevent some repeated calculations
244+ let before_start = tidy_hunk. before . start ;
245+ let before_end = tidy_hunk. before . end . saturating_sub ( 1 ) ;
246+ let after_start = tidy_hunk. after . start ;
247+ let after_end = tidy_hunk. after . end . saturating_sub ( 1 ) ;
248+ let delta = tidy_hunk. after . len ( ) as i32 - tidy_hunk. before . len ( ) as i32 ;
249+
250+ // The tidy hunk is a pure removal that exactly matches the og range.
251+ if tidy_hunk. is_pure_removal ( ) && before_start == og_start && before_end == og_end {
252+ // Skip the og range and tidy hunk entirely.
253+ // The line shift must still be adjusted for the pure removal though
254+ line_shift += delta;
255+ merged_end = 0 ; // causes invalid inclusive range which does not get pushed.
256+ tidy_iter. next ( ) ; // skip this tidy hunk
257+ break ; // skip og range and iterate to the next one.
258+ }
259+
260+ // tidy hunk is before the og range.
261+ if before_end < og_start {
262+ maybe_push_range ( & mut joint_ranges, after_start, after_end) ;
263+ line_shift += delta;
264+ tidy_iter. next ( ) ;
265+ continue ;
266+ }
267+
268+ // tidy hunk is after the og range.
269+ if before_start > og_end {
270+ // handle the og range before iterating the next tidy hunk
271+ break ;
272+ }
273+
274+ // tidy hunk overlaps with the og range in some way (case 4).
275+ if tidy_hunk. before . contains ( & og_start) {
276+ merged_start = after_start;
277+ }
278+
279+ // commit the line shift now that the tidy hunk start is checked.
280+ line_shift += delta;
281+
282+ // tidy hunk suffixes the og range.
283+ if tidy_hunk. before . contains ( & og_end) {
284+ merged_end = after_end;
285+ tidy_iter. next ( ) ; // this tidy hunk is handled.
286+ break ; // break from loop to push the merged range into joint_ranges.
287+ }
288+
289+ // tidy hunk is contained within the og range.
290+ // so adjust the og range end accordingly and continue iterating tidy hunks
291+ merged_end = ( og_end as i32 + line_shift) as u32 ;
292+ tidy_iter. next ( ) ;
293+ }
294+
295+ maybe_push_range ( & mut joint_ranges, merged_start, merged_end) ;
296+ }
297+
298+ // handle any remaining tidy hunks that are after all og ranges.
299+ for tidy_hunk in tidy_iter {
300+ maybe_push_range (
301+ & mut joint_ranges,
302+ tidy_hunk. after . start ,
303+ tidy_hunk. after . end . saturating_sub ( 1 ) ,
304+ ) ;
305+ }
306+
307+ joint_ranges
308+ }
309+
226310#[ cfg( test) ]
227311mod tests {
228312 #![ allow( clippy:: unwrap_used) ]
229313
230- use super :: summarize_style;
314+ use std:: ops:: RangeInclusive ;
315+
316+ use gix_imara_diff:: { Diff , InternedInput } ;
317+
318+ use super :: { summarize_style, three_way_diff} ;
231319
232320 fn formalize_style ( style : & str , expected : & str ) {
233321 assert_eq ! ( summarize_style( style) , expected) ;
@@ -247,4 +335,42 @@ mod tests {
247335 fn formalize_custom_style ( ) {
248336 formalize_style ( "file" , "Custom" ) ;
249337 }
338+
339+ #[ test]
340+ fn three_way_diff_mixed ( ) {
341+ const OG_SRC : & str =
342+ "line1\n line2\n line3\n line4\n line5\n line6\n line7\n line8\n line9\n line10\n line11" ;
343+ // TIDY_SRC replaces line3->StringA (hunk before=2..3) and
344+ // line8+line9+line10->StringB+StringC (hunk before=7..10), then appends StringE.
345+ // The second hunk's before=7..10 contains og_end=9 but not og_start=6,
346+ // which exercises the "tidy hunk suffixes og range" branch.
347+ const TIDY_SRC : & str =
348+ "line1\n line2\n StringA\n line4\n line5\n line6\n line7\n StringB\n StringC\n line11\n StringE" ;
349+ let input = InternedInput :: new ( OG_SRC , TIDY_SRC ) ;
350+ let mut tidy_diff = Diff :: compute ( gix_imara_diff:: Algorithm :: Histogram , & input) ;
351+ tidy_diff. postprocess_lines ( & input) ;
352+ let ranges = vec ! [ RangeInclusive :: new( 2 , 4 ) , RangeInclusive :: new( 6 , 9 ) ] ;
353+ println ! ( "tidy diff: {tidy_diff:#?}\n compared to og ranges: {ranges:?}" ) ;
354+ let joint_ranges = three_way_diff ( & ranges, tidy_diff) ;
355+ println ! ( "joint ranges: {joint_ranges:#?}" ) ;
356+ assert_eq ! ( joint_ranges, vec![ 2 ..=4 , 6 ..=10 ] ) ;
357+ }
358+
359+ #[ test]
360+ fn three_way_diff_separated ( ) {
361+ const OG_SRC : & str =
362+ "line1\n line2\n line3\n line4\n line5\n line6\n line7\n line8\n line9\n line10\n line11" ;
363+ // TIDY_SRC removes "line3" (index 2) which decrements offsets in ranges[5,8] and removes ranges[2,2].
364+ // TIDY_SRC appends StringE, which handles remaining tidy hunks after done iterating ranges
365+ const TIDY_SRC : & str =
366+ "line1\n line2\n line4\n line5\n line6\n line7\n line8\n line9\n line10\n line11\n StringE" ;
367+ let input = InternedInput :: new ( OG_SRC , TIDY_SRC ) ;
368+ let mut tidy_diff = Diff :: compute ( gix_imara_diff:: Algorithm :: Histogram , & input) ;
369+ tidy_diff. postprocess_lines ( & input) ;
370+ let ranges = vec ! [ 2 ..=2 , 5 ..=8 ] ;
371+ println ! ( "tidy diff: {tidy_diff:#?}\n compared to og ranges: {ranges:?}" ) ;
372+ let joint_ranges = three_way_diff ( & ranges, tidy_diff) ;
373+ println ! ( "joint ranges: {joint_ranges:#?}" ) ;
374+ assert_eq ! ( joint_ranges, vec![ 4 ..=7 , 9 ..=10 ] ) ;
375+ }
250376}
0 commit comments