//! Helpers for applying unified diffs using the system `git` binary. //! //! The entry point is [`apply_git_patch`], which writes a diff to a temporary //! file, shells out to `git apply` with the right flags, and then parses the //! command’s output into structured details. Callers can opt into dry-run //! mode via [`ApplyGitRequest::preflight`] and inspect the resulting paths to //! learn what would change before applying for real. use std::io; use std::path::Path; use std::path::PathBuf; use crate::FsmonitorOverride; use crate::apply_output::parse_git_apply_output; use crate::git_command::GitRunner; use crate::git_config_sources::ensure_no_worktree_config_sources; use crate::patch_paths::extract_effective_paths_from_patch; use crate::patch_paths::stage_effective_paths; use crate::safe_git::DISABLED_HOOKS_PATH; use crate::safe_git::ensure_no_selected_executable_git_filters; #[cfg(test)] use crate::safe_git::isolate_git_command_environment; /// Parameters for invoking [`apply_git_patch`]. #[derive(Debug, Clone)] pub struct ApplyGitRequest { pub cwd: PathBuf, pub diff: String, pub revert: bool, pub preflight: bool, } /// Result of running [`apply_git_patch`], including paths gleaned from stdout/stderr. #[derive(Debug, Clone)] pub struct ApplyGitResult { pub exit_code: i32, pub applied_paths: Vec, pub skipped_paths: Vec, pub conflicted_paths: Vec, pub stdout: String, pub stderr: String, pub cmd_for_log: String, } /// Apply a unified diff to the target repository by shelling out to `git apply`. /// /// When [`ApplyGitRequest::preflight`] is `true`, this behaves like `git apply --check` and /// leaves the working tree untouched while still parsing the command output for diagnostics. pub fn apply_git_patch(req: &ApplyGitRequest) -> io::Result { let git = GitRunner::for_cwd_io(&req.cwd)?; let mut cfg_parts = configured_git_config_parts(); ensure_no_worktree_config_sources(&git, &req.cwd, &cfg_parts)?; let requested_cwd = std::fs::canonicalize(&req.cwd)?; let git_root = resolve_git_root(&git, &req.cwd, &cfg_parts)?; if git_root != requested_cwd { ensure_no_worktree_config_sources(&git, &git_root, &cfg_parts)?; } // Write unified diff into a temporary file let (tmpdir, patch_path) = write_temp_patch(&req.diff)?; // Keep tmpdir alive until function end to ensure the file exists let _guard = tmpdir; let patch_paths = extract_effective_paths_from_patch(&git, &git_root, &patch_path, req.revert, &cfg_parts)?; let filter_guard = ensure_no_selected_executable_git_filters(&git, &git_root, &patch_paths, &cfg_parts)?; cfg_parts.extend(safe_git_config_parts()); cfg_parts.extend_from_slice(filter_guard.git_config_args()); if req.revert && !req.preflight { // Stage WT paths first to avoid index mismatch on revert. stage_effective_paths(&git, &git_root, &patch_paths, &cfg_parts)?; } // Build git args let mut args: Vec = vec!["apply".into(), "--3way".into()]; if req.revert { args.push("-R".into()); } args.push(patch_path.to_string_lossy().to_string()); // Optional preflight: dry-run only; do not modify working tree if req.preflight { let mut check_args = vec!["apply".to_string(), "--check".to_string()]; if req.revert { check_args.push("-R".to_string()); } check_args.push(patch_path.to_string_lossy().to_string()); let rendered = render_command_for_log(&git_root, &cfg_parts, &check_args); let (c_code, c_out, c_err) = run_git(&git, &git_root, &cfg_parts, &check_args)?; let (mut applied_paths, mut skipped_paths, mut conflicted_paths) = parse_git_apply_output(&c_out, &c_err); applied_paths.sort(); applied_paths.dedup(); skipped_paths.sort(); skipped_paths.dedup(); conflicted_paths.sort(); conflicted_paths.dedup(); return Ok(ApplyGitResult { exit_code: c_code, applied_paths, skipped_paths, conflicted_paths, stdout: c_out, stderr: c_err, cmd_for_log: rendered, }); } let cmd_for_log = render_command_for_log(&git_root, &cfg_parts, &args); let (code, stdout, stderr) = run_git(&git, &git_root, &cfg_parts, &args)?; let (mut applied_paths, mut skipped_paths, mut conflicted_paths) = parse_git_apply_output(&stdout, &stderr); applied_paths.sort(); applied_paths.dedup(); skipped_paths.sort(); skipped_paths.dedup(); conflicted_paths.sort(); conflicted_paths.dedup(); Ok(ApplyGitResult { exit_code: code, applied_paths, skipped_paths, conflicted_paths, stdout, stderr, cmd_for_log, }) } fn resolve_git_root( git: &GitRunner, cwd: &Path, git_config_args: &[String], ) -> io::Result { let requested_cwd = std::fs::canonicalize(cwd)?; let mut command = git.command_for_cwd(&requested_cwd)?; command .args(git_config_args) .arg("rev-parse") .arg("--show-toplevel"); let out = git.output(command)?; let code = out.status.code().unwrap_or(-1); if code != 0 { return Err(io::Error::other(format!( "not a git repository (exit {}): {}", code, String::from_utf8_lossy(&out.stderr) ))); } let reported_root = PathBuf::from(String::from_utf8_lossy(&out.stdout).trim()); let root = std::fs::canonicalize(&reported_root)?; let expected_root = crate::get_git_repo_root(&requested_cwd) .ok_or_else(|| { io::Error::new( io::ErrorKind::PermissionDenied, format!( "refusing to apply a patch because Git resolved worktree {} without a .git marker above requested cwd {}", root.display(), requested_cwd.display() ), ) }) .and_then(std::fs::canonicalize)?; if root != expected_root { return Err(io::Error::new( io::ErrorKind::PermissionDenied, format!( "refusing to apply a patch because Git resolved worktree {} instead of expected worktree {} for requested cwd {}", root.display(), expected_root.display(), requested_cwd.display() ), )); } Ok(root) } fn configured_git_config_parts() -> Vec { let mut cfg_parts = Vec::new(); if let Ok(cfg) = std::env::var("CODEX_APPLY_GIT_CFG") { for pair in cfg.split(',') { let pair = pair.trim(); if pair.is_empty() || !pair.contains('=') { continue; } cfg_parts.push("-c".to_string()); cfg_parts.push(pair.to_string()); } } cfg_parts } pub(crate) fn write_temp_patch(diff: &str) -> io::Result<(tempfile::TempDir, PathBuf)> { let dir = tempfile::tempdir()?; let path = dir.path().join("patch.diff"); std::fs::write(&path, diff)?; Ok((dir, path)) } pub(crate) fn run_git( git: &GitRunner, cwd: &Path, git_cfg: &[String], args: &[String], ) -> io::Result<(i32, String, String)> { let mut cmd = git.command_for_cwd(cwd)?; for p in git_cfg { cmd.arg(p); } for a in args { cmd.arg(a); } let out = git.output(cmd)?; let code = out.status.code().unwrap_or(-1); let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); Ok((code, stdout, stderr)) } pub(crate) fn safe_git_config_parts() -> Vec { vec![ "-c".to_string(), format!("core.hooksPath={DISABLED_HOOKS_PATH}"), "-c".to_string(), FsmonitorOverride::Disabled.git_config_arg().to_string(), ] } fn quote_shell(s: &str) -> String { let simple = s .chars() .all(|c| c.is_ascii_alphanumeric() || "-_.:/@%+".contains(c)); if simple { s.to_string() } else { format!("'{}'", s.replace('\'', "'\\''")) } } fn render_command_for_log(cwd: &Path, git_cfg: &[String], args: &[String]) -> String { let mut parts: Vec = Vec::new(); parts.push("git".to_string()); for a in git_cfg { parts.push(quote_shell(a)); } for a in args { parts.push(quote_shell(a)); } format!( "(cd {} && {})", quote_shell(&cwd.display().to_string()), parts.join(" ") ) } #[cfg(all(test, unix))] #[path = "apply_transport_tests.rs"] mod transport_tests; #[cfg(test)] #[path = "apply_filter_tests.rs"] mod filter_tests; #[cfg(test)] mod tests { use super::*; use std::ffi::OsStr; use std::path::Path; use std::sync::Mutex; use std::sync::OnceLock; fn env_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(())) } fn run(cwd: &Path, args: &[&str]) -> (i32, String, String) { let mut command = std::process::Command::new(args[0]); isolate_git_command_environment(&mut command); let out = command .args(&args[1..]) .current_dir(cwd) .output() .expect("spawn ok"); ( out.status.code().unwrap_or(-1), String::from_utf8_lossy(&out.stdout).into_owned(), String::from_utf8_lossy(&out.stderr).into_owned(), ) } fn run_isolated_test(test_name: &str, env: &[(&str, &OsStr)]) { let mut command = std::process::Command::new(std::env::current_exe().expect("test binary")); isolate_git_command_environment(&mut command); command .arg(test_name) .arg("--exact") .arg("--nocapture") .env("CODEX_GIT_UTILS_APPLY_ENV_CHILD", "1") .env("RUST_TEST_THREADS", "1"); for (name, value) in env { command.env(name, value); } let output = command.output().expect("run isolated test process"); assert!( output.status.success(), "isolated test {test_name} failed:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } fn init_repo() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("tempdir"); let root = dir.path(); // git init and minimal identity let _ = run(root, &["git", "init"]); let _ = run(root, &["git", "config", "user.email", "codex@example.com"]); let _ = run(root, &["git", "config", "user.name", "Codex"]); dir } fn read_file_normalized(path: &Path) -> String { std::fs::read_to_string(path) .expect("read file") .replace("\r\n", "\n") } fn commit_filter_attributes(root: &Path, tracked_path: &str) { std::fs::write( root.join(".gitattributes"), format!("{tracked_path} filter=x=y\n"), ) .expect("write attributes"); let (add_code, _, add_err) = run(root, &["git", "add", ".gitattributes"]); assert_eq!(add_code, 0, "add attributes: {add_err}"); let (commit_code, _, commit_err) = run(root, &["git", "commit", "-m", "attributes"]); assert_eq!(commit_code, 0, "commit attributes: {commit_err}"); } fn configure_clean_filter(root: &Path, tracked_path: &str) { commit_filter_attributes(root, tracked_path); let (config_code, _, config_err) = run( root, &[ "git", "config", "filter.x=y.clean", "git config codex.filterran true && git hash-object --stdin", ], ); assert_eq!(config_code, 0, "configure filter: {config_err}"); } fn configure_worktree_clean_filter(root: &Path, tracked_path: &str) { commit_filter_attributes(root, tracked_path); let (extension_code, _, extension_err) = run( root, &["git", "config", "extensions.worktreeConfig", "true"], ); assert_eq!(extension_code, 0, "enable worktree config: {extension_err}"); let (config_code, _, config_err) = run( root, &[ "git", "config", "--worktree", "filter.x=y.clean", "git config codex.filterran true && git hash-object --stdin", ], ); assert_eq!(config_code, 0, "configure worktree filter: {config_err}"); } fn configured_filter_ran(root: &Path) -> bool { let (code, _, _) = run(root, &["git", "config", "--get", "codex.filterran"]); code == 0 } #[cfg(unix)] fn trusted_git_directory() -> PathBuf { std::env::split_paths(&std::env::var_os("PATH").expect("PATH")) .find(|directory| directory.is_absolute() && directory.join("git").is_file()) .expect("trusted Git directory") } #[test] fn parse_output_unescapes_quoted_paths() { let stderr = "error: patch failed: \"hello\\tworld.txt\":1\n"; let (applied, skipped, conflicted) = parse_git_apply_output("", stderr); assert_eq!(applied, Vec::::new()); assert_eq!(conflicted, Vec::::new()); assert_eq!(skipped, vec!["hello\tworld.txt".to_string()]); } #[test] fn apply_add_success() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); let nested_cwd = root.join("nested"); std::fs::create_dir(&nested_cwd).expect("nested cwd"); let diff = "diff --git a/hello.txt b/hello.txt\nnew file mode 100644\n--- /dev/null\n+++ b/hello.txt\n@@ -0,0 +1,2 @@\n+hello\n+world\n"; let req = ApplyGitRequest { cwd: nested_cwd, diff: diff.to_string(), revert: false, preflight: false, }; let r = apply_git_patch(&req).expect("run apply"); assert_eq!(r.exit_code, 0, "exit code 0"); // File exists now assert!(root.join("hello.txt").exists()); } #[test] fn apply_uses_cwd_repo_despite_inherited_repository_selectors() { let _g = env_lock().lock().unwrap(); if std::env::var_os("CODEX_GIT_UTILS_APPLY_ENV_CHILD").is_none() { let alternate = init_repo(); let alternate_root = alternate.path(); std::fs::write(alternate_root.join("sentinel.txt"), "alternate\n") .expect("write alternate sentinel"); let (add_code, _, add_err) = run(alternate_root, &["git", "add", "sentinel.txt"]); assert_eq!(add_code, 0, "add alternate sentinel: {add_err}"); let (commit_code, _, commit_err) = run(alternate_root, &["git", "commit", "-m", "alternate"]); assert_eq!(commit_code, 0, "commit alternate sentinel: {commit_err}"); let alternate_git_dir = alternate_root.join(".git"); let alternate_index = alternate_git_dir.join("index"); run_isolated_test( "apply::tests::apply_uses_cwd_repo_despite_inherited_repository_selectors", &[ ("GIT_DIR", alternate_git_dir.as_os_str()), ("GIT_WORK_TREE", alternate_root.as_os_str()), ("GIT_COMMON_DIR", alternate_git_dir.as_os_str()), ("GIT_INDEX_FILE", alternate_index.as_os_str()), ("GIT_PREFIX", OsStr::new("elsewhere/")), ], ); assert_eq!( read_file_normalized(&alternate_root.join("sentinel.txt")), "alternate\n" ); return; } let repo = init_repo(); let root = repo.path(); std::fs::write(root.join("file.txt"), "old\n").expect("write target file"); let (add_code, _, add_err) = run(root, &["git", "add", "file.txt"]); assert_eq!(add_code, 0, "add target file: {add_err}"); let (commit_code, _, commit_err) = run(root, &["git", "commit", "-m", "target"]); assert_eq!(commit_code, 0, "commit target file: {commit_err}"); let result = apply_git_patch(&ApplyGitRequest { cwd: root.to_path_buf(), diff: "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new\n".to_string(), revert: false, preflight: false, }) .expect("apply in cwd-selected repository"); assert_eq!(result.exit_code, 0); assert_eq!(read_file_normalized(&root.join("file.txt")), "new\n"); } #[cfg(unix)] #[test] fn apply_uses_logical_process_cwd_to_reject_enclosing_git() { use std::os::unix::fs::PermissionsExt; let _g = env_lock().lock().unwrap(); if std::env::var_os("CODEX_GIT_UTILS_APPLY_LOGICAL_CWD_CHILD").is_none() { let fixture = tempfile::tempdir().expect("fixture"); let outer = fixture.path().join("outer"); let physical_nested = fixture.path().join("physical-nested"); let lexical_nested = outer.join("nested"); let outer_bin = outer.join("bin"); let outer_git = outer_bin.join("git"); let marker = outer_bin.join("git.ran"); std::fs::create_dir_all(&outer_bin).expect("outer Git directory"); std::fs::create_dir_all(&physical_nested).expect("physical nested repository"); let (outer_init, _, outer_err) = run(&outer, &["git", "init", "-q"]); assert_eq!(outer_init, 0, "init outer repository: {outer_err}"); let (nested_init, _, nested_err) = run(&physical_nested, &["git", "init", "-q"]); assert_eq!(nested_init, 0, "init nested repository: {nested_err}"); std::os::unix::fs::symlink(&physical_nested, &lexical_nested) .expect("symlink nested repository"); std::fs::write(&outer_git, "#!/bin/sh\nprintf ran >\"$0.ran\"\nexit 1\n") .expect("outer Git shim"); let mut permissions = std::fs::metadata(&outer_git) .expect("outer Git metadata") .permissions(); permissions.set_mode(0o755); std::fs::set_permissions(&outer_git, permissions).expect("executable outer Git"); let path = std::env::join_paths([outer_bin, trusted_git_directory()]).expect("PATH"); let mut command = std::process::Command::new(std::env::current_exe().expect("test binary")); isolate_git_command_environment(&mut command); let output = command .arg("apply::tests::apply_uses_logical_process_cwd_to_reject_enclosing_git") .arg("--exact") .arg("--nocapture") .current_dir(&lexical_nested) .env("CODEX_GIT_UTILS_APPLY_LOGICAL_CWD_CHILD", "1") .env("CODEX_GIT_UTILS_APPLY_LOGICAL_CWD_MARKER", &marker) .env("PWD", &lexical_nested) .env("PATH", path) .env("RUST_TEST_THREADS", "1") .output() .expect("run isolated logical-cwd test"); assert!( output.status.success(), "isolated logical-cwd test failed:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); assert!(!marker.exists(), "enclosing Git shim must not run"); return; } let cwd = std::env::current_dir().expect("physical process cwd"); let marker = PathBuf::from( std::env::var_os("CODEX_GIT_UTILS_APPLY_LOGICAL_CWD_MARKER").expect("marker path"), ); let result = apply_git_patch(&ApplyGitRequest { cwd, diff: "diff --git a/hello.txt b/hello.txt\nnew file mode 100644\n--- /dev/null\n+++ b/hello.txt\n@@ -0,0 +1 @@\n+hello\n".to_string(), revert: false, preflight: true, }) .expect("preflight through trusted Git"); assert_eq!(result.exit_code, 0, "preflight should succeed"); assert!(!marker.exists(), "enclosing Git shim must not run"); } #[test] fn apply_modify_conflict() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); // seed file and commit std::fs::write(root.join("file.txt"), "line1\nline2\nline3\n").unwrap(); let _ = run(root, &["git", "add", "file.txt"]); let _ = run(root, &["git", "commit", "-m", "seed"]); // local edit (unstaged) std::fs::write(root.join("file.txt"), "line1\nlocal2\nline3\n").unwrap(); // patch wants to change the same line differently let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n line1\n-line2\n+remote2\n line3\n"; let req = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: false, preflight: false, }; let r = apply_git_patch(&req).expect("run apply"); assert_ne!(r.exit_code, 0, "non-zero exit on conflict"); } #[test] fn apply_modify_skipped_missing_index() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); // Try to modify a file that is not in the index let diff = "diff --git a/ghost.txt b/ghost.txt\n--- a/ghost.txt\n+++ b/ghost.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n"; let req = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: false, preflight: false, }; let r = apply_git_patch(&req).expect("run apply"); assert_ne!(r.exit_code, 0, "non-zero exit on missing index"); } #[test] fn apply_then_revert_success() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); // Seed file and commit original content std::fs::write(root.join("file.txt"), "orig\n").unwrap(); let _ = run(root, &["git", "add", "file.txt"]); let _ = run(root, &["git", "commit", "-m", "seed"]); // Forward patch: orig -> ORIG let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+ORIG\n"; let apply_req = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: false, preflight: false, }; let res_apply = apply_git_patch(&apply_req).expect("apply ok"); assert_eq!(res_apply.exit_code, 0, "forward apply succeeded"); let after_apply = read_file_normalized(&root.join("file.txt")); assert_eq!(after_apply, "ORIG\n"); // Revert patch: ORIG -> orig (stage paths first; engine handles it) let revert_req = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: true, preflight: false, }; let res_revert = apply_git_patch(&revert_req).expect("revert ok"); assert_eq!(res_revert.exit_code, 0, "revert apply succeeded"); let after_revert = read_file_normalized(&root.join("file.txt")); assert_eq!(after_revert, "orig\n"); } #[test] fn revert_preflight_does_not_stage_index() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); // Seed repo and apply forward patch so the working tree reflects the change. std::fs::write(root.join("file.txt"), "orig\n").unwrap(); let _ = run(root, &["git", "add", "file.txt"]); let _ = run(root, &["git", "commit", "-m", "seed"]); let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+ORIG\n"; let apply_req = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: false, preflight: false, }; let res_apply = apply_git_patch(&apply_req).expect("apply ok"); assert_eq!(res_apply.exit_code, 0, "forward apply succeeded"); let (commit_code, _, commit_err) = run(root, &["git", "commit", "-am", "apply change"]); assert_eq!(commit_code, 0, "commit applied change: {commit_err}"); let (_code_before, staged_before, _stderr_before) = run(root, &["git", "diff", "--cached", "--name-only"]); let preflight_req = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: true, preflight: true, }; let res_preflight = apply_git_patch(&preflight_req).expect("preflight ok"); assert_eq!(res_preflight.exit_code, 0, "revert preflight succeeded"); let (_code_after, staged_after, _stderr_after) = run(root, &["git", "diff", "--cached", "--name-only"]); assert_eq!( staged_after.trim(), staged_before.trim(), "preflight should not stage new paths", ); let after_preflight = read_file_normalized(&root.join("file.txt")); assert_eq!(after_preflight, "ORIG\n"); } #[test] fn preflight_blocks_partial_changes() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); // Build a multi-file diff: one valid add (ok.txt) and one invalid modify (ghost.txt) let diff = "diff --git a/ok.txt b/ok.txt\nnew file mode 100644\n--- /dev/null\n+++ b/ok.txt\n@@ -0,0 +1,2 @@\n+alpha\n+beta\n\n\ diff --git a/ghost.txt b/ghost.txt\n--- a/ghost.txt\n+++ b/ghost.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n"; // 1) With preflight enabled, nothing should be changed (even though ok.txt could be added) let req1 = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: false, preflight: true, }; let r1 = apply_git_patch(&req1).expect("preflight apply"); assert_ne!(r1.exit_code, 0, "preflight reports failure"); assert!( !root.join("ok.txt").exists(), "preflight must prevent adding ok.txt" ); assert!( r1.cmd_for_log.contains("--check"), "preflight path recorded --check" ); // 2) Without preflight, we should see no --check in the executed command let req2 = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert: false, preflight: false, }; let r2 = apply_git_patch(&req2).expect("direct apply"); assert_ne!(r2.exit_code, 0, "apply is expected to fail overall"); assert!( !r2.cmd_for_log.contains("--check"), "non-preflight path should not use --check" ); } #[test] fn apply_rejects_configured_clean_filter_without_running_it() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); std::fs::write(root.join("file.txt"), "orig\n").expect("write file"); let (add_code, _, add_err) = run(root, &["git", "add", "file.txt"]); assert_eq!(add_code, 0, "add file: {add_err}"); let (commit_code, _, commit_err) = run(root, &["git", "commit", "-m", "seed"]); assert_eq!(commit_code, 0, "commit file: {commit_err}"); configure_clean_filter(root, "file.txt"); let diff = "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+next\n"; for (revert, preflight) in [(false, false), (false, true), (true, false), (true, true)] { let request = ApplyGitRequest { cwd: root.to_path_buf(), diff: diff.to_string(), revert, preflight, }; let error = apply_git_patch(&request).expect_err("reject configured filter"); assert_eq!(error.kind(), io::ErrorKind::Unsupported); assert!(!configured_filter_ran(root)); assert_eq!(read_file_normalized(&root.join("file.txt")), "orig\n"); } } #[test] fn apply_rejects_worktree_scoped_clean_filter_without_running_it() { let _g = env_lock().lock().unwrap(); let repo = init_repo(); let root = repo.path(); std::fs::write(root.join("file.txt"), "orig\n").expect("write file"); let (add_code, _, add_err) = run(root, &["git", "add", "file.txt"]); assert_eq!(add_code, 0, "add file: {add_err}"); let (commit_code, _, commit_err) = run(root, &["git", "commit", "-m", "seed"]); assert_eq!(commit_code, 0, "commit file: {commit_err}"); configure_worktree_clean_filter(root, "file.txt"); let request = ApplyGitRequest { cwd: root.to_path_buf(), diff: "diff --git a/file.txt b/file.txt\n--- a/file.txt\n+++ b/file.txt\n@@ -1,1 +1,1 @@\n-orig\n+next\n".to_string(), revert: false, preflight: true, }; let error = apply_git_patch(&request).expect_err("reject worktree filter"); assert_eq!(error.kind(), io::ErrorKind::Unsupported); assert!(!configured_filter_ran(root)); assert_eq!(read_file_normalized(&root.join("file.txt")), "orig\n"); } #[test] fn apply_probe_rejects_command_scoped_clean_filter() { let _g = env_lock().lock().unwrap(); if std::env::var_os("CODEX_GIT_UTILS_APPLY_ENV_CHILD").is_none() { run_isolated_test( "apply::tests::apply_probe_rejects_command_scoped_clean_filter", &[( "CODEX_APPLY_GIT_CFG", OsStr::new( "filter.codex-test.clean=git config codex.filterran true && git hash-object --stdin", ), )], ); return; } let repo = init_repo(); let root = repo.path(); std::fs::write(root.join("test.txt"), "orig\n").expect("write file"); let (add_code, _, add_err) = run(root, &["git", "add", "test.txt"]); assert_eq!(add_code, 0, "add file: {add_err}"); let (commit_code, _, commit_err) = run(root, &["git", "commit", "-m", "seed"]); assert_eq!(commit_code, 0, "commit file: {commit_err}"); std::fs::write(root.join(".gitattributes"), "test.txt filter=codex-test\n") .expect("attributes"); let request = ApplyGitRequest { cwd: root.to_path_buf(), diff: "diff --git a/test.txt b/test.txt\n--- a/test.txt\n+++ b/test.txt\n@@ -1 +1 @@\n-orig\n+next\n".to_string(), revert: false, preflight: true, }; let error = apply_git_patch(&request).expect_err("reject command-scoped filter"); assert_eq!(error.kind(), io::ErrorKind::Unsupported); assert!(!configured_filter_ran(root)); assert_eq!(read_file_normalized(&root.join("test.txt")), "orig\n"); } #[test] fn resolve_git_root_rejects_core_worktree_redirection() { let temp = tempfile::tempdir().expect("tempdir"); let attacker = temp.path().join("attacker"); let victim = temp.path().join("victim"); std::fs::create_dir_all(&attacker).expect("attacker"); std::fs::create_dir_all(&victim).expect("victim"); let (init_code, _, init_err) = run(&attacker, &["git", "init"]); assert_eq!(init_code, 0, "init attacker repo: {init_err}"); for redirected_worktree in [&victim, temp.path()] { let redirected_worktree = redirected_worktree.to_string_lossy(); let (config_code, _, config_err) = run( &attacker, &["git", "config", "core.worktree", &redirected_worktree], ); assert_eq!(config_code, 0, "configure core.worktree: {config_err}"); let git = GitRunner::for_cwd_io(&attacker).expect("trusted Git"); let error = resolve_git_root(&git, &attacker, &[]).expect_err("reject redirected worktree"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); assert!(error.to_string().contains("instead of expected worktree")); } } #[cfg(unix)] #[test] fn apply_propagates_unsafe_repository_metadata_before_git_launch() { use std::os::unix::fs::symlink; let repo = init_repo(); let root = repo.path(); let external = tempfile::tempdir().expect("external metadata parent"); let admin = external.path().join("admin"); std::fs::rename(root.join(".git"), &admin).expect("move metadata external"); symlink(&admin, root.join("switch")).expect("worktree metadata switch"); std::fs::write(root.join(".git"), "gitdir: switch\n").expect("write gitdir marker"); let (code, _, stderr) = run(root, &["git", "rev-parse", "--absolute-git-dir"]); assert_eq!(code, 0, "native Git fixture: {stderr}"); let request = ApplyGitRequest { cwd: root.to_path_buf(), diff: String::new(), revert: false, preflight: true, }; let error = apply_git_patch(&request).expect_err("unsafe metadata route"); assert_eq!(error.kind(), io::ErrorKind::PermissionDenied); assert!(error.to_string().contains("Git metadata route crosses")); assert!(!error.to_string().contains("PATH")); } }