Compare commits

...

4 Commits

Author SHA1 Message Date
Dylan Hurd
234532042b Merge branch 'main' into fix/windows-eol-apply-patch-4003 2025-12-02 14:40:15 -08:00
Dylan Hurd
f488bacf71 Merge branch 'main' into fix/windows-eol-apply-patch-4003 2025-11-17 21:58:36 -08:00
Chase Naples
9590c08d7c fix(apply-patch): detect CRLF only when all newlines are CRLF to avoid unintended newline conversion\n\n- Count total vs CRLF pairs and treat as CRLF only if every newline is CRLF\n- Prevents converting LF-formatted files that contain embedded in literals 2025-09-21 18:04:54 -04:00
Chase Naples
3a953c8b21 fix(apply-patch): preserve CRLF line endings on Windows when updating files (Fixes #4003)
- Detect CRLF in original file, strip trailing \r for matching, and re-emit using original EOL style
- Add test to ensure updated files retain CRLF endings and avoid mixed EOLs
2025-09-21 17:47:33 -04:00

View File

@@ -699,7 +699,33 @@ fn derive_new_contents_from_chunks(
}
};
let mut original_lines: Vec<String> = original_contents.split('\n').map(String::from).collect();
// Detect whether the source file uses Windows CRLF line endings consistently.
// We only consider a file CRLF-formatted if every newline is part of a
// CRLF sequence. This avoids rewriting an LF-formatted file that merely
// contains embedded "\r\n" within string literals.
let bytes = original_contents.as_bytes();
let mut n_newlines = 0usize;
let mut n_crlf = 0usize;
for i in 0..bytes.len() {
if bytes[i] == b'\n' {
n_newlines += 1;
if i > 0 && bytes[i - 1] == b'\r' {
n_crlf += 1;
}
}
}
let uses_crlf = n_newlines > 0 && n_crlf == n_newlines;
let mut original_lines: Vec<String> = original_contents
.split('\n')
.map(|s| {
if uses_crlf && s.ends_with('\r') {
s.trim_end_matches('\r').to_string()
} else {
s.to_string()
}
})
.collect();
// Drop the trailing empty element that results from the final newline so
// that line counts match the behaviour of standard `diff`.
@@ -713,7 +739,11 @@ fn derive_new_contents_from_chunks(
if !new_lines.last().is_some_and(String::is_empty) {
new_lines.push(String::new());
}
let new_contents = new_lines.join("\n");
let new_contents = if uses_crlf {
new_lines.join("\r\n")
} else {
new_lines.join("\n")
};
Ok(AppliedPatch {
original_contents,
new_contents,
@@ -1359,6 +1389,43 @@ PATCH"#,
assert_eq!(contents, "a\nB\nc\nd\nE\nf\ng\n");
}
/// Ensure CRLF line endings are preserved for updated files on Windowsstyle inputs.
#[test]
fn test_preserve_crlf_line_endings_on_update() {
let dir = tempdir().unwrap();
let path = dir.path().join("crlf.txt");
// Original file uses CRLF (\r\n) endings.
std::fs::write(&path, b"a\r\nb\r\nc\r\n").unwrap();
// Replace `b` -> `B` and append `d`.
let patch = wrap_patch(&format!(
r#"*** Update File: {}
@@
a
-b
+B
@@
c
+d
*** End of File"#,
path.display()
));
let mut stdout = Vec::new();
let mut stderr = Vec::new();
apply_patch(&patch, &mut stdout, &mut stderr).unwrap();
let out = std::fs::read(&path).unwrap();
// Expect all CRLF endings; count occurrences of CRLF and ensure there are 4 lines.
let content = String::from_utf8_lossy(&out);
assert!(content.contains("\r\n"));
// No bare LF occurrences immediately preceding a non-CR: the text should not contain "a\nb".
assert!(!content.contains("a\nb"));
// Validate exact content sequence with CRLF delimiters.
assert_eq!(content, "a\r\nB\r\nc\r\nd\r\n");
}
#[test]
fn test_pure_addition_chunk_followed_by_removal() {
let dir = tempdir().unwrap();