@@ -27,6 +27,8 @@ pub struct TurnDiffTracker {
2727 temp_name_to_current_external : HashMap < String , PathBuf > ,
2828 /// Internal filename -> baseline file contents (None means the file did not exist, i.e. /dev/null).
2929 baseline_contents : HashMap < String , Option < String > > ,
30+ /// Internal filename -> baseline file mode (100644 or 100755). Only set when baseline file existed.
31+ baseline_mode : HashMap < String , String > ,
3032 /// Aggregated unified diff for all accumulated changes across files.
3133 pub unified_diff : Option < String > ,
3234}
@@ -56,6 +58,10 @@ impl TurnDiffTracker {
5658 let baseline = if path. exists ( ) {
5759 let contents = fs:: read ( path)
5860 . with_context ( || format ! ( "failed to read original {}" , path. display( ) ) ) ?;
61+ // Capture baseline mode for later file mode lines.
62+ if let Some ( mode) = file_mode_for_path ( path) {
63+ self . baseline_mode . insert ( internal. clone ( ) , mode) ;
64+ }
5965 Some ( String :: from_utf8_lossy ( & contents) . into_owned ( ) )
6066 } else {
6167 None
@@ -172,6 +178,30 @@ impl TurnDiffTracker {
172178 // Emit a git-style header for better readability and parity with previous behavior.
173179 aggregated. push_str ( & format ! ( "diff --git a/{left_display} b/{right_display}\n " ) ) ;
174180
181+ // Emit file mode lines and index line similar to git.
182+ let is_add = left_content. is_none ( ) && right_content. is_some ( ) ;
183+ let is_delete = left_content. is_some ( ) && right_content. is_none ( ) ;
184+
185+ // Determine modes.
186+ let baseline_mode = self
187+ . baseline_mode
188+ . get ( & internal)
189+ . cloned ( )
190+ . unwrap_or_else ( || "100644" . to_string ( ) ) ;
191+ let current_mode =
192+ file_mode_for_path ( & current_external) . unwrap_or_else ( || "100644" . to_string ( ) ) ;
193+
194+ if is_add {
195+ aggregated. push_str ( & format ! ( "new file mode {current_mode}\n " ) ) ;
196+ } else if is_delete {
197+ aggregated. push_str ( & format ! ( "deleted file mode {baseline_mode}\n " ) ) ;
198+ } else if baseline_mode != current_mode {
199+ aggregated. push_str ( & format ! ( "old mode {baseline_mode}\n " ) ) ;
200+ aggregated. push_str ( & format ! ( "new mode {current_mode}\n " ) ) ;
201+ }
202+
203+ aggregated. push_str ( & format ! ( "index {ZERO_OID}..{ZERO_OID}\n " ) ) ;
204+
175205 let old_header = if left_content. is_some ( ) {
176206 format ! ( "a/{left_display}" )
177207 } else {
@@ -213,6 +243,28 @@ fn uuid_filename_for(path: &Path) -> String {
213243 }
214244}
215245
246+ const ZERO_OID : & str = "0000000000000000000000000000000000000000" ;
247+
248+ fn file_mode_for_path ( path : & Path ) -> Option < String > {
249+ #[ cfg( unix) ]
250+ {
251+ use std:: os:: unix:: fs:: PermissionsExt ;
252+ let meta = fs:: metadata ( path) . ok ( ) ?;
253+ let mode = meta. permissions ( ) . mode ( ) ;
254+ let is_exec = ( mode & 0o111 ) != 0 ;
255+ Some ( if is_exec {
256+ "100755" . to_string ( )
257+ } else {
258+ "100644" . to_string ( )
259+ } )
260+ }
261+ #[ cfg( not( unix) ) ]
262+ {
263+ // Default to non-executable on non-unix.
264+ Some ( "100644" . to_string ( ) )
265+ }
266+ }
267+
216268#[ cfg( test) ]
217269mod tests {
218270 #![ allow( clippy:: unwrap_used) ]
@@ -268,12 +320,12 @@ mod tests {
268320 acc. get_unified_diff ( ) . unwrap ( ) ;
269321 let first = acc. unified_diff . clone ( ) . unwrap ( ) ;
270322 let first = normalize_diff_for_test ( & first, dir. path ( ) ) ;
271- let expected_first = r#"diff --git a/<TMP>/a.txt b/<TMP>/a.txt
272- --- /dev/null
273- +++ b/<TMP>/a.txt
274- @@ -0,0 +1 @@
275- +foo
276- "# ;
323+ let expected_first = {
324+ let mode = file_mode_for_path ( & file ) . unwrap_or_else ( || "100644" . to_string ( ) ) ;
325+ format ! (
326+ "diff --git a/<TMP>/a.txt b/<TMP>/a.txt \n new file mode {mode} \n index {ZERO_OID}..{ZERO_OID} \n --- /dev/null \n +++ b/<TMP>/a.txt \n @@ -0,0 +1 @@\n +foo \n " ,
327+ )
328+ } ;
277329 assert_eq ! ( first, expected_first) ;
278330
279331 // Second patch: update the file on disk.
@@ -291,13 +343,12 @@ mod tests {
291343 acc. get_unified_diff ( ) . unwrap ( ) ;
292344 let combined = acc. unified_diff . clone ( ) . unwrap ( ) ;
293345 let combined = normalize_diff_for_test ( & combined, dir. path ( ) ) ;
294- let expected_combined = r#"diff --git a/<TMP>/a.txt b/<TMP>/a.txt
295- --- /dev/null
296- +++ b/<TMP>/a.txt
297- @@ -0,0 +1,2 @@
298- +foo
299- +bar
300- "# ;
346+ let expected_combined = {
347+ let mode = file_mode_for_path ( & file) . unwrap_or_else ( || "100644" . to_string ( ) ) ;
348+ format ! (
349+ "diff --git a/<TMP>/a.txt b/<TMP>/a.txt\n new file mode {mode}\n index {ZERO_OID}..{ZERO_OID}\n --- /dev/null\n +++ b/<TMP>/a.txt\n @@ -0,0 +1,2 @@\n +foo\n +bar\n " ,
350+ )
351+ } ;
301352 assert_eq ! ( combined, expected_combined) ;
302353 }
303354
@@ -312,16 +363,14 @@ mod tests {
312363 acc. on_patch_begin ( & del_changes) . unwrap ( ) ;
313364
314365 // Simulate apply: delete the file from disk.
366+ let baseline_mode = file_mode_for_path ( & file) . unwrap_or_else ( || "100644" . to_string ( ) ) ;
315367 fs:: remove_file ( & file) . unwrap ( ) ;
316368 acc. get_unified_diff ( ) . unwrap ( ) ;
317369 let diff = acc. unified_diff . clone ( ) . unwrap ( ) ;
318370 let diff = normalize_diff_for_test ( & diff, dir. path ( ) ) ;
319- let expected = r#"diff --git a/<TMP>/b.txt b/<TMP>/b.txt
320- --- a/<TMP>/b.txt
321- +++ /dev/null
322- @@ -1 +0,0 @@
323- -x
324- "# ;
371+ let expected = format ! (
372+ "diff --git a/<TMP>/b.txt b/<TMP>/b.txt\n deleted file mode {baseline_mode}\n index {ZERO_OID}..{ZERO_OID}\n --- a/<TMP>/b.txt\n +++ /dev/null\n @@ -1 +0,0 @@\n -x\n " ,
373+ ) ;
325374 assert_eq ! ( diff, expected) ;
326375 }
327376
@@ -349,13 +398,11 @@ mod tests {
349398 acc. get_unified_diff ( ) . unwrap ( ) ;
350399 let out = acc. unified_diff . clone ( ) . unwrap ( ) ;
351400 let out = normalize_diff_for_test ( & out, dir. path ( ) ) ;
352- let expected = r#"diff --git a/<TMP>/src.txt b/<TMP>/dst.txt
353- --- a/<TMP>/src.txt
354- +++ b/<TMP>/dst.txt
355- @@ -1 +1 @@
356- -line
357- +line2
358- "# ;
401+ let expected = {
402+ format ! (
403+ "diff --git a/<TMP>/src.txt b/<TMP>/dst.txt\n index {ZERO_OID}..{ZERO_OID}\n --- a/<TMP>/src.txt\n +++ b/<TMP>/dst.txt\n @@ -1 +1 @@\n -line\n +line2\n "
404+ )
405+ } ;
359406 assert_eq ! ( out, expected) ;
360407 }
361408
@@ -383,36 +430,29 @@ mod tests {
383430 acc. get_unified_diff ( ) . unwrap ( ) ;
384431 let first = acc. unified_diff . clone ( ) . unwrap ( ) ;
385432 let first = normalize_diff_for_test ( & first, dir. path ( ) ) ;
386- let expected_first = r#"diff --git a/<TMP>/a.txt b/<TMP>/a.txt
387- --- a/<TMP>/a.txt
388- +++ b/<TMP>/a.txt
389- @@ -1 +1,2 @@
390- foo
391- +bar
392- "# ;
433+ let expected_first = {
434+ format ! (
435+ "diff --git a/<TMP>/a.txt b/<TMP>/a.txt\n index {ZERO_OID}..{ZERO_OID}\n --- a/<TMP>/a.txt\n +++ b/<TMP>/a.txt\n @@ -1 +1,2 @@\n foo\n +bar\n "
436+ )
437+ } ;
393438 assert_eq ! ( first, expected_first) ;
394439
395440 // Next: introduce a brand-new path b.txt into baseline snapshots via a delete change.
396441 let del_b = HashMap :: from ( [ ( b. clone ( ) , FileChange :: Delete ) ] ) ;
397442 acc. on_patch_begin ( & del_b) . unwrap ( ) ;
398443 // Simulate apply: delete b.txt.
444+ let baseline_mode = file_mode_for_path ( & b) . unwrap_or_else ( || "100644" . to_string ( ) ) ;
399445 fs:: remove_file ( & b) . unwrap ( ) ;
400446 acc. get_unified_diff ( ) . unwrap ( ) ;
401447
402448 let combined = acc. unified_diff . clone ( ) . unwrap ( ) ;
403449 let combined = normalize_diff_for_test ( & combined, dir. path ( ) ) ;
404- let expected = r#"diff --git a/<TMP>/a.txt b/<TMP>/a.txt
405- --- a/<TMP>/a.txt
406- +++ b/<TMP>/a.txt
407- @@ -1 +1,2 @@
408- foo
409- +bar
410- diff --git a/<TMP>/b.txt b/<TMP>/b.txt
411- --- a/<TMP>/b.txt
412- +++ /dev/null
413- @@ -1 +0,0 @@
414- -z
415- "# ;
450+ let expected = {
451+ format ! (
452+ "diff --git a/<TMP>/a.txt b/<TMP>/a.txt\n index {ZERO_OID}..{ZERO_OID}\n --- a/<TMP>/a.txt\n +++ b/<TMP>/a.txt\n @@ -1 +1,2 @@\n foo\n +bar\n \
453+ diff --git a/<TMP>/b.txt b/<TMP>/b.txt\n deleted file mode {baseline_mode}\n index {ZERO_OID}..{ZERO_OID}\n --- a/<TMP>/b.txt\n +++ /dev/null\n @@ -1 +0,0 @@\n -z\n ",
454+ )
455+ } ;
416456 assert_eq ! ( combined, expected) ;
417457 }
418458}
0 commit comments