@@ -69,7 +69,7 @@ pub enum Hunk {
69
69
path : PathBuf ,
70
70
move_path : Option < PathBuf > ,
71
71
72
- /// Chunks should be in order, i.e. the `change_context` of one chunk
72
+ /// Chunks should be in order, i.e. the first context line of one chunk
73
73
/// should occur later in the file than the previous chunk.
74
74
chunks : Vec < UpdateFileChunk > ,
75
75
} ,
@@ -89,12 +89,13 @@ use Hunk::*;
89
89
90
90
#[ derive( Debug , PartialEq , Clone ) ]
91
91
pub struct UpdateFileChunk {
92
- /// A single line of context used to narrow down the position of the chunk
93
- /// (this is usually a class, method, or function definition.)
94
- pub change_context : Option < String > ,
92
+ /// Context lines used to narrow down the position of the chunk.
93
+ /// Each entry is searched sequentially to progressively restrict the
94
+ /// search to the desired region (e.g. class → method).
95
+ pub context_lines : Vec < String > ,
95
96
96
97
/// A contiguous block of lines that should be replaced with `new_lines`.
97
- /// `old_lines` must occur strictly after `change_context` .
98
+ /// `old_lines` must occur strictly after the context .
98
99
pub old_lines : Vec < String > ,
99
100
pub new_lines : Vec < String > ,
100
101
@@ -344,32 +345,38 @@ fn parse_update_file_chunk(
344
345
line_number,
345
346
} ) ;
346
347
}
347
- // If we see an explicit context marker @@ or @@ <context>, consume it; otherwise, optionally
348
- // allow treating the chunk as starting directly with diff lines.
349
- let ( change_context, start_index) = if lines[ 0 ] == EMPTY_CHANGE_CONTEXT_MARKER {
350
- ( None , 1 )
351
- } else if let Some ( context) = lines[ 0 ] . strip_prefix ( CHANGE_CONTEXT_MARKER ) {
352
- ( Some ( context. to_string ( ) ) , 1 )
353
- } else {
354
- if !allow_missing_context {
355
- return Err ( InvalidHunkError {
356
- message : format ! (
357
- "Expected update hunk to start with a @@ context marker, got: '{}'" ,
358
- lines[ 0 ]
359
- ) ,
360
- line_number,
361
- } ) ;
348
+ let mut context_lines = Vec :: new ( ) ;
349
+ let mut start_index = 0 ;
350
+ let mut saw_context_marker = false ;
351
+ while start_index < lines. len ( ) {
352
+ if lines[ start_index] == EMPTY_CHANGE_CONTEXT_MARKER {
353
+ saw_context_marker = true ;
354
+ start_index += 1 ;
355
+ } else if let Some ( context) = lines[ start_index] . strip_prefix ( CHANGE_CONTEXT_MARKER ) {
356
+ saw_context_marker = true ;
357
+ context_lines. push ( context. to_string ( ) ) ;
358
+ start_index += 1 ;
359
+ } else {
360
+ break ;
362
361
}
363
- ( None , 0 )
364
- } ;
362
+ }
363
+ if !saw_context_marker && !allow_missing_context {
364
+ return Err ( InvalidHunkError {
365
+ message : format ! (
366
+ "Expected update hunk to start with a @@ context marker, got: '{}'" ,
367
+ lines[ 0 ]
368
+ ) ,
369
+ line_number,
370
+ } ) ;
371
+ }
365
372
if start_index >= lines. len ( ) {
366
373
return Err ( InvalidHunkError {
367
374
message : "Update hunk does not contain any lines" . to_string ( ) ,
368
- line_number : line_number + 1 ,
375
+ line_number : line_number + start_index ,
369
376
} ) ;
370
377
}
371
378
let mut chunk = UpdateFileChunk {
372
- change_context ,
379
+ context_lines ,
373
380
old_lines : Vec :: new ( ) ,
374
381
new_lines : Vec :: new ( ) ,
375
382
is_end_of_file : false ,
@@ -381,7 +388,7 @@ fn parse_update_file_chunk(
381
388
if parsed_lines == 0 {
382
389
return Err ( InvalidHunkError {
383
390
message : "Update hunk does not contain any lines" . to_string ( ) ,
384
- line_number : line_number + 1 ,
391
+ line_number : line_number + start_index ,
385
392
} ) ;
386
393
}
387
394
chunk. is_end_of_file = true ;
@@ -411,7 +418,7 @@ fn parse_update_file_chunk(
411
418
message : format ! (
412
419
"Unexpected line found in update hunk: '{line_contents}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)"
413
420
) ,
414
- line_number : line_number + 1 ,
421
+ line_number : line_number + start_index ,
415
422
} ) ;
416
423
}
417
424
// Assume this is the start of the next hunk.
@@ -491,7 +498,7 @@ fn test_parse_patch() {
491
498
path: PathBuf :: from( "path/update.py" ) ,
492
499
move_path: Some ( PathBuf :: from( "path/update2.py" ) ) ,
493
500
chunks: vec![ UpdateFileChunk {
494
- change_context : Some ( "def f():" . to_string( ) ) ,
501
+ context_lines : vec! [ "def f():" . to_string( ) ] ,
495
502
old_lines: vec![ " pass" . to_string( ) ] ,
496
503
new_lines: vec![ " return 123" . to_string( ) ] ,
497
504
is_end_of_file: false
@@ -518,7 +525,7 @@ fn test_parse_patch() {
518
525
path: PathBuf :: from( "file.py" ) ,
519
526
move_path: None ,
520
527
chunks: vec![ UpdateFileChunk {
521
- change_context : None ,
528
+ context_lines : Vec :: new ( ) ,
522
529
old_lines: vec![ ] ,
523
530
new_lines: vec![ "line" . to_string( ) ] ,
524
531
is_end_of_file: false
@@ -548,7 +555,7 @@ fn test_parse_patch() {
548
555
path: PathBuf :: from( "file2.py" ) ,
549
556
move_path: None ,
550
557
chunks: vec![ UpdateFileChunk {
551
- change_context : None ,
558
+ context_lines : Vec :: new ( ) ,
552
559
old_lines: vec![ "import foo" . to_string( ) ] ,
553
560
new_lines: vec![ "import foo" . to_string( ) , "bar" . to_string( ) ] ,
554
561
is_end_of_file: false ,
@@ -568,7 +575,7 @@ fn test_parse_patch_lenient() {
568
575
path: PathBuf :: from( "file2.py" ) ,
569
576
move_path: None ,
570
577
chunks: vec![ UpdateFileChunk {
571
- change_context : None ,
578
+ context_lines : Vec :: new ( ) ,
572
579
old_lines: vec![ "import foo" . to_string( ) ] ,
573
580
new_lines: vec![ "import foo" . to_string( ) , "bar" . to_string( ) ] ,
574
581
is_end_of_file: false ,
@@ -701,7 +708,7 @@ fn test_update_file_chunk() {
701
708
) ,
702
709
Ok ( (
703
710
( UpdateFileChunk {
704
- change_context : Some ( "change_context" . to_string( ) ) ,
711
+ context_lines : vec! [ "change_context" . to_string( ) ] ,
705
712
old_lines: vec![
706
713
"" . to_string( ) ,
707
714
"context" . to_string( ) ,
@@ -723,12 +730,37 @@ fn test_update_file_chunk() {
723
730
parse_update_file_chunk( & [ "@@" , "+line" , "*** End of File" ] , 123 , false ) ,
724
731
Ok ( (
725
732
( UpdateFileChunk {
726
- change_context : None ,
733
+ context_lines : Vec :: new ( ) ,
727
734
old_lines: vec![ ] ,
728
735
new_lines: vec![ "line" . to_string( ) ] ,
729
736
is_end_of_file: true
730
737
} ) ,
731
738
3
732
739
) )
733
740
) ;
741
+ assert_eq ! (
742
+ parse_update_file_chunk(
743
+ & [
744
+ "@@ class BaseClass" ,
745
+ "@@ def method()" ,
746
+ " context" ,
747
+ "-old" ,
748
+ "+new" ,
749
+ ] ,
750
+ 123 ,
751
+ false
752
+ ) ,
753
+ Ok ( (
754
+ ( UpdateFileChunk {
755
+ context_lines: vec![
756
+ "class BaseClass" . to_string( ) ,
757
+ " def method()" . to_string( )
758
+ ] ,
759
+ old_lines: vec![ "context" . to_string( ) , "old" . to_string( ) ] ,
760
+ new_lines: vec![ "context" . to_string( ) , "new" . to_string( ) ] ,
761
+ is_end_of_file: false
762
+ } ) ,
763
+ 5
764
+ ) )
765
+ ) ;
734
766
}
0 commit comments