Skip to content

Commit feeb266

Browse files
committed
Added index
1 parent 6bd7ef3 commit feeb266

1 file changed

Lines changed: 85 additions & 45 deletions

File tree

codex-rs/core/src/turn_diff_tracker.rs

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
217269
mod 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\nnew file mode {mode}\nindex {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\nnew file mode {mode}\nindex {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\ndeleted file mode {baseline_mode}\nindex {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\nindex {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\nindex {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\nindex {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\ndeleted file mode {baseline_mode}\nindex {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

Comments
 (0)