mirror of
https://github.com/openai/codex.git
synced 2026-04-24 00:11:51 +03:00
Compare commits
5 Commits
pr16131
...
dh--apply-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b61a996d | ||
|
|
fee0462f5b | ||
|
|
2cf4550d5d | ||
|
|
ffddaa61f1 | ||
|
|
fa14a6b305 |
@@ -13,8 +13,10 @@ use crate::ApplyPatchArgs;
|
||||
use crate::ApplyPatchError;
|
||||
use crate::ApplyPatchFileChange;
|
||||
use crate::ApplyPatchFileUpdate;
|
||||
use crate::ApplyPatchOptions;
|
||||
use crate::IoError;
|
||||
use crate::MaybeApplyPatchVerified;
|
||||
use crate::PRESERVE_CRLF_FLAG;
|
||||
use crate::parser::Hunk;
|
||||
use crate::parser::ParseError;
|
||||
use crate::parser::parse_patch;
|
||||
@@ -100,23 +102,59 @@ fn extract_apply_patch_from_shell(
|
||||
}
|
||||
|
||||
// TODO: make private once we remove tests in lib.rs
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||
maybe_parse_apply_patch_with_options(argv, ApplyPatchOptions::default())
|
||||
}
|
||||
|
||||
pub fn maybe_parse_apply_patch_with_options(
|
||||
argv: &[String],
|
||||
options: ApplyPatchOptions,
|
||||
) -> MaybeApplyPatch {
|
||||
match argv {
|
||||
// Direct invocation: apply_patch <patch>
|
||||
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => match parse_patch(body) {
|
||||
Ok(source) => MaybeApplyPatch::Body(source),
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
},
|
||||
[cmd, body] if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) => {
|
||||
let body = if options.preserve_crlf {
|
||||
body.to_string()
|
||||
} else {
|
||||
body.replace("\r\n", "\n")
|
||||
};
|
||||
match parse_patch(&body) {
|
||||
Ok(source) => MaybeApplyPatch::Body(source),
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
}
|
||||
}
|
||||
// Direct invocation with CRLF preservation: apply_patch --preserve-crlf <patch>
|
||||
[cmd, flag, body]
|
||||
if APPLY_PATCH_COMMANDS.contains(&cmd.as_str()) && flag == PRESERVE_CRLF_FLAG =>
|
||||
{
|
||||
let body = if options.preserve_crlf {
|
||||
body.to_string()
|
||||
} else {
|
||||
body.replace("\r\n", "\n")
|
||||
};
|
||||
match parse_patch(&body) {
|
||||
Ok(source) => MaybeApplyPatch::Body(source),
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
}
|
||||
}
|
||||
// Shell heredoc form: (optional `cd <path> &&`) apply_patch <<'EOF' ...
|
||||
_ => match parse_shell_script(argv) {
|
||||
Some((shell, script)) => match extract_apply_patch_from_shell(shell, script) {
|
||||
Ok((body, workdir)) => match parse_patch(&body) {
|
||||
Ok(mut source) => {
|
||||
source.workdir = workdir;
|
||||
MaybeApplyPatch::Body(source)
|
||||
Ok((body, workdir)) => {
|
||||
let body = if options.preserve_crlf {
|
||||
body
|
||||
} else {
|
||||
body.replace("\r\n", "\n")
|
||||
};
|
||||
match parse_patch(&body) {
|
||||
Ok(mut source) => {
|
||||
source.workdir = workdir;
|
||||
MaybeApplyPatch::Body(source)
|
||||
}
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
}
|
||||
Err(e) => MaybeApplyPatch::PatchParseError(e),
|
||||
},
|
||||
}
|
||||
Err(ExtractHeredocError::CommandDidNotStartWithApplyPatch) => {
|
||||
MaybeApplyPatch::NotApplyPatch
|
||||
}
|
||||
@@ -130,6 +168,14 @@ pub fn maybe_parse_apply_patch(argv: &[String]) -> MaybeApplyPatch {
|
||||
/// cwd must be an absolute path so that we can resolve relative paths in the
|
||||
/// patch.
|
||||
pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApplyPatchVerified {
|
||||
maybe_parse_apply_patch_verified_with_options(argv, cwd, ApplyPatchOptions::default())
|
||||
}
|
||||
|
||||
pub fn maybe_parse_apply_patch_verified_with_options(
|
||||
argv: &[String],
|
||||
cwd: &Path,
|
||||
options: ApplyPatchOptions,
|
||||
) -> MaybeApplyPatchVerified {
|
||||
// Detect a raw patch body passed directly as the command or as the body of a shell
|
||||
// script. In these cases, report an explicit error rather than applying the patch.
|
||||
if let [body] = argv
|
||||
@@ -143,7 +189,7 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
|
||||
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
|
||||
}
|
||||
|
||||
match maybe_parse_apply_patch(argv) {
|
||||
match maybe_parse_apply_patch_with_options(argv, options) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs {
|
||||
patch,
|
||||
hunks,
|
||||
@@ -221,7 +267,9 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
|
||||
///
|
||||
/// Supported top‑level forms (must be the only top‑level statement):
|
||||
/// - `apply_patch <<'EOF'\n...\nEOF`
|
||||
/// - `apply_patch --preserve-crlf <<'EOF'\n...\nEOF`
|
||||
/// - `cd <path> && apply_patch <<'EOF'\n...\nEOF`
|
||||
/// - `cd <path> && apply_patch --preserve-crlf <<'EOF'\n...\nEOF`
|
||||
///
|
||||
/// Notes about matching:
|
||||
/// - Parsed with Tree‑sitter Bash and a strict query that uses anchors so the
|
||||
@@ -243,7 +291,9 @@ fn extract_apply_patch_from_bash(
|
||||
// whole-script forms, each expressed as a single top-level statement:
|
||||
//
|
||||
// 1. apply_patch <<'EOF'\n...\nEOF
|
||||
// 2. cd <path> && apply_patch <<'EOF'\n...\nEOF
|
||||
// 2. apply_patch --preserve-crlf <<'EOF'\n...\nEOF
|
||||
// 3. cd <path> && apply_patch <<'EOF'\n...\nEOF
|
||||
// 4. cd <path> && apply_patch --preserve-crlf <<'EOF'\n...\nEOF
|
||||
//
|
||||
// Key ideas when reading the query:
|
||||
// - dots (`.`) between named nodes enforces adjacency among named children and
|
||||
@@ -279,6 +329,21 @@ fn extract_apply_patch_from_bash(
|
||||
.))
|
||||
.)
|
||||
|
||||
(
|
||||
program
|
||||
. (redirected_statement
|
||||
body: (command
|
||||
name: (command_name (word) @apply_name)
|
||||
argument: (word) @apply_flag .)
|
||||
(#any-of? @apply_name "apply_patch" "applypatch")
|
||||
(#eq? @apply_flag "--preserve-crlf")
|
||||
redirect: (heredoc_redirect
|
||||
. (heredoc_start)
|
||||
. (heredoc_body) @heredoc
|
||||
. (heredoc_end)
|
||||
.))
|
||||
.)
|
||||
|
||||
(
|
||||
program
|
||||
. (redirected_statement
|
||||
@@ -302,6 +367,32 @@ fn extract_apply_patch_from_bash(
|
||||
. (heredoc_end)
|
||||
.))
|
||||
.)
|
||||
|
||||
(
|
||||
program
|
||||
. (redirected_statement
|
||||
body: (list
|
||||
. (command
|
||||
name: (command_name (word) @cd_name) .
|
||||
argument: [
|
||||
(word) @cd_path
|
||||
(string (string_content) @cd_path)
|
||||
(raw_string) @cd_raw_string
|
||||
] .)
|
||||
"&&"
|
||||
. (command
|
||||
name: (command_name (word) @apply_name)
|
||||
argument: (word) @apply_flag)
|
||||
.)
|
||||
(#eq? @cd_name "cd")
|
||||
(#any-of? @apply_name "apply_patch" "applypatch")
|
||||
(#eq? @apply_flag "--preserve-crlf")
|
||||
redirect: (heredoc_redirect
|
||||
. (heredoc_start)
|
||||
. (heredoc_body) @heredoc
|
||||
. (heredoc_end)
|
||||
.))
|
||||
.)
|
||||
"#,
|
||||
)
|
||||
.expect("valid bash query")
|
||||
@@ -522,6 +613,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal_with_preserve_crlf_flag() {
|
||||
let args = strs_to_strings(&[
|
||||
"apply_patch",
|
||||
PRESERVE_CRLF_FLAG,
|
||||
r#"*** Begin Patch
|
||||
*** Add File: foo
|
||||
+hi
|
||||
*** End Patch
|
||||
"#,
|
||||
]);
|
||||
|
||||
match maybe_parse_apply_patch(&args) {
|
||||
MaybeApplyPatch::Body(ApplyPatchArgs { hunks, .. }) => {
|
||||
assert_eq!(hunks, expected_single_add());
|
||||
}
|
||||
result => panic!("expected MaybeApplyPatch::Body got {result:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_heredoc() {
|
||||
assert_match(&heredoc_script(""), None);
|
||||
|
||||
@@ -18,6 +18,7 @@ use similar::TextDiff;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use invocation::maybe_parse_apply_patch_verified;
|
||||
pub use invocation::maybe_parse_apply_patch_verified_with_options;
|
||||
pub use standalone_executable::main;
|
||||
|
||||
use crate::invocation::ExtractHeredocError;
|
||||
@@ -25,6 +26,11 @@ use crate::invocation::ExtractHeredocError;
|
||||
/// Detailed instructions for gpt-4.1 on how to use the `apply_patch` tool.
|
||||
pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_tool_instructions.md");
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct ApplyPatchOptions {
|
||||
pub preserve_crlf: bool,
|
||||
}
|
||||
|
||||
/// Special argv[1] flag used when the Codex executable self-invokes to run the
|
||||
/// internal `apply_patch` path.
|
||||
///
|
||||
@@ -34,6 +40,9 @@ pub const APPLY_PATCH_TOOL_INSTRUCTIONS: &str = include_str!("../apply_patch_too
|
||||
/// dispatcher.
|
||||
pub const CODEX_CORE_APPLY_PATCH_ARG1: &str = "--codex-run-as-apply-patch";
|
||||
|
||||
/// Flag used to preserve CRLF line endings when applying CRLF patches.
|
||||
pub const PRESERVE_CRLF_FLAG: &str = "--preserve-crlf";
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum ApplyPatchError {
|
||||
#[error(transparent)]
|
||||
@@ -185,7 +194,21 @@ pub fn apply_patch(
|
||||
stdout: &mut impl std::io::Write,
|
||||
stderr: &mut impl std::io::Write,
|
||||
) -> Result<(), ApplyPatchError> {
|
||||
let hunks = match parse_patch(patch) {
|
||||
apply_patch_with_options(patch, ApplyPatchOptions::default(), stdout, stderr)
|
||||
}
|
||||
|
||||
pub fn apply_patch_with_options(
|
||||
patch: &str,
|
||||
options: ApplyPatchOptions,
|
||||
stdout: &mut impl std::io::Write,
|
||||
stderr: &mut impl std::io::Write,
|
||||
) -> Result<(), ApplyPatchError> {
|
||||
let patch = if options.preserve_crlf {
|
||||
patch.to_string()
|
||||
} else {
|
||||
patch.replace("\r\n", "\n")
|
||||
};
|
||||
let hunks = match parse_patch(&patch) {
|
||||
Ok(source) => source.hunks,
|
||||
Err(e) => {
|
||||
match &e {
|
||||
|
||||
@@ -152,7 +152,7 @@ enum ParseMode {
|
||||
}
|
||||
|
||||
fn parse_patch_text(patch: &str, mode: ParseMode) -> Result<ApplyPatchArgs, ParseError> {
|
||||
let lines: Vec<&str> = patch.trim().lines().collect();
|
||||
let lines: Vec<&str> = patch.trim().split('\n').collect();
|
||||
let lines: &[&str] = match check_patch_boundaries_strict(&lines) {
|
||||
Ok(()) => &lines,
|
||||
Err(e) => match mode {
|
||||
@@ -206,7 +206,9 @@ fn check_patch_boundaries_lenient<'a>(
|
||||
) -> Result<&'a [&'a str], ParseError> {
|
||||
match original_lines {
|
||||
[first, .., last] => {
|
||||
if (first == &"<<EOF" || first == &"<<'EOF'" || first == &"<<\"EOF\"")
|
||||
let first = first.trim();
|
||||
let last = last.trim();
|
||||
if (first == "<<EOF" || first == "<<'EOF'" || first == "<<\"EOF\"")
|
||||
&& last.ends_with("EOF")
|
||||
&& original_lines.len() >= 4
|
||||
{
|
||||
@@ -282,9 +284,11 @@ fn parse_one_hunk(lines: &[&str], line_number: usize) -> Result<(Hunk, usize), P
|
||||
let mut parsed_lines = 1;
|
||||
|
||||
// Optional: move file line
|
||||
let move_path = remaining_lines
|
||||
.first()
|
||||
.and_then(|x| x.strip_prefix(MOVE_TO_MARKER));
|
||||
let move_path = remaining_lines.first().and_then(|x| {
|
||||
x.strip_suffix('\r')
|
||||
.unwrap_or(x)
|
||||
.strip_prefix(MOVE_TO_MARKER)
|
||||
});
|
||||
|
||||
if move_path.is_some() {
|
||||
remaining_lines = &remaining_lines[1..];
|
||||
@@ -353,9 +357,10 @@ fn parse_update_file_chunk(
|
||||
}
|
||||
// If we see an explicit context marker @@ or @@ <context>, consume it; otherwise, optionally
|
||||
// allow treating the chunk as starting directly with diff lines.
|
||||
let (change_context, start_index) = if lines[0] == EMPTY_CHANGE_CONTEXT_MARKER {
|
||||
let first_line = lines[0].strip_suffix('\r').unwrap_or(lines[0]);
|
||||
let (change_context, start_index) = if first_line == EMPTY_CHANGE_CONTEXT_MARKER {
|
||||
(None, 1)
|
||||
} else if let Some(context) = lines[0].strip_prefix(CHANGE_CONTEXT_MARKER) {
|
||||
} else if let Some(context) = first_line.strip_prefix(CHANGE_CONTEXT_MARKER) {
|
||||
(Some(context.to_string()), 1)
|
||||
} else {
|
||||
if !allow_missing_context {
|
||||
@@ -383,7 +388,8 @@ fn parse_update_file_chunk(
|
||||
};
|
||||
let mut parsed_lines = 0;
|
||||
for line in &lines[start_index..] {
|
||||
match *line {
|
||||
let line_contents = line.strip_suffix('\r').unwrap_or(line);
|
||||
match line_contents {
|
||||
EOF_MARKER => {
|
||||
if parsed_lines == 0 {
|
||||
return Err(InvalidHunkError {
|
||||
@@ -395,7 +401,7 @@ fn parse_update_file_chunk(
|
||||
parsed_lines += 1;
|
||||
break;
|
||||
}
|
||||
line_contents => {
|
||||
_ => {
|
||||
match line_contents.chars().next() {
|
||||
None => {
|
||||
// Interpret this as an empty line.
|
||||
@@ -403,14 +409,14 @@ fn parse_update_file_chunk(
|
||||
chunk.new_lines.push(String::new());
|
||||
}
|
||||
Some(' ') => {
|
||||
chunk.old_lines.push(line_contents[1..].to_string());
|
||||
chunk.new_lines.push(line_contents[1..].to_string());
|
||||
chunk.old_lines.push(line[1..].to_string());
|
||||
chunk.new_lines.push(line[1..].to_string());
|
||||
}
|
||||
Some('+') => {
|
||||
chunk.new_lines.push(line_contents[1..].to_string());
|
||||
chunk.new_lines.push(line[1..].to_string());
|
||||
}
|
||||
Some('-') => {
|
||||
chunk.old_lines.push(line_contents[1..].to_string());
|
||||
chunk.old_lines.push(line[1..].to_string());
|
||||
}
|
||||
_ => {
|
||||
if parsed_lines == 0 {
|
||||
@@ -669,6 +675,28 @@ fn test_parse_patch_lenient() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_patch_preserves_crlf() {
|
||||
let patch_text = "*** Begin Patch\r\n*** Update File: sample.txt\r\n@@\r\n-one\r\n+uno\r\n two\r\n*** End Patch\r\n";
|
||||
assert_eq!(
|
||||
parse_patch_text(patch_text, ParseMode::Strict),
|
||||
Ok(ApplyPatchArgs {
|
||||
hunks: vec![UpdateFile {
|
||||
path: PathBuf::from("sample.txt"),
|
||||
move_path: None,
|
||||
chunks: vec![UpdateFileChunk {
|
||||
change_context: None,
|
||||
old_lines: vec!["one\r".to_string(), "two\r".to_string()],
|
||||
new_lines: vec!["uno\r".to_string(), "two\r".to_string()],
|
||||
is_end_of_file: false,
|
||||
}],
|
||||
}],
|
||||
patch: patch_text.trim().to_string(),
|
||||
workdir: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_one_hunk() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -9,12 +9,46 @@ pub fn main() -> ! {
|
||||
/// We would prefer to return `std::process::ExitCode`, but its `exit_process()`
|
||||
/// method is still a nightly API and we want main() to return !.
|
||||
pub fn run_main() -> i32 {
|
||||
// Expect either one argument (the full apply_patch payload) or read it from stdin.
|
||||
// Expect either one argument (the full apply_patch payload), optionally prefixed
|
||||
// by --preserve-crlf, or read it from stdin.
|
||||
let mut args = std::env::args_os();
|
||||
let _argv0 = args.next();
|
||||
let mut preserve_crlf = false;
|
||||
|
||||
let patch_arg = match args.next() {
|
||||
let first_arg = args.next();
|
||||
let patch_arg = match first_arg {
|
||||
Some(arg) => match arg.into_string() {
|
||||
Ok(arg) if arg == crate::PRESERVE_CRLF_FLAG => {
|
||||
preserve_crlf = true;
|
||||
match args.next() {
|
||||
Some(arg) => match arg.into_string() {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
eprintln!("Error: apply_patch requires a UTF-8 PATCH argument.");
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// No patch argument after flag; attempt to read patch from stdin.
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_to_string(&mut buf) {
|
||||
Ok(_) => {
|
||||
if buf.is_empty() {
|
||||
eprintln!(
|
||||
"Usage: apply_patch [--preserve-crlf] 'PATCH'\n echo 'PATCH' | apply_patch [--preserve-crlf]"
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
buf
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: Failed to read PATCH from stdin.\n{err}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
eprintln!("Error: apply_patch requires a UTF-8 PATCH argument.");
|
||||
@@ -27,7 +61,9 @@ pub fn run_main() -> i32 {
|
||||
match std::io::stdin().read_to_string(&mut buf) {
|
||||
Ok(_) => {
|
||||
if buf.is_empty() {
|
||||
eprintln!("Usage: apply_patch 'PATCH'\n echo 'PATCH' | apply_patch");
|
||||
eprintln!(
|
||||
"Usage: apply_patch [--preserve-crlf] 'PATCH'\n echo 'PATCH' | apply_patch [--preserve-crlf]"
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
buf
|
||||
@@ -42,13 +78,14 @@ pub fn run_main() -> i32 {
|
||||
|
||||
// Refuse extra args to avoid ambiguity.
|
||||
if args.next().is_some() {
|
||||
eprintln!("Error: apply_patch accepts exactly one argument.");
|
||||
eprintln!("Error: apply_patch accepts exactly one PATCH argument.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
let mut stderr = std::io::stderr();
|
||||
match crate::apply_patch(&patch_arg, &mut stdout, &mut stderr) {
|
||||
let options = crate::ApplyPatchOptions { preserve_crlf };
|
||||
match crate::apply_patch_with_options(&patch_arg, options, &mut stdout, &mut stderr) {
|
||||
Ok(()) => {
|
||||
// Flush to ensure output ordering when used in pipelines.
|
||||
let _ = stdout.flush();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* -text
|
||||
@@ -0,0 +1 @@
|
||||
--preserve-crlf
|
||||
@@ -0,0 +1,2 @@
|
||||
uno
|
||||
two
|
||||
@@ -0,0 +1,2 @@
|
||||
one
|
||||
two
|
||||
7
codex-rs/apply-patch/tests/fixtures/scenarios/023_preserves_crlf_for_update_hunks/patch.txt
vendored
Normal file
7
codex-rs/apply-patch/tests/fixtures/scenarios/023_preserves_crlf_for_update_hunks/patch.txt
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*** Begin Patch
|
||||
*** Update File: sample.txt
|
||||
@@
|
||||
-one
|
||||
+uno
|
||||
two
|
||||
*** End Patch
|
||||
@@ -0,0 +1 @@
|
||||
* -text
|
||||
@@ -0,0 +1 @@
|
||||
--preserve-crlf
|
||||
@@ -0,0 +1,2 @@
|
||||
hello
|
||||
world
|
||||
@@ -0,0 +1,5 @@
|
||||
*** Begin Patch
|
||||
*** Add File: created.txt
|
||||
+hello
|
||||
+world
|
||||
*** End Patch
|
||||
@@ -0,0 +1 @@
|
||||
* -text
|
||||
@@ -0,0 +1 @@
|
||||
--preserve-crlf
|
||||
@@ -0,0 +1,2 @@
|
||||
first
|
||||
third
|
||||
@@ -0,0 +1,3 @@
|
||||
first
|
||||
second
|
||||
third
|
||||
@@ -0,0 +1,7 @@
|
||||
*** Begin Patch
|
||||
*** Update File: lines.txt
|
||||
@@
|
||||
first
|
||||
-second
|
||||
third
|
||||
*** End Patch
|
||||
@@ -0,0 +1 @@
|
||||
* -text
|
||||
@@ -0,0 +1 @@
|
||||
--preserve-crlf
|
||||
@@ -0,0 +1 @@
|
||||
keep
|
||||
@@ -0,0 +1,2 @@
|
||||
new
|
||||
line
|
||||
@@ -0,0 +1,2 @@
|
||||
old
|
||||
line
|
||||
@@ -0,0 +1 @@
|
||||
keep
|
||||
@@ -0,0 +1,8 @@
|
||||
*** Begin Patch
|
||||
*** Update File: old/name.txt
|
||||
*** Move to: renamed/dir/name.txt
|
||||
@@
|
||||
-old
|
||||
+new
|
||||
line
|
||||
*** End Patch
|
||||
@@ -0,0 +1 @@
|
||||
* -text
|
||||
@@ -0,0 +1 @@
|
||||
--preserve-crlf
|
||||
@@ -0,0 +1,4 @@
|
||||
one
|
||||
dos
|
||||
three
|
||||
cuatro
|
||||
@@ -0,0 +1,4 @@
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
@@ -0,0 +1,9 @@
|
||||
*** Begin Patch
|
||||
*** Update File: multi.txt
|
||||
@@
|
||||
-two
|
||||
+dos
|
||||
@@
|
||||
-four
|
||||
+cuatro
|
||||
*** End Patch
|
||||
@@ -5,6 +5,8 @@ This directory is a collection of end to end tests for the apply-patch specifica
|
||||
# Specification
|
||||
Each test case is one directory, composed of input state (input/), the patch operation (patch.txt), and the expected final state (expected/). This structure is designed to keep tests simple (i.e. test exactly one patch at a time) while still providing enough flexibility to test any given operation across files.
|
||||
|
||||
An optional `cli_flags.txt` may be provided with one CLI flag per line when a scenario needs non-default `apply_patch` arguments.
|
||||
|
||||
Here's what this would look like for a simple test apply-patch test case to create a new file:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use assert_cmd::Command;
|
||||
use codex_apply_patch::PRESERVE_CRLF_FLAG;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
@@ -89,3 +90,48 @@ fn test_apply_patch_cli_stdin_add_and_update() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_patch_cli_normalizes_crlf_without_flag() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let file = "crlf_default.txt";
|
||||
let absolute_path = tmp.path().join(file);
|
||||
fs::write(&absolute_path, b"one\r\ntwo\r\n")?;
|
||||
|
||||
let patch = format!(
|
||||
"*** Begin Patch\r\n*** Update File: {file}\r\n@@\r\n-one\r\n+uno\r\n two\r\n*** End Patch\r\n"
|
||||
);
|
||||
|
||||
apply_patch_command()?
|
||||
.arg(patch)
|
||||
.current_dir(tmp.path())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(format!("Success. Updated the following files:\nM {file}\n"));
|
||||
|
||||
assert_eq!(fs::read(absolute_path)?, b"uno\ntwo\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_patch_cli_preserves_crlf_with_flag() -> anyhow::Result<()> {
|
||||
let tmp = tempdir()?;
|
||||
let file = "crlf_flag.txt";
|
||||
let absolute_path = tmp.path().join(file);
|
||||
fs::write(&absolute_path, b"one\r\ntwo\r\n")?;
|
||||
|
||||
let patch = format!(
|
||||
"*** Begin Patch\r\n*** Update File: {file}\r\n@@\r\n-one\r\n+uno\r\n two\r\n*** End Patch\r\n"
|
||||
);
|
||||
|
||||
apply_patch_command()?
|
||||
.arg(PRESERVE_CRLF_FLAG)
|
||||
.arg(patch)
|
||||
.current_dir(tmp.path())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(format!("Success. Updated the following files:\nM {file}\n"));
|
||||
|
||||
assert_eq!(fs::read(absolute_path)?, b"uno\r\ntwo\r\n");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -38,14 +38,19 @@ fn run_apply_patch_scenario(dir: &Path) -> anyhow::Result<()> {
|
||||
|
||||
// Read the patch.txt file
|
||||
let patch = fs::read_to_string(dir.join("patch.txt"))?;
|
||||
let cli_flags = dir.join("cli_flags.txt");
|
||||
|
||||
// Run apply_patch in the temporary directory. We intentionally do not assert
|
||||
// on the exit status here; the scenarios are specified purely in terms of
|
||||
// final filesystem state, which we compare below.
|
||||
Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?)
|
||||
.arg(patch)
|
||||
.current_dir(tmp.path())
|
||||
.output()?;
|
||||
let mut command = Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?);
|
||||
if cli_flags.is_file() {
|
||||
let flags = fs::read_to_string(&cli_flags)?;
|
||||
for flag in flags.lines().map(str::trim).filter(|line| !line.is_empty()) {
|
||||
command.arg(flag);
|
||||
}
|
||||
}
|
||||
command.arg(patch).current_dir(tmp.path()).output()?;
|
||||
|
||||
// Assert that the final state matches the expected state exactly
|
||||
let expected_dir = dir.join("expected");
|
||||
|
||||
@@ -88,16 +88,36 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
|
||||
let argv1 = args.next().unwrap_or_default();
|
||||
if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 {
|
||||
let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned));
|
||||
let mut preserve_crlf = false;
|
||||
let patch_arg = match args.next() {
|
||||
Some(arg) if arg.to_str() == Some(codex_apply_patch::PRESERVE_CRLF_FLAG) => {
|
||||
preserve_crlf = true;
|
||||
args.next().and_then(|s| s.to_str().map(str::to_owned))
|
||||
}
|
||||
Some(arg) => arg.to_str().map(str::to_owned),
|
||||
None => None,
|
||||
};
|
||||
let exit_code = match patch_arg {
|
||||
Some(patch_arg) => {
|
||||
Some(patch_arg) if args.next().is_none() => {
|
||||
let mut stdout = std::io::stdout();
|
||||
let mut stderr = std::io::stderr();
|
||||
match codex_apply_patch::apply_patch(&patch_arg, &mut stdout, &mut stderr) {
|
||||
let options = codex_apply_patch::ApplyPatchOptions { preserve_crlf };
|
||||
match codex_apply_patch::apply_patch_with_options(
|
||||
&patch_arg,
|
||||
options,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
) {
|
||||
Ok(()) => 0,
|
||||
Err(_) => 1,
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
eprintln!(
|
||||
"Error: {CODEX_CORE_APPLY_PATCH_ARG1} accepts exactly one PATCH argument."
|
||||
);
|
||||
1
|
||||
}
|
||||
None => {
|
||||
eprintln!("Error: {CODEX_CORE_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
|
||||
1
|
||||
|
||||
@@ -332,6 +332,9 @@
|
||||
"default": null,
|
||||
"description": "Optional feature toggles scoped to this profile.",
|
||||
"properties": {
|
||||
"apply_patch_crlf": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"apply_patch_freeform": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1949,6 +1952,9 @@
|
||||
"default": null,
|
||||
"description": "Centralized feature flags (new). Prefer this over individual toggles.",
|
||||
"properties": {
|
||||
"apply_patch_crlf": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"apply_patch_freeform": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -5639,12 +5639,18 @@ fn write_fake_bwrap(contents: &str) -> tempfile::TempPath {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
// Bazel can mount the OS temp directory `noexec`, so prefer the current
|
||||
// working directory for fake executables and fall back to the default temp
|
||||
// dir outside that environment.
|
||||
let temp_file = std::env::current_dir()
|
||||
.ok()
|
||||
// Prefer Bazel's per-test temp dir when available; it is writable inside
|
||||
// the test sandbox and avoids creating fake executables under runfiles.
|
||||
// Fall back to the current working directory because the OS temp directory
|
||||
// can be mounted `noexec` under Bazel.
|
||||
let temp_file = std::env::var_os("TEST_TMPDIR")
|
||||
.map(std::path::PathBuf::from)
|
||||
.and_then(|dir| NamedTempFile::new_in(dir).ok())
|
||||
.or_else(|| {
|
||||
std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|dir| NamedTempFile::new_in(dir).ok())
|
||||
})
|
||||
.unwrap_or_else(|| NamedTempFile::new().expect("temp file"));
|
||||
// Linux rejects exec-ing a file that is still open for writing.
|
||||
let path = temp_file.into_temp_path();
|
||||
|
||||
@@ -31,6 +31,8 @@ use crate::tools::spec::JsonSchema;
|
||||
use async_trait::async_trait;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::ApplyPatchFileChange;
|
||||
use codex_apply_patch::ApplyPatchOptions;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
|
||||
@@ -171,8 +173,15 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
// Re-parse and verify the patch so we can compute changes and approval.
|
||||
// Avoid building temporary ExecParams/command vectors; derive directly from inputs.
|
||||
let cwd = turn.cwd.clone();
|
||||
let apply_patch_options = ApplyPatchOptions {
|
||||
preserve_crlf: turn.features.enabled(Feature::ApplyPatchCrlf),
|
||||
};
|
||||
let command = vec!["apply_patch".to_string(), patch_input.clone()];
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(&command, &cwd) {
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified_with_options(
|
||||
&command,
|
||||
&cwd,
|
||||
apply_patch_options,
|
||||
) {
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
let (file_paths, effective_additional_permissions, file_system_sandbox_policy) =
|
||||
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes).await;
|
||||
@@ -197,6 +206,7 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
|
||||
let req = ApplyPatchRequest {
|
||||
action: apply.action,
|
||||
preserve_crlf: turn.features.enabled(Feature::ApplyPatchCrlf),
|
||||
file_paths,
|
||||
changes,
|
||||
exec_approval_requirement: apply.exec_approval_requirement,
|
||||
@@ -268,7 +278,14 @@ pub(crate) async fn intercept_apply_patch(
|
||||
call_id: &str,
|
||||
tool_name: &str,
|
||||
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd) {
|
||||
let apply_patch_options = ApplyPatchOptions {
|
||||
preserve_crlf: turn.features.enabled(Feature::ApplyPatchCrlf),
|
||||
};
|
||||
match codex_apply_patch::maybe_parse_apply_patch_verified_with_options(
|
||||
command,
|
||||
cwd,
|
||||
apply_patch_options,
|
||||
) {
|
||||
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
|
||||
session
|
||||
.record_model_warning(
|
||||
@@ -300,6 +317,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
|
||||
let req = ApplyPatchRequest {
|
||||
action: apply.action,
|
||||
preserve_crlf: turn.features.enabled(Feature::ApplyPatchCrlf),
|
||||
file_paths: approval_keys,
|
||||
changes,
|
||||
exec_approval_requirement: apply.exec_approval_requirement,
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::tools::sandboxing::ToolRuntime;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_apply_patch::ApplyPatchAction;
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
use codex_apply_patch::PRESERVE_CRLF_FLAG;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
@@ -36,6 +37,7 @@ use std::path::PathBuf;
|
||||
#[derive(Debug)]
|
||||
pub struct ApplyPatchRequest {
|
||||
pub action: ApplyPatchAction,
|
||||
pub preserve_crlf: bool,
|
||||
pub file_paths: Vec<AbsolutePathBuf>,
|
||||
pub changes: std::collections::HashMap<PathBuf, FileChange>,
|
||||
pub exec_approval_requirement: ExecApprovalRequirement,
|
||||
@@ -84,12 +86,14 @@ impl ApplyPatchRuntime {
|
||||
})?
|
||||
}
|
||||
};
|
||||
let mut args = vec![CODEX_CORE_APPLY_PATCH_ARG1.to_string()];
|
||||
if req.preserve_crlf {
|
||||
args.push(PRESERVE_CRLF_FLAG.to_string());
|
||||
}
|
||||
args.push(req.action.patch.clone());
|
||||
Ok(SandboxCommand {
|
||||
program: exe.to_string_lossy().to_string(),
|
||||
args: vec![
|
||||
CODEX_CORE_APPLY_PATCH_ARG1.to_string(),
|
||||
req.action.patch.clone(),
|
||||
],
|
||||
args,
|
||||
cwd: req.action.cwd.clone(),
|
||||
// Run apply_patch with a minimal environment for determinism and to avoid leaks.
|
||||
env: HashMap::new(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use codex_protocol::protocol::GranularApprovalConfig;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn wants_no_sandbox_approval_granular_respects_sandbox_flag() {
|
||||
@@ -35,6 +36,7 @@ fn guardian_review_request_includes_patch_context() {
|
||||
let expected_patch = action.patch.clone();
|
||||
let request = ApplyPatchRequest {
|
||||
action,
|
||||
preserve_crlf: false,
|
||||
file_paths: vec![
|
||||
AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"),
|
||||
],
|
||||
@@ -67,3 +69,39 @@ fn guardian_review_request_includes_patch_context() {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_sandbox_command_includes_crlf_flag_when_requested() {
|
||||
let dir = tempdir().expect("tmp");
|
||||
let path = dir.path().join("a.txt");
|
||||
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
|
||||
let req = ApplyPatchRequest {
|
||||
action,
|
||||
preserve_crlf: true,
|
||||
file_paths: vec![AbsolutePathBuf::try_from(path.clone()).expect("abs path")],
|
||||
changes: HashMap::from([(
|
||||
path,
|
||||
FileChange::Add {
|
||||
content: "hello".to_string(),
|
||||
},
|
||||
)]),
|
||||
exec_approval_requirement: ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
proposed_execpolicy_amendment: None,
|
||||
},
|
||||
timeout_ms: None,
|
||||
codex_exe: None,
|
||||
additional_permissions: None,
|
||||
permissions_preapproved: false,
|
||||
};
|
||||
|
||||
let spec = ApplyPatchRuntime::build_sandbox_command(&req, dir.path()).expect("sandbox command");
|
||||
assert_eq!(
|
||||
spec.args,
|
||||
vec![
|
||||
CODEX_CORE_APPLY_PATCH_ARG1.to_string(),
|
||||
PRESERVE_CRLF_FLAG.to_string(),
|
||||
req.action.patch
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@ pub enum Feature {
|
||||
CodexHooks,
|
||||
/// Expose the built-in request_permissions tool.
|
||||
RequestPermissionsTool,
|
||||
/// Preserve CRLF line endings when applying CRLF patches.
|
||||
ApplyPatchCrlf,
|
||||
/// Allow the model to request web searches that fetch live content.
|
||||
WebSearchRequest,
|
||||
/// Allow the model to request web searches that fetch cached content.
|
||||
@@ -653,6 +655,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ApplyPatchCrlf,
|
||||
key: "apply_patch_crlf",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::UseLinuxSandboxBwrap,
|
||||
key: "use_linux_sandbox_bwrap",
|
||||
|
||||
Reference in New Issue
Block a user