diff --git a/codex-rs/apply-patch/src/lib.rs b/codex-rs/apply-patch/src/lib.rs index bec4c3c855..e65eb98cb7 100644 --- a/codex-rs/apply-patch/src/lib.rs +++ b/codex-rs/apply-patch/src/lib.rs @@ -44,6 +44,9 @@ use crate::invocation::ExtractHeredocError; /// surface. pub const CODEX_CORE_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch"; +/// Enables CRLF preservation for the standalone `apply_patch` command. +pub const APPLY_PATCH_PRESERVE_CRLF_ARG: &str = "--preserve-crlf"; + /// Controls optional behavior when parsing or applying a patch. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct ApplyPatchOptions { diff --git a/codex-rs/apply-patch/src/parser.rs b/codex-rs/apply-patch/src/parser.rs index 767339a02c..10173a69ed 100644 --- a/codex-rs/apply-patch/src/parser.rs +++ b/codex-rs/apply-patch/src/parser.rs @@ -442,8 +442,8 @@ fn parse_update_file_chunk( match line_contents.chars().next() { None => { // Interpret this as an empty line. - chunk.old_lines.push(String::new()); - chunk.new_lines.push(String::new()); + chunk.old_lines.push(line.to_string()); + chunk.new_lines.push(line.to_string()); } Some(' ') => { chunk.old_lines.push(line[1..].to_string()); @@ -587,6 +587,34 @@ fn test_update_file_chunk() { ); } +#[test] +fn test_update_file_chunk_preserves_crlf_for_bare_empty_context_line() { + assert_eq!( + parse_update_file_chunk( + &["@@\r", " before\r", "\r", " after\r", "*** End Patch\r"], + /*line_number*/ 123, + /*allow_missing_context*/ false, + ), + Ok(( + UpdateFileChunk { + change_context: None, + old_lines: vec![ + "before\r".to_string(), + "\r".to_string(), + "after\r".to_string(), + ], + new_lines: vec![ + "before\r".to_string(), + "\r".to_string(), + "after\r".to_string(), + ], + is_end_of_file: false, + }, + 4, + )) + ); +} + #[test] fn test_parse_patch() { assert_eq!( diff --git a/codex-rs/apply-patch/src/standalone_executable.rs b/codex-rs/apply-patch/src/standalone_executable.rs index 4534facc0b..cbb53a0e67 100644 --- a/codex-rs/apply-patch/src/standalone_executable.rs +++ b/codex-rs/apply-patch/src/standalone_executable.rs @@ -1,8 +1,6 @@ use std::io::Read; use std::io::Write; -const PRESERVE_CRLF_FLAG: &str = "--preserve-crlf"; - pub fn main() -> ! { let exit_code = run_main(); std::process::exit(exit_code); @@ -17,7 +15,7 @@ pub fn run_main() -> i32 { let _argv0 = args.next(); let first_arg = args.next(); let (options, patch_arg) = match first_arg { - Some(arg) if arg.to_str() == Some(PRESERVE_CRLF_FLAG) => { + Some(arg) if arg == crate::APPLY_PATCH_PRESERVE_CRLF_ARG => { (crate::ApplyPatchOptions::preserve_crlf(), args.next()) } patch_arg => (crate::ApplyPatchOptions::default(), patch_arg), diff --git a/codex-rs/arg0/src/lib.rs b/codex-rs/arg0/src/lib.rs index ba254d57ab..d18dd655fa 100644 --- a/codex-rs/arg0/src/lib.rs +++ b/codex-rs/arg0/src/lib.rs @@ -4,6 +4,7 @@ use std::future::Future; use std::path::Path; use std::path::PathBuf; +use codex_apply_patch::APPLY_PATCH_PRESERVE_CRLF_ARG; use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1; use codex_exec_server::CODEX_FS_HELPER_ARG1; use codex_install_context::InstallContext; @@ -100,7 +101,15 @@ pub fn arg0_dispatch() -> Option { codex_exec_server::run_fs_helper_main(); } if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 { - let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned)); + let first_arg = args.next(); + let (options, patch_arg) = match first_arg { + Some(arg) if arg == APPLY_PATCH_PRESERVE_CRLF_ARG => ( + codex_apply_patch::ApplyPatchOptions::preserve_crlf(), + args.next(), + ), + patch_arg => (codex_apply_patch::ApplyPatchOptions::default(), patch_arg), + }; + let patch_arg = patch_arg.and_then(|s| s.to_str().map(str::to_owned)); let exit_code = match patch_arg { Some(patch_arg) => { let mut stdout = std::io::stdout(); @@ -116,8 +125,9 @@ pub fn arg0_dispatch() -> Option { Ok(runtime) => runtime, Err(_) => std::process::exit(1), }; - match runtime.block_on(codex_apply_patch::apply_patch( + match runtime.block_on(codex_apply_patch::apply_patch_with_options( &patch_arg, + options, &cwd, &mut stdout, &mut stderr, diff --git a/codex-rs/exec/tests/suite/apply_patch.rs b/codex-rs/exec/tests/suite/apply_patch.rs index 3b0006695e..b67f366584 100644 --- a/codex-rs/exec/tests/suite/apply_patch.rs +++ b/codex-rs/exec/tests/suite/apply_patch.rs @@ -44,6 +44,31 @@ fn test_standalone_exec_cli_can_use_apply_patch() -> anyhow::Result<()> { Ok(()) } +#[test] +fn test_standalone_exec_cli_can_preserve_crlf() -> anyhow::Result<()> { + let tmp = tempdir()?; + let relative_path = "source.txt"; + let absolute_path = tmp.path().join(relative_path); + fs::write(&absolute_path, "original content\r\n")?; + + Command::new(codex_utils_cargo_bin::cargo_bin("codex-exec")?) + .arg(CODEX_CORE_APPLY_PATCH_ARG1) + .arg("--preserve-crlf") + .arg( + "*** Begin Patch\r\n*** Update File: source.txt\r\n@@\r\n-original content\r\n+modified by apply_patch\r\n*** End Patch\r\n", + ) + .current_dir(tmp.path()) + .assert() + .success() + .stdout("Success. Updated the following files:\nM source.txt\n") + .stderr(predicates::str::is_empty()); + assert_eq!( + fs::read_to_string(absolute_path)?, + "modified by apply_patch\r\n" + ); + Ok(()) +} + #[cfg(not(target_os = "windows"))] #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn test_apply_patch_tool() -> anyhow::Result<()> {