mod file_update; mod invocation; mod parser; mod seek_sequence; mod standalone_executable; mod streaming_parser; mod text_file; use std::collections::HashMap; use std::io; use std::path::PathBuf; use anyhow::Context; use anyhow::Result; use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::GetMetadataOptions; use codex_exec_server::ReadFileOptions; use codex_exec_server::RemoveOptions; use codex_exec_server::WriteFileOptions; use codex_utils_path_uri::PathUri; use codex_utils_path_uri::PathUriParseError; pub use parser::Hunk; pub use parser::ParseError; use parser::ParseError::*; pub use parser::UpdateFileChunk; pub use parser::parse_patch; pub use streaming_parser::StreamingPatchParser; use thiserror::Error; use file_update::AppliedPatch; pub use file_update::ApplyPatchFileUpdate; use file_update::derive_new_contents_from_chunks; pub use file_update::unified_diff_from_chunks; pub use file_update::unified_diff_from_chunks_with_context; pub(crate) use file_update::unified_diff_from_chunks_with_mode; pub use invocation::MaybeApplyPatch; pub use invocation::maybe_parse_apply_patch; pub use invocation::maybe_parse_apply_patch_verified; pub use invocation::maybe_parse_apply_patch_verified_with_mode; pub use invocation::verify_apply_patch_args; pub use invocation::verify_apply_patch_args_with_mode; pub use standalone_executable::main; use crate::invocation::ExtractHeredocError; /// Special argv[1] flag used when the Codex executable self-invokes to run the /// internal `apply_patch` path. /// /// Although this constant lives in `codex-apply-patch` (to avoid forcing /// `codex-arg0` to depend on `codex-core`), it remains part of the "codex core" /// process-invocation contract for the standalone `apply_patch` command /// surface. pub const CODEX_CORE_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch"; /// Internal environment variable used to carry the selected update mode /// through the arg0-dispatched standalone executable. pub const CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR: &str = "CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS"; /// Controls how updates reconstruct the target file after matching a patch. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum ApplyPatchFileUpdateMode { /// Preserve the historical behavior of normalizing updated files to LF. #[default] NormalizeToLf, /// Preserve existing line endings and use the file's preferred ending for new lines. PreserveLineEndings, } /// Policy for one patch application. Standalone callers follow symlinks by default. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ApplyPatchOptions { pub update_file_mode: ApplyPatchFileUpdateMode, /// Whether filesystem operations may resolve symlinks in any path component. pub follow_symlinks: bool, } impl Default for ApplyPatchOptions { fn default() -> Self { Self { update_file_mode: ApplyPatchFileUpdateMode::default(), follow_symlinks: true, } } } /// Reads the update mode selected for an arg0-dispatched `apply_patch` process. #[doc(hidden)] pub fn apply_patch_file_update_mode_from_env() -> ApplyPatchFileUpdateMode { match std::env::var(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR).as_deref() { Ok("1") => ApplyPatchFileUpdateMode::PreserveLineEndings, _ => ApplyPatchFileUpdateMode::NormalizeToLf, } } #[derive(Debug, Error, PartialEq)] pub enum ApplyPatchError { #[error(transparent)] ParseError(#[from] ParseError), #[error(transparent)] IoError(#[from] IoError), /// Error that occurs while computing replacements when applying patch chunks #[error("{0}")] ComputeReplacements(String), /// A patch path could not be resolved as a path URI. #[error(transparent)] PathUri(#[from] PathUriParseError), /// A raw patch body was provided without an explicit `apply_patch` invocation. #[error( "patch detected without explicit call to apply_patch. Rerun as [\"apply_patch\", \"\"]" )] ImplicitInvocation, } impl From for ApplyPatchError { fn from(err: std::io::Error) -> Self { ApplyPatchError::IoError(IoError { context: "I/O error".to_string(), source: err, }) } } impl From<&std::io::Error> for ApplyPatchError { fn from(err: &std::io::Error) -> Self { ApplyPatchError::IoError(IoError { context: "I/O error".to_string(), source: std::io::Error::new(err.kind(), err.to_string()), }) } } #[derive(Debug, Error)] #[error("{context}: {source}")] pub struct IoError { context: String, #[source] source: std::io::Error, } impl PartialEq for IoError { fn eq(&self, other: &Self) -> bool { self.context == other.context && self.source.to_string() == other.source.to_string() } } /// Both the raw PATCH argument to `apply_patch` as well as the PATCH argument /// parsed into hunks. #[derive(Debug, PartialEq)] pub struct ApplyPatchArgs { pub patch: String, pub hunks: Vec, pub workdir: Option, pub environment_id: Option, } #[derive(Debug, PartialEq)] pub enum ApplyPatchFileChange { Add { content: String, }, Delete { content: String, }, Update { unified_diff: String, move_path: Option, /// new_content that will result after the unified_diff is applied. new_content: String, }, } #[derive(Debug, PartialEq)] pub enum MaybeApplyPatchVerified { /// `argv` corresponded to an `apply_patch` invocation, and these are the /// resulting proposed file changes. Body(ApplyPatchAction), /// `argv` could not be parsed to determine whether it corresponds to an /// `apply_patch` invocation. ShellParseError(ExtractHeredocError), /// `argv` corresponded to an `apply_patch` invocation, but it could not /// be fulfilled due to the specified error. CorrectnessError(ApplyPatchError), /// `argv` decidedly did not correspond to an `apply_patch` invocation. NotApplyPatch, } /// ApplyPatchAction is the result of parsing an `apply_patch` command. By /// construction, all paths should be absolute paths. #[derive(Debug, PartialEq)] pub struct ApplyPatchAction { changes: HashMap, update_file_mode: ApplyPatchFileUpdateMode, /// The raw patch argument that can be used to apply the patch. i.e., if the /// original arg was parsed in "lenient" mode with a /// heredoc, this should be the value without the heredoc wrapper. pub patch: String, /// The working directory that was used to resolve relative paths in the patch. pub cwd: PathUri, } impl ApplyPatchAction { pub fn is_empty(&self) -> bool { self.changes.is_empty() } /// Returns the changes that would be made by applying the patch. pub fn changes(&self) -> &HashMap { &self.changes } /// Returns the update mode selected while the patch was verified. pub fn update_file_mode(&self) -> ApplyPatchFileUpdateMode { self.update_file_mode } /// Should be used exclusively for testing. (Not worth the overhead of /// creating a feature flag for this.) pub fn new_add_for_test(path: &PathUri, content: String) -> Self { #[expect(clippy::expect_used)] let filename = path.basename().expect("path should not be empty"); let patch = format!( r#"*** Begin Patch *** Update File: {filename} @@ + {content} *** End Patch"#, ); let changes = HashMap::from([(path.clone(), ApplyPatchFileChange::Add { content })]); #[expect(clippy::expect_used)] Self { changes, update_file_mode: ApplyPatchFileUpdateMode::default(), cwd: path.parent().expect("path should have parent"), patch, } } } /// Textual file changes that were actually committed while applying a patch. #[derive(Clone, Debug, PartialEq)] pub struct AppliedPatchDelta { changes: Vec, exact: bool, } impl AppliedPatchDelta { fn new(changes: Vec, exact: bool) -> Self { Self { changes, exact } } fn empty() -> Self { Self::new(Vec::new(), /*exact*/ true) } pub fn changes(&self) -> &[AppliedPatchChange] { &self.changes } pub fn is_empty(&self) -> bool { self.changes.is_empty() } pub fn is_exact(&self) -> bool { self.exact } /// Appends a later committed prefix while preserving the aggregate exactness. pub fn append(&mut self, other: Self) { self.changes.extend(other.changes); self.exact &= other.exact; } } impl Default for AppliedPatchDelta { fn default() -> Self { Self::empty() } } /// A committed file change, preserved in the order it was applied. #[derive(Clone, Debug, PartialEq)] pub struct AppliedPatchChange { pub path: PathUri, pub change: AppliedPatchFileChange, } #[derive(Clone, Debug, PartialEq)] pub enum AppliedPatchFileChange { Add { content: String, overwritten_content: Option, }, Delete { content: String, }, Update { move_path: Option, old_content: String, overwritten_move_content: Option, new_content: String, }, } /// A failed patch application together with the textual mutations that were /// definitely committed before the failure was observed. #[derive(Debug, Error)] #[error("{error}")] pub struct ApplyPatchFailure { #[source] error: ApplyPatchError, delta: AppliedPatchDelta, } impl ApplyPatchFailure { fn new(error: ApplyPatchError, delta: AppliedPatchDelta) -> Self { Self { error, delta } } fn without_delta(error: ApplyPatchError) -> Self { Self::new(error, AppliedPatchDelta::empty()) } pub fn delta(&self) -> &AppliedPatchDelta { &self.delta } pub fn into_parts(self) -> (ApplyPatchError, AppliedPatchDelta) { (self.error, self.delta) } } /// Applies the patch and prints the result to stdout/stderr. pub async fn apply_patch( patch: &str, cwd: &PathUri, stdout: &mut impl std::io::Write, stderr: &mut impl std::io::Write, fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, ) -> Result { apply_patch_with_options( patch, ApplyPatchOptions::default(), cwd, stdout, stderr, fs, sandbox, ) .await } /// Applies the patch using the selected options and prints the result /// to stdout/stderr. pub async fn apply_patch_with_options( patch: &str, options: ApplyPatchOptions, cwd: &PathUri, stdout: &mut impl std::io::Write, stderr: &mut impl std::io::Write, fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, ) -> Result { let hunks = match parse_patch(patch) { Ok(source) => source.hunks, Err(e) => { match &e { InvalidPatchError(message) => { writeln!(stderr, "Invalid patch: {message}") .map_err(ApplyPatchError::from) .map_err(ApplyPatchFailure::without_delta)?; } InvalidHunkError { message, line_number, } => { writeln!( stderr, "Invalid patch hunk on line {line_number}: {message}" ) .map_err(ApplyPatchError::from) .map_err(ApplyPatchFailure::without_delta)?; } } return Err(ApplyPatchFailure::without_delta( ApplyPatchError::ParseError(e), )); } }; apply_hunks_with_options(&hunks, options, cwd, stdout, stderr, fs, sandbox).await } /// Applies hunks and continues to update stdout/stderr pub async fn apply_hunks( hunks: &[Hunk], cwd: &PathUri, stdout: &mut impl std::io::Write, stderr: &mut impl std::io::Write, fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, ) -> Result { apply_hunks_with_options( hunks, ApplyPatchOptions::default(), cwd, stdout, stderr, fs, sandbox, ) .await } /// Applies hunks using the selected file-update mode and continues to update /// stdout/stderr. async fn apply_hunks_with_options( hunks: &[Hunk], options: ApplyPatchOptions, cwd: &PathUri, stdout: &mut impl std::io::Write, stderr: &mut impl std::io::Write, fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, ) -> Result { let mut delta = AppliedPatchDelta::empty(); match apply_hunks_to_files(hunks, options, cwd, fs, sandbox, &mut delta).await { Ok(affected_paths) => { print_summary(&affected_paths, stdout).map_err(|error| { ApplyPatchFailure::new(ApplyPatchError::from(error), delta.clone()) })?; Ok(delta) } Err(error) => { let msg = error.to_string(); writeln!(stderr, "{msg}").map_err(|error| { ApplyPatchFailure::new(ApplyPatchError::from(error), delta.clone()) })?; let error = if let Some(io) = error.downcast_ref::() { ApplyPatchError::from(io) } else { ApplyPatchError::IoError(IoError { context: msg, source: std::io::Error::other(error), }) }; Err(ApplyPatchFailure::new(error, delta)) } } } /// Applies each parsed patch hunk to the filesystem. /// Returns an error if any of the changes could not be applied. /// Tracks file paths affected by applying a patch, preserving the path spelling /// from the patch for user-facing summaries. pub struct AffectedPaths { pub added: Vec, pub modified: Vec, pub deleted: Vec, } /// Apply the hunks to the filesystem, returning which files were added, modified, or deleted. /// Returns an error if the patch could not be applied. async fn apply_hunks_to_files( hunks: &[Hunk], options: ApplyPatchOptions, cwd: &PathUri, fs: &dyn ExecutorFileSystem, sandbox: Option<&FileSystemSandboxContext>, delta: &mut AppliedPatchDelta, ) -> anyhow::Result { let ApplyPatchOptions { update_file_mode, follow_symlinks, } = options; if hunks.is_empty() { anyhow::bail!("No files were modified."); } let mut added: Vec = Vec::new(); let mut modified: Vec = Vec::new(); let mut deleted: Vec = Vec::new(); // A failed write can still have modified the target before surfacing an // error (for example by truncating before ENOSPC), so the accumulated // delta is no longer exact when a write fails. macro_rules! try_write { ($result:expr) => { match $result { Ok(value) => value, Err(error) => { delta.exact = false; return Err(anyhow::Error::from(error)); } } }; } for hunk in hunks { let affected_path = hunk.path().to_path_buf(); let path_uri = hunk.resolve_path(cwd)?; match hunk { Hunk::AddFile { contents, .. } => { let overwritten_content = read_optional_file_text_for_delta( &path_uri, fs, follow_symlinks, sandbox, &mut delta.exact, ) .await; try_write!( write_file_with_missing_parent_retry( fs, &path_uri, contents.clone().into_bytes(), follow_symlinks, sandbox, ) .await ); delta.changes.push(AppliedPatchChange { path: path_uri, change: AppliedPatchFileChange::Add { content: contents.clone(), overwritten_content, }, }); added.push(affected_path); } Hunk::DeleteFile { .. } => { note_existing_path_delta_support( &path_uri, fs, follow_symlinks, sandbox, &mut delta.exact, ) .await; let deleted_content = fs .read_file_text(&path_uri, ReadFileOptions { follow_symlinks }, sandbox) .await .ok(); if deleted_content.is_none() { delta.exact = false; } ensure_not_directory(&path_uri, fs, follow_symlinks, sandbox) .await .with_context(|| { format!( "Failed to delete file {}", path_uri.inferred_native_path_string() ) })?; if let Err(error) = fs .remove( &path_uri, RemoveOptions { recursive: false, force: false, follow_symlinks, }, sandbox, ) .await .with_context(|| { format!( "Failed to delete file {}", path_uri.inferred_native_path_string() ) }) { delta.exact &= remove_failure_was_side_effect_free( &path_uri, deleted_content.as_deref(), fs, follow_symlinks, sandbox, ) .await; return Err(error); } if let Some(content) = deleted_content { delta.changes.push(AppliedPatchChange { path: path_uri, change: AppliedPatchFileChange::Delete { content }, }); } deleted.push(affected_path); } Hunk::UpdateFile { move_path, chunks, .. } => { note_existing_path_delta_support( &path_uri, fs, follow_symlinks, sandbox, &mut delta.exact, ) .await; let AppliedPatch { original_contents, new_contents, } = derive_new_contents_from_chunks( &path_uri, chunks, update_file_mode, fs, follow_symlinks, sandbox, ) .await?; if let Some(dest) = move_path { let dest_uri = cwd.join(&dest.to_string_lossy())?; let overwritten_move_content = read_optional_file_text_for_delta( &dest_uri, fs, follow_symlinks, sandbox, &mut delta.exact, ) .await; try_write!( write_file_with_missing_parent_retry( fs, &dest_uri, new_contents.clone().into_bytes(), follow_symlinks, sandbox, ) .await ); let dest_write_change_index = delta.changes.len(); delta.changes.push(AppliedPatchChange { path: dest_uri.clone(), change: AppliedPatchFileChange::Add { content: new_contents.clone(), overwritten_content: overwritten_move_content.clone(), }, }); ensure_not_directory(&path_uri, fs, follow_symlinks, sandbox) .await .with_context(|| { format!( "Failed to remove original {}", path_uri.inferred_native_path_string() ) })?; if let Err(error) = fs .remove( &path_uri, RemoveOptions { recursive: false, force: false, follow_symlinks, }, sandbox, ) .await .with_context(|| { format!( "Failed to remove original {}", path_uri.inferred_native_path_string() ) }) { delta.exact &= remove_failure_was_side_effect_free( &path_uri, Some(&original_contents), fs, follow_symlinks, sandbox, ) .await; return Err(error); } delta.changes[dest_write_change_index] = AppliedPatchChange { path: path_uri, change: AppliedPatchFileChange::Update { move_path: Some(dest_uri), old_content: original_contents, overwritten_move_content, new_content: new_contents, }, }; modified.push(affected_path); } else { try_write!( fs.write_file( &path_uri, new_contents.clone().into_bytes(), WriteFileOptions { follow_symlinks }, sandbox, ) .await .with_context(|| format!( "Failed to write file {}", path_uri.inferred_native_path_string() )) ); delta.changes.push(AppliedPatchChange { path: path_uri, change: AppliedPatchFileChange::Update { move_path: None, old_content: original_contents, overwritten_move_content: None, new_content: new_contents, }, }); modified.push(affected_path); } } } } Ok(AffectedPaths { added, modified, deleted, }) } async fn ensure_not_directory( path: &PathUri, fs: &dyn ExecutorFileSystem, follow_symlinks: bool, sandbox: Option<&FileSystemSandboxContext>, ) -> io::Result<()> { let metadata = fs .get_metadata(path, GetMetadataOptions { follow_symlinks }, sandbox) .await?; if metadata.is_directory { return Err(io::Error::new( io::ErrorKind::InvalidInput, "path is a directory", )); } Ok(()) } async fn remove_failure_was_side_effect_free( path: &PathUri, expected_content: Option<&str>, fs: &dyn ExecutorFileSystem, follow_symlinks: bool, sandbox: Option<&FileSystemSandboxContext>, ) -> bool { match expected_content { Some(expected_content) => fs .read_file_text(path, ReadFileOptions { follow_symlinks }, sandbox) .await .is_ok_and(|content| content == expected_content), None => false, } } async fn read_optional_file_text_for_delta( path: &PathUri, fs: &dyn ExecutorFileSystem, follow_symlinks: bool, sandbox: Option<&FileSystemSandboxContext>, exact: &mut bool, ) -> Option { note_existing_path_delta_support(path, fs, follow_symlinks, sandbox, exact).await; match fs .read_file_text(path, ReadFileOptions { follow_symlinks }, sandbox) .await { Ok(content) => Some(content), Err(source) if source.kind() == io::ErrorKind::NotFound => None, Err(_) => { *exact = false; None } } } async fn note_existing_path_delta_support( path: &PathUri, fs: &dyn ExecutorFileSystem, follow_symlinks: bool, sandbox: Option<&FileSystemSandboxContext>, exact: &mut bool, ) { match fs .get_metadata(path, GetMetadataOptions { follow_symlinks }, sandbox) .await { Ok(metadata) if metadata.is_file && !metadata.is_symlink => {} Ok(_) => *exact = false, Err(source) if source.kind() == io::ErrorKind::NotFound => {} Err(_) => *exact = false, } } async fn write_file_with_missing_parent_retry( fs: &dyn ExecutorFileSystem, path: &PathUri, contents: Vec, follow_symlinks: bool, sandbox: Option<&FileSystemSandboxContext>, ) -> anyhow::Result<()> { match fs .write_file( path, contents.clone(), WriteFileOptions { follow_symlinks }, sandbox, ) .await { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound => { if let Some(parent) = path.parent() { fs.create_directory( &parent, CreateDirectoryOptions { recursive: true, follow_symlinks, }, sandbox, ) .await .with_context(|| { format!( "Failed to create parent directories for {}", path.inferred_native_path_string() ) })?; } fs.write_file( path, contents, WriteFileOptions { follow_symlinks }, sandbox, ) .await .with_context(|| { format!( "Failed to write file {}", path.inferred_native_path_string() ) })?; Ok(()) } Err(err) => Err(err).with_context(|| { format!( "Failed to write file {}", path.inferred_native_path_string() ) }), } } /// Print the summary of changes in git-style format. /// Write a summary of changes to the given writer. pub fn print_summary( affected: &AffectedPaths, out: &mut impl std::io::Write, ) -> std::io::Result<()> { writeln!(out, "Success. Updated the following files:")?; for path in &affected.added { writeln!(out, "A {}", path.display())?; } for path in &affected.modified { writeln!(out, "M {}", path.display())?; } for path in &affected.deleted { writeln!(out, "D {}", path.display())?; } Ok(()) } #[cfg(test)] mod tests { use super::*; use codex_exec_server::LOCAL_FS; use pretty_assertions::assert_eq; use std::fs; use tempfile::tempdir; /// Helper to construct a patch with the given body. fn wrap_patch(body: &str) -> String { format!("*** Begin Patch\n{body}\n*** End Patch") } #[tokio::test] async fn test_add_file_hunk_creates_file_with_contents() { let dir = tempdir().unwrap(); let path = dir.path().join("add.txt"); let patch = wrap_patch(&format!( r#"*** Add File: {} +ab +cd"#, path.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); // Verify expected stdout and stderr outputs. let stdout_str = String::from_utf8(stdout).unwrap(); let stderr_str = String::from_utf8(stderr).unwrap(); let expected_out = format!( "Success. Updated the following files:\nA {}\n", path.display() ); assert_eq!(stdout_str, expected_out); assert_eq!(stderr_str, ""); let contents = fs::read_to_string(path).unwrap(); assert_eq!(contents, "ab\ncd\n"); } #[tokio::test] async fn test_apply_patch_hunks_accept_relative_and_absolute_paths() { let dir = tempdir().unwrap(); let cwd = PathUri::from_host_native_path(dir.path()).expect("absolute test path"); let relative_add = dir.path().join("relative-add.txt"); let absolute_add = dir.path().join("absolute-add.txt"); let relative_delete = dir.path().join("relative-delete.txt"); let absolute_delete = dir.path().join("absolute-delete.txt"); let relative_update = dir.path().join("relative-update.txt"); let absolute_update = dir.path().join("absolute-update.txt"); fs::write(&relative_delete, "delete relative\n").unwrap(); fs::write(&absolute_delete, "delete absolute\n").unwrap(); fs::write(&relative_update, "relative old\n").unwrap(); fs::write(&absolute_update, "absolute old\n").unwrap(); let patch = wrap_patch(&format!( r#"*** Add File: relative-add.txt +relative add *** Add File: {} +absolute add *** Delete File: relative-delete.txt *** Delete File: {} *** Update File: relative-update.txt @@ -relative old +relative new *** Update File: {} @@ -absolute old +absolute new"#, absolute_add.display(), absolute_delete.display(), absolute_update.display(), )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &cwd, &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); assert_eq!(fs::read_to_string(&relative_add).unwrap(), "relative add\n"); assert_eq!(fs::read_to_string(&absolute_add).unwrap(), "absolute add\n"); assert!(!relative_delete.exists()); assert!(!absolute_delete.exists()); assert_eq!( fs::read_to_string(&relative_update).unwrap(), "relative new\n" ); assert_eq!( fs::read_to_string(&absolute_update).unwrap(), "absolute new\n" ); assert_eq!(String::from_utf8(stderr).unwrap(), ""); assert_eq!( String::from_utf8(stdout).unwrap(), format!( "Success. Updated the following files:\nA relative-add.txt\nA {}\nM relative-update.txt\nM {}\nD relative-delete.txt\nD {}\n", absolute_add.display(), absolute_update.display(), absolute_delete.display(), ) ); } #[tokio::test] async fn test_delete_file_hunk_removes_file() { let dir = tempdir().unwrap(); let path = dir.path().join("del.txt"); fs::write(&path, "x").unwrap(); let patch = wrap_patch(&format!("*** Delete File: {}", path.display())); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); let stdout_str = String::from_utf8(stdout).unwrap(); let stderr_str = String::from_utf8(stderr).unwrap(); let expected_out = format!( "Success. Updated the following files:\nD {}\n", path.display() ); assert_eq!(stdout_str, expected_out); assert_eq!(stderr_str, ""); assert!(!path.exists()); } #[tokio::test] async fn test_update_file_hunk_modifies_content() { let dir = tempdir().unwrap(); let path = dir.path().join("update.txt"); fs::write(&path, "foo\nbar\n").unwrap(); let patch = wrap_patch(&format!( r#"*** Update File: {} @@ foo -bar +baz"#, path.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); // Validate modified file contents and expected stdout/stderr. let stdout_str = String::from_utf8(stdout).unwrap(); let stderr_str = String::from_utf8(stderr).unwrap(); let expected_out = format!( "Success. Updated the following files:\nM {}\n", path.display() ); assert_eq!(stdout_str, expected_out); assert_eq!(stderr_str, ""); let contents = fs::read_to_string(&path).unwrap(); assert_eq!(contents, "foo\nbaz\n"); } #[tokio::test] async fn test_update_file_hunk_can_move_file() { let dir = tempdir().unwrap(); let src = dir.path().join("src.txt"); let dest = dir.path().join("dst.txt"); fs::write(&src, "line\n").unwrap(); let patch = wrap_patch(&format!( r#"*** Update File: {} *** Move to: {} @@ -line +line2"#, src.display(), dest.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); // Validate move semantics and expected stdout/stderr. let stdout_str = String::from_utf8(stdout).unwrap(); let stderr_str = String::from_utf8(stderr).unwrap(); let expected_out = format!( "Success. Updated the following files:\nM {}\n", dest.display() ); assert_eq!(stdout_str, expected_out); assert_eq!(stderr_str, ""); assert!(!src.exists()); let contents = fs::read_to_string(&dest).unwrap(); assert_eq!(contents, "line2\n"); } #[cfg(unix)] #[tokio::test] async fn test_failed_move_returns_committed_destination_delta() { use std::os::unix::fs::PermissionsExt; let dir = tempdir().unwrap(); let source_dir = dir.path().join("locked"); let dest_dir = dir.path().join("out"); fs::create_dir(&source_dir).unwrap(); fs::create_dir(&dest_dir).unwrap(); let src = source_dir.join("src.txt"); let dest = dest_dir.join("dst.txt"); fs::write(&src, "line\n").unwrap(); fs::set_permissions(&source_dir, fs::Permissions::from_mode(0o555)).unwrap(); let patch = wrap_patch( "*** Update File: locked/src.txt\n*** Move to: out/dst.txt\n@@\n-line\n+line2", ); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let failure = apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .expect_err("source removal should fail after destination write"); fs::set_permissions(&source_dir, fs::Permissions::from_mode(0o755)).unwrap(); assert!( String::from_utf8(stderr) .unwrap() .contains(&format!("Failed to remove original {}", src.display())) ); assert_eq!( failure.delta(), &AppliedPatchDelta::new( vec![AppliedPatchChange { path: PathUri::from_host_native_path(&dest).expect("absolute destination path"), change: AppliedPatchFileChange::Add { content: "line2\n".to_string(), overwritten_content: None, }, }], /*exact*/ true, ) ); assert_eq!(fs::read_to_string(src).unwrap(), "line\n"); assert_eq!(fs::read_to_string(dest).unwrap(), "line2\n"); } /// Verify that a single `Update File` hunk with multiple change chunks can update different /// parts of a file and that the file is listed only once in the summary. #[tokio::test] async fn test_multiple_update_chunks_apply_to_single_file() { // Start with a file containing four lines. let dir = tempdir().unwrap(); let path = dir.path().join("multi.txt"); fs::write(&path, "foo\nbar\nbaz\nqux\n").unwrap(); // Construct an update patch with two separate change chunks. // The first chunk uses the line `foo` as context and transforms `bar` into `BAR`. // The second chunk uses `baz` as context and transforms `qux` into `QUX`. let patch = wrap_patch(&format!( r#"*** Update File: {} @@ foo -bar +BAR @@ baz -qux +QUX"#, path.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); let stdout_str = String::from_utf8(stdout).unwrap(); let stderr_str = String::from_utf8(stderr).unwrap(); let expected_out = format!( "Success. Updated the following files:\nM {}\n", path.display() ); assert_eq!(stdout_str, expected_out); assert_eq!(stderr_str, ""); let contents = fs::read_to_string(&path).unwrap(); assert_eq!(contents, "foo\nBAR\nbaz\nQUX\n"); } /// A more involved `Update File` hunk that exercises additions, deletions and /// replacements in separate chunks that appear in non‑adjacent parts of the /// file. Verifies that all edits are applied and that the summary lists the /// file only once. #[tokio::test] async fn test_update_file_hunk_interleaved_changes() { let dir = tempdir().unwrap(); let path = dir.path().join("interleaved.txt"); // Original file: six numbered lines. fs::write(&path, "a\nb\nc\nd\ne\nf\n").unwrap(); // Patch performs: // • Replace `b` → `B` // • Replace `e` → `E` (using surrounding context) // • Append new line `g` at the end‑of‑file let patch = wrap_patch(&format!( r#"*** Update File: {} @@ a -b +B @@ c d -e +E @@ f +g *** End of File"#, path.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); let stdout_str = String::from_utf8(stdout).unwrap(); let stderr_str = String::from_utf8(stderr).unwrap(); let expected_out = format!( "Success. Updated the following files:\nM {}\n", path.display() ); assert_eq!(stdout_str, expected_out); assert_eq!(stderr_str, ""); let contents = fs::read_to_string(&path).unwrap(); assert_eq!(contents, "a\nB\nc\nd\nE\nf\ng\n"); } #[tokio::test] async fn test_pure_addition_chunk_followed_by_removal() { let dir = tempdir().unwrap(); let path = dir.path().join("panic.txt"); fs::write(&path, "line1\nline2\nline3\n").unwrap(); let patch = wrap_patch(&format!( r#"*** Update File: {} @@ +after-context +second-line @@ line1 -line2 -line3 +line2-replacement"#, path.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); let contents = fs::read_to_string(path).unwrap(); assert_eq!( contents, "line1\nline2-replacement\nafter-context\nsecond-line\n" ); } /// Ensure that patches authored with ASCII characters can update lines that /// contain typographic Unicode punctuation (e.g. EN DASH, NON-BREAKING /// HYPHEN). Historically `git apply` succeeds in such scenarios but our /// internal matcher failed requiring an exact byte-for-byte match. The /// fuzzy-matching pass that normalises common punctuation should now bridge /// the gap. #[tokio::test] async fn test_update_line_with_unicode_dash() { let dir = tempdir().unwrap(); let path = dir.path().join("unicode.py"); // Original line contains EN DASH (\u{2013}) and NON-BREAKING HYPHEN (\u{2011}). let original = "import asyncio # local import \u{2013} avoids top\u{2011}level dep\n"; std::fs::write(&path, original).unwrap(); // Patch uses plain ASCII dash / hyphen. let patch = wrap_patch(&format!( r#"*** Update File: {} @@ -import asyncio # local import - avoids top-level dep +import asyncio # HELLO"#, path.display() )); let mut stdout = Vec::new(); let mut stderr = Vec::new(); apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); // File should now contain the replaced comment. let expected = "import asyncio # HELLO\n"; let contents = std::fs::read_to_string(&path).unwrap(); assert_eq!(contents, expected); // Ensure success summary lists the file as modified. let stdout_str = String::from_utf8(stdout).unwrap(); let expected_out = format!( "Success. Updated the following files:\nM {}\n", path.display() ); assert_eq!(stdout_str, expected_out); // No stderr expected. assert_eq!(String::from_utf8(stderr).unwrap(), ""); } #[cfg(unix)] #[tokio::test] async fn test_apply_patch_fails_on_write_error() { use std::os::unix::fs::PermissionsExt; let dir = tempdir().unwrap(); let locked_dir = dir.path().join("locked"); fs::create_dir(&locked_dir).unwrap(); fs::set_permissions(&locked_dir, fs::Permissions::from_mode(0o555)).unwrap(); let patch = wrap_patch("*** Add File: locked/new.txt\n+after"); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let result = apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await; let failure = result.expect_err("write should fail"); fs::set_permissions(&locked_dir, fs::Permissions::from_mode(0o755)).unwrap(); assert!(!failure.delta().is_exact()); } #[tokio::test] async fn test_unreadable_destinations_return_inexact_delta() { let dir = tempdir().unwrap(); let path = dir.path().join("binary.dat"); fs::write(dir.path().join("source.txt"), "before\n").unwrap(); let cwd = PathUri::from_host_native_path(dir.path()).expect("absolute test path"); for patch in [ wrap_patch("*** Add File: binary.dat\n+text"), wrap_patch("*** Update File: source.txt\n*** Move to: binary.dat\n@@\n-before\n+after"), ] { fs::write(&path, [0xff, 0xfe, 0xfd]).unwrap(); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let delta = apply_patch( &patch, &cwd, &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); assert!(!delta.is_exact()); } } #[cfg(unix)] #[tokio::test] async fn test_delete_symlink_returns_inexact_delta() { use std::os::unix::fs::symlink; let dir = tempdir().unwrap(); fs::write(dir.path().join("target.txt"), "target\n").unwrap(); symlink(dir.path().join("target.txt"), dir.path().join("link.txt")).unwrap(); let patch = wrap_patch("*** Delete File: link.txt"); let mut stdout = Vec::new(); let mut stderr = Vec::new(); let delta = apply_patch( &patch, &PathUri::from_host_native_path(dir.path()).expect("absolute test path"), &mut stdout, &mut stderr, LOCAL_FS.as_ref(), /*sandbox*/ None, ) .await .unwrap(); assert!(!delta.is_exact()); } }