mirror of
https://github.com/openai/codex.git
synced 2026-02-03 07:23:39 +00:00
Compare commits
6 Commits
codex-work
...
dh--apply-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ce70dbb5c | ||
|
|
961270022d | ||
|
|
28e8306f6a | ||
|
|
14f85c1d31 | ||
|
|
050382098a | ||
|
|
badd55aae3 |
@@ -383,9 +383,12 @@ fn compute_replacements(
|
||||
let mut line_index: usize = 0;
|
||||
|
||||
for chunk in chunks {
|
||||
// If a chunk has a `change_context`, we use seek_sequence to find it, then
|
||||
// adjust our `line_index` to continue from there.
|
||||
if let Some(ctx_line) = &chunk.change_context {
|
||||
// If a chunk has one or more `change_context` lines, seek them in order
|
||||
// to progressively narrow down the position of the chunk. This supports
|
||||
// multiple @@ context headers such as:
|
||||
// @@ class BaseClass:
|
||||
// @@ def method():
|
||||
for ctx_line in &chunk.change_context {
|
||||
if let Some(idx) = seek_sequence::seek_sequence(
|
||||
original_lines,
|
||||
std::slice::from_ref(ctx_line),
|
||||
@@ -824,6 +827,51 @@ mod tests {
|
||||
assert_eq!(String::from_utf8(stderr).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_patch_multiple_change_contexts_success() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("example.py");
|
||||
let original = r#"
|
||||
class BaseClass:
|
||||
def method():
|
||||
# untouched
|
||||
pass
|
||||
|
||||
class OtherClass:
|
||||
def method():
|
||||
# to_remove
|
||||
pass
|
||||
"#;
|
||||
fs::write(&path, original).unwrap();
|
||||
|
||||
let patch = wrap_patch(&format!(
|
||||
r#"*** Update File: {}
|
||||
@@ class OtherClass:
|
||||
@@ def method():
|
||||
- # to_remove
|
||||
+ # to_add"#,
|
||||
path.display()
|
||||
));
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
apply_patch(&patch, &mut stdout, &mut stderr).unwrap();
|
||||
|
||||
let contents = fs::read_to_string(&path).unwrap();
|
||||
let expected = r#"
|
||||
class BaseClass:
|
||||
def method():
|
||||
# untouched
|
||||
pass
|
||||
|
||||
class OtherClass:
|
||||
def method():
|
||||
# to_add
|
||||
pass
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unified_diff() {
|
||||
// Start with a file containing four lines.
|
||||
@@ -1062,4 +1110,40 @@ g
|
||||
let result = apply_patch(&patch, &mut stdout, &mut stderr);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_patch_multiple_change_contexts_missing_context() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("example.py");
|
||||
let original = r#"class BaseClass:
|
||||
def method():
|
||||
# to_remove
|
||||
pass
|
||||
|
||||
class OtherClass:
|
||||
def method():
|
||||
# untouched
|
||||
pass"#;
|
||||
fs::write(&path, original).unwrap();
|
||||
|
||||
let patch = wrap_patch(&format!(
|
||||
r#"*** Update File: {}
|
||||
@@ class BaseClass:
|
||||
@@ def missing():
|
||||
- # to_remove
|
||||
+ # to_add"#,
|
||||
path.display()
|
||||
));
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let result = apply_patch(&patch, &mut stdout, &mut stderr);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ApplyPatchError::IoError(IoError { context, .. }))
|
||||
if context.contains("Failed to find context ' def missing():'")
|
||||
&& context.contains(&path.display().to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,12 +89,13 @@ use Hunk::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct UpdateFileChunk {
|
||||
/// A single line of context used to narrow down the position of the chunk
|
||||
/// (this is usually a class, method, or function definition.)
|
||||
pub change_context: Option<String>,
|
||||
/// Lines of context used to narrow down the position of the chunk.
|
||||
/// These are usually class, method, or function definitions. If empty,
|
||||
/// the chunk has no explicit context.
|
||||
pub change_context: Vec<String>,
|
||||
|
||||
/// A contiguous block of lines that should be replaced with `new_lines`.
|
||||
/// `old_lines` must occur strictly after `change_context`.
|
||||
/// `old_lines` must occur strictly after the last `change_context` line.
|
||||
pub old_lines: Vec<String>,
|
||||
pub new_lines: Vec<String>,
|
||||
|
||||
@@ -351,28 +352,42 @@ fn parse_update_file_chunk(
|
||||
line_number,
|
||||
});
|
||||
}
|
||||
// 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 {
|
||||
(None, 1)
|
||||
} else if let Some(context) = lines[0].strip_prefix(CHANGE_CONTEXT_MARKER) {
|
||||
(Some(context.to_string()), 1)
|
||||
} else {
|
||||
if !allow_missing_context {
|
||||
return Err(InvalidHunkError {
|
||||
message: format!(
|
||||
"Expected update hunk to start with a @@ context marker, got: '{}'",
|
||||
lines[0]
|
||||
),
|
||||
line_number,
|
||||
});
|
||||
// Consume one or more explicit context markers (`@@` or `@@ <context>`). This
|
||||
// supports multiple `@@` lines to narrow down the target location, e.g.:
|
||||
// @@ class BaseClass:
|
||||
// @@ def method():
|
||||
// -old
|
||||
// +new
|
||||
let mut change_context: Vec<String> = Vec::new();
|
||||
let mut start_index = 0;
|
||||
while start_index < lines.len() {
|
||||
let line = lines[start_index];
|
||||
if line == EMPTY_CHANGE_CONTEXT_MARKER {
|
||||
start_index += 1;
|
||||
continue;
|
||||
}
|
||||
(None, 0)
|
||||
};
|
||||
if let Some(context) = line.strip_prefix(CHANGE_CONTEXT_MARKER) {
|
||||
change_context.push(context.to_string());
|
||||
start_index += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if start_index == 0 && !allow_missing_context {
|
||||
return Err(InvalidHunkError {
|
||||
message: format!(
|
||||
"Expected update hunk to start with a @@ context marker, got: '{}'",
|
||||
lines[0]
|
||||
),
|
||||
line_number,
|
||||
});
|
||||
}
|
||||
|
||||
if start_index >= lines.len() {
|
||||
return Err(InvalidHunkError {
|
||||
message: "Update hunk does not contain any lines".to_string(),
|
||||
line_number: line_number + 1,
|
||||
line_number: line_number + start_index,
|
||||
});
|
||||
}
|
||||
let mut chunk = UpdateFileChunk {
|
||||
@@ -517,7 +532,7 @@ fn test_parse_patch() {
|
||||
path: PathBuf::from("path/update.py"),
|
||||
move_path: Some(PathBuf::from("path/update2.py")),
|
||||
chunks: vec![UpdateFileChunk {
|
||||
change_context: Some("def f():".to_string()),
|
||||
change_context: vec!["def f():".to_string()],
|
||||
old_lines: vec![" pass".to_string()],
|
||||
new_lines: vec![" return 123".to_string()],
|
||||
is_end_of_file: false
|
||||
@@ -544,7 +559,7 @@ fn test_parse_patch() {
|
||||
path: PathBuf::from("file.py"),
|
||||
move_path: None,
|
||||
chunks: vec![UpdateFileChunk {
|
||||
change_context: None,
|
||||
change_context: Vec::new(),
|
||||
old_lines: vec![],
|
||||
new_lines: vec!["line".to_string()],
|
||||
is_end_of_file: false
|
||||
@@ -574,7 +589,7 @@ fn test_parse_patch() {
|
||||
path: PathBuf::from("file2.py"),
|
||||
move_path: None,
|
||||
chunks: vec![UpdateFileChunk {
|
||||
change_context: None,
|
||||
change_context: Vec::new(),
|
||||
old_lines: vec!["import foo".to_string()],
|
||||
new_lines: vec!["import foo".to_string(), "bar".to_string()],
|
||||
is_end_of_file: false,
|
||||
@@ -594,7 +609,7 @@ fn test_parse_patch_lenient() {
|
||||
path: PathBuf::from("file2.py"),
|
||||
move_path: None,
|
||||
chunks: vec![UpdateFileChunk {
|
||||
change_context: None,
|
||||
change_context: Vec::new(),
|
||||
old_lines: vec!["import foo".to_string()],
|
||||
new_lines: vec!["import foo".to_string(), "bar".to_string()],
|
||||
is_end_of_file: false,
|
||||
@@ -730,7 +745,7 @@ fn test_update_file_chunk() {
|
||||
),
|
||||
Ok((
|
||||
(UpdateFileChunk {
|
||||
change_context: Some("change_context".to_string()),
|
||||
change_context: vec!["change_context".to_string()],
|
||||
old_lines: vec![
|
||||
"".to_string(),
|
||||
"context".to_string(),
|
||||
@@ -752,7 +767,7 @@ fn test_update_file_chunk() {
|
||||
parse_update_file_chunk(&["@@", "+line", "*** End of File"], 123, false),
|
||||
Ok((
|
||||
(UpdateFileChunk {
|
||||
change_context: None,
|
||||
change_context: Vec::new(),
|
||||
old_lines: vec![],
|
||||
new_lines: vec!["line".to_string()],
|
||||
is_end_of_file: true
|
||||
@@ -761,3 +776,31 @@ fn test_update_file_chunk() {
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_file_chunk_multiple_change_context_lines() {
|
||||
assert_eq!(
|
||||
parse_update_file_chunk(
|
||||
&[
|
||||
"@@ class BaseClass:",
|
||||
"@@ def method():",
|
||||
"- # to_remove",
|
||||
"+ # to_add",
|
||||
],
|
||||
200,
|
||||
false
|
||||
),
|
||||
Ok((
|
||||
(UpdateFileChunk {
|
||||
change_context: vec![
|
||||
"class BaseClass:".to_string(),
|
||||
" def method():".to_string()
|
||||
],
|
||||
old_lines: vec![" # to_remove".to_string()],
|
||||
new_lines: vec![" # to_add".to_string()],
|
||||
is_end_of_file: false
|
||||
}),
|
||||
4
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/expected/first.py
vendored
Normal file
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/expected/first.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class BaseClass:
|
||||
def method(self):
|
||||
# to_add
|
||||
pass
|
||||
|
||||
|
||||
class OtherClass:
|
||||
def method(self):
|
||||
# untouched
|
||||
pass
|
||||
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/expected/second.py
vendored
Normal file
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/expected/second.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class BaseClass:
|
||||
def method(self):
|
||||
# untouched
|
||||
pass
|
||||
|
||||
|
||||
class OtherClass:
|
||||
def method(self):
|
||||
# to_add
|
||||
pass
|
||||
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/input/first.py
vendored
Normal file
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/input/first.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class BaseClass:
|
||||
def method(self):
|
||||
# to_remove
|
||||
pass
|
||||
|
||||
|
||||
class OtherClass:
|
||||
def method(self):
|
||||
# untouched
|
||||
pass
|
||||
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/input/second.py
vendored
Normal file
10
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/input/second.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class BaseClass:
|
||||
def method(self):
|
||||
# untouched
|
||||
pass
|
||||
|
||||
|
||||
class OtherClass:
|
||||
def method(self):
|
||||
# to_remove
|
||||
pass
|
||||
12
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/patch.txt
vendored
Normal file
12
codex-rs/apply-patch/tests/fixtures/scenarios/024_multiple_context_lines/patch.txt
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
*** Begin Patch
|
||||
*** Update File: first.py
|
||||
@@ class BaseClass:
|
||||
@@ def method(self):
|
||||
- # to_remove
|
||||
+ # to_add
|
||||
*** Update File: second.py
|
||||
@@ class OtherClass:
|
||||
@@ def method(self):
|
||||
- # to_remove
|
||||
+ # to_add
|
||||
*** End Patch
|
||||
@@ -0,0 +1,5 @@
|
||||
foo
|
||||
bar
|
||||
bar
|
||||
bar
|
||||
new
|
||||
@@ -0,0 +1,5 @@
|
||||
foo
|
||||
bar
|
||||
bar
|
||||
bar
|
||||
baz
|
||||
7
codex-rs/apply-patch/tests/fixtures/scenarios/025_multiple_context_lines_subset/patch.txt
vendored
Normal file
7
codex-rs/apply-patch/tests/fixtures/scenarios/025_multiple_context_lines_subset/patch.txt
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*** Begin Patch
|
||||
*** Update File: foo.txt
|
||||
@@ bar
|
||||
@@ bar
|
||||
-baz
|
||||
+new
|
||||
*** End Patch
|
||||
@@ -0,0 +1,6 @@
|
||||
foo
|
||||
bar
|
||||
one
|
||||
bar
|
||||
bar
|
||||
two
|
||||
@@ -0,0 +1,6 @@
|
||||
foo
|
||||
bar
|
||||
bar
|
||||
bar
|
||||
bar
|
||||
baz
|
||||
11
codex-rs/apply-patch/tests/fixtures/scenarios/026_multiple_context_lines_overlap/patch.txt
vendored
Normal file
11
codex-rs/apply-patch/tests/fixtures/scenarios/026_multiple_context_lines_overlap/patch.txt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
*** Begin Patch
|
||||
*** Update File: foo.txt
|
||||
@@ foo
|
||||
@@ bar
|
||||
-bar
|
||||
+one
|
||||
@@ bar
|
||||
@@ bar
|
||||
-baz
|
||||
+two
|
||||
*** End Patch
|
||||
@@ -0,0 +1,4 @@
|
||||
class Foo:
|
||||
def foo(self):
|
||||
# to_remove
|
||||
pass
|
||||
@@ -0,0 +1,10 @@
|
||||
class BaseClass:
|
||||
def method(self):
|
||||
# to_add
|
||||
pass
|
||||
|
||||
|
||||
class OtherClass:
|
||||
def method(self):
|
||||
# untouched
|
||||
pass
|
||||
@@ -0,0 +1,4 @@
|
||||
class Foo:
|
||||
def foo(self):
|
||||
# to_remove
|
||||
pass
|
||||
@@ -0,0 +1,10 @@
|
||||
class BaseClass:
|
||||
def method(self):
|
||||
# to_add
|
||||
pass
|
||||
|
||||
|
||||
class OtherClass:
|
||||
def method(self):
|
||||
# untouched
|
||||
pass
|
||||
@@ -0,0 +1,12 @@
|
||||
*** Begin Patch
|
||||
*** Update File: success.py
|
||||
@@ class BaseClass:
|
||||
@@ def method(self):
|
||||
- # to_remove
|
||||
+ # to_add
|
||||
*** Update File: failure.py
|
||||
@@ class Foo:
|
||||
@@ def missing(self):
|
||||
- # to_remove
|
||||
+ # to_add
|
||||
*** End Patch
|
||||
@@ -0,0 +1,5 @@
|
||||
class Foo:
|
||||
# this is a comment
|
||||
def foo(self):
|
||||
# to_add
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
class Foo:
|
||||
# this is a comment
|
||||
def foo(self):
|
||||
# to_remove
|
||||
pass
|
||||
7
codex-rs/apply-patch/tests/fixtures/scenarios/028_multiple_context_lines_skipped/patch.txt
vendored
Normal file
7
codex-rs/apply-patch/tests/fixtures/scenarios/028_multiple_context_lines_skipped/patch.txt
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*** Begin Patch
|
||||
*** Update File: skip.py
|
||||
@@ class Foo:
|
||||
@@ def foo(self):
|
||||
- # to_remove
|
||||
+ # to_add
|
||||
*** End Patch
|
||||
Reference in New Issue
Block a user