Update test script in package.json and enhance patch_files.js with chroot support and improved path handling for file operations.
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* A self-contained JavaScript utility for applying human-readable
|
||||
* "pseudo-diff" patch files to a collection of text files.
|
||||
*
|
||||
|
||||
|
||||
|
||||
|
||||
const desc = `
|
||||
This is a custom utility that makes it more convenient to add, remove, move, or edit code files. 'apply_patch' effectively allows you to execute a diff/patch against a file,
|
||||
but the format of the diff specification is unique to this task, so pay careful attention to these instructions.
|
||||
To use the 'apply_patch' command, you should pass a message of the following structure as "input":
|
||||
|
||||
*** Begin Patch
|
||||
[YOUR_PATH]
|
||||
*** End Patch
|
||||
|
||||
|
||||
Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
|
||||
|
||||
*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
|
||||
@@ -24,7 +19,7 @@ For each snippet of code that needs to be changed, repeat the following:
|
||||
[context_after] -> See below for further instructions on context.
|
||||
|
||||
For instructions on [context_before] and [context_after]:
|
||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
|
||||
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first changes [context_after] lines in the second changes [context_before] lines.
|
||||
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
|
||||
@@ class BaseClass
|
||||
[3 lines of pre-context]
|
||||
@@ -32,7 +27,7 @@ For instructions on [context_before] and [context_after]:
|
||||
+ [new_code]
|
||||
[3 lines of post-context]
|
||||
|
||||
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
|
||||
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple '@@' statements to jump to the right context. For instance:
|
||||
|
||||
@@ class BaseClass
|
||||
@@ def method():
|
||||
@@ -42,12 +37,7 @@ For instructions on [context_before] and [context_after]:
|
||||
[3 lines of post-context]
|
||||
|
||||
Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
|
||||
|
||||
*
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
`;
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Domain objects
|
||||
// --------------------------------------------------------------------------- //
|
||||
@@ -109,16 +99,55 @@ class Patch {
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Path utilities for chroot functionality
|
||||
// --------------------------------------------------------------------------- //
|
||||
function normalizePath(path) {
|
||||
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
function joinPaths(...paths) {
|
||||
return normalizePath(paths.filter(p => p).join('/'));
|
||||
}
|
||||
|
||||
function resolvePath(chroot, filepath) {
|
||||
if (!chroot) return filepath;
|
||||
|
||||
// Remove leading slash from filepath if present
|
||||
const cleanFilepath = filepath.startsWith('/') ? filepath.substring(1) : filepath;
|
||||
|
||||
// Join chroot and filepath
|
||||
const resolved = joinPaths(chroot, cleanFilepath);
|
||||
|
||||
// Ensure it starts with /
|
||||
return resolved.startsWith('/') ? resolved : '/' + resolved;
|
||||
}
|
||||
|
||||
function unresolvePath(chroot, filepath) {
|
||||
if (!chroot) return filepath;
|
||||
|
||||
const chrootPath = chroot.startsWith('/') ? chroot : '/' + chroot;
|
||||
const cleanFilepath = filepath.startsWith('/') ? filepath : '/' + filepath;
|
||||
|
||||
if (cleanFilepath.startsWith(chrootPath)) {
|
||||
const relativePath = cleanFilepath.substring(chrootPath.length);
|
||||
return relativePath.startsWith('/') ? relativePath : '/' + relativePath;
|
||||
}
|
||||
|
||||
return filepath;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Patch text parser
|
||||
// --------------------------------------------------------------------------- //
|
||||
class Parser {
|
||||
constructor(current_files, lines) {
|
||||
constructor(current_files, lines, chroot = null) {
|
||||
this.current_files = current_files;
|
||||
this.lines = lines;
|
||||
this.index = 0;
|
||||
this.patch = new Patch();
|
||||
this.fuzz = 0;
|
||||
this.chroot = chroot;
|
||||
}
|
||||
|
||||
// ------------- low-level helpers -------------------------------------- //
|
||||
@@ -186,43 +215,52 @@ class Parser {
|
||||
// ---------- UPDATE ---------- //
|
||||
let path = this.read_str("*** Update File: ");
|
||||
if (path) {
|
||||
if (path in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate update for file: ${path}`);
|
||||
// Resolve path with chroot
|
||||
const resolvedPath = resolvePath(this.chroot, path);
|
||||
|
||||
if (resolvedPath in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate update for file: ${resolvedPath}`);
|
||||
}
|
||||
const move_to = this.read_str("*** Move to: ");
|
||||
if (!(path in this.current_files)) {
|
||||
throw new DiffError(`Update File Error - missing file: ${path}`);
|
||||
if (!(resolvedPath in this.current_files)) {
|
||||
throw new DiffError(`Update File Error - missing file: ${resolvedPath}`);
|
||||
}
|
||||
const text = this.current_files[path];
|
||||
const text = this.current_files[resolvedPath];
|
||||
const action = this._parse_update_file(text);
|
||||
action.move_path = move_to || null;
|
||||
this.patch.actions[path] = action;
|
||||
action.move_path = move_to ? resolvePath(this.chroot, move_to) : null;
|
||||
this.patch.actions[resolvedPath] = action;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------- DELETE ---------- //
|
||||
path = this.read_str("*** Delete File: ");
|
||||
if (path) {
|
||||
if (path in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate delete for file: ${path}`);
|
||||
// Resolve path with chroot
|
||||
const resolvedPath = resolvePath(this.chroot, path);
|
||||
|
||||
if (resolvedPath in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate delete for file: ${resolvedPath}`);
|
||||
}
|
||||
if (!(path in this.current_files)) {
|
||||
throw new DiffError(`Delete File Error - missing file: ${path}`);
|
||||
if (!(resolvedPath in this.current_files)) {
|
||||
throw new DiffError(`Delete File Error - missing file: ${resolvedPath}`);
|
||||
}
|
||||
this.patch.actions[path] = new PatchAction(ActionType.DELETE);
|
||||
this.patch.actions[resolvedPath] = new PatchAction(ActionType.DELETE);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------- ADD ---------- //
|
||||
path = this.read_str("*** Add File: ");
|
||||
if (path) {
|
||||
if (path in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate add for file: ${path}`);
|
||||
// Resolve path with chroot
|
||||
const resolvedPath = resolvePath(this.chroot, path);
|
||||
|
||||
if (resolvedPath in this.patch.actions) {
|
||||
throw new DiffError(`Duplicate add for file: ${resolvedPath}`);
|
||||
}
|
||||
if (path in this.current_files) {
|
||||
throw new DiffError(`Add File Error - file already exists: ${path}`);
|
||||
if (resolvedPath in this.current_files) {
|
||||
throw new DiffError(`Add File Error - file already exists: ${resolvedPath}`);
|
||||
}
|
||||
this.patch.actions[path] = this._parse_add_file();
|
||||
this.patch.actions[resolvedPath] = this._parse_add_file();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -512,7 +550,7 @@ function _get_updated_file(text, action, path) {
|
||||
return dest_lines.join("\n");
|
||||
}
|
||||
|
||||
function patch_to_commit(patch, orig) {
|
||||
function patch_to_commit(patch, orig, chroot = null) {
|
||||
const commit = new Commit();
|
||||
for (const [path, action] of Object.entries(patch.actions)) {
|
||||
if (action.type === ActionType.DELETE) {
|
||||
@@ -534,11 +572,12 @@ function patch_to_commit(patch, orig) {
|
||||
);
|
||||
} else if (action.type === ActionType.UPDATE) {
|
||||
const new_content = _get_updated_file(orig[path], action, path);
|
||||
const move_path = action.move_path ? unresolvePath(chroot, action.move_path) : null;
|
||||
commit.changes[path] = new FileChange(
|
||||
ActionType.UPDATE,
|
||||
orig[path],
|
||||
new_content,
|
||||
action.move_path
|
||||
move_path
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -548,7 +587,7 @@ function patch_to_commit(patch, orig) {
|
||||
// --------------------------------------------------------------------------- //
|
||||
// User-facing helpers
|
||||
// --------------------------------------------------------------------------- //
|
||||
function text_to_patch(text, orig) {
|
||||
function text_to_patch(text, orig, chroot = null) {
|
||||
// Handle different line ending scenarios
|
||||
let lines = text.split(/\r?\n/);
|
||||
|
||||
@@ -574,13 +613,13 @@ function text_to_patch(text, orig) {
|
||||
throw new DiffError("Invalid patch text - missing sentinels");
|
||||
}
|
||||
|
||||
const parser = new Parser(orig, lines);
|
||||
const parser = new Parser(orig, lines, chroot);
|
||||
parser.index = 1;
|
||||
parser.parse();
|
||||
return [parser.patch, parser.fuzz];
|
||||
}
|
||||
|
||||
function identify_files_needed(text) {
|
||||
function identify_files_needed(text, chroot = null) {
|
||||
// Handle line splitting consistently
|
||||
let lines = text.split(/\r?\n/);
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
@@ -589,16 +628,16 @@ function identify_files_needed(text) {
|
||||
|
||||
const updateFiles = lines
|
||||
.filter(line => line.startsWith("*** Update File: "))
|
||||
.map(line => line.substring("*** Update File: ".length));
|
||||
.map(line => resolvePath(chroot, line.substring("*** Update File: ".length)));
|
||||
|
||||
const deleteFiles = lines
|
||||
.filter(line => line.startsWith("*** Delete File: "))
|
||||
.map(line => line.substring("*** Delete File: ".length));
|
||||
.map(line => resolvePath(chroot, line.substring("*** Delete File: ".length)));
|
||||
|
||||
return [...updateFiles, ...deleteFiles];
|
||||
}
|
||||
|
||||
function identify_files_added(text) {
|
||||
function identify_files_added(text, chroot = null) {
|
||||
// Handle line splitting consistently
|
||||
let lines = text.split(/\r?\n/);
|
||||
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
||||
@@ -607,7 +646,7 @@ function identify_files_added(text) {
|
||||
|
||||
return lines
|
||||
.filter(line => line.startsWith("*** Add File: "))
|
||||
.map(line => line.substring("*** Add File: ".length));
|
||||
.map(line => resolvePath(chroot, line.substring("*** Add File: ".length)));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
@@ -621,7 +660,7 @@ function load_files(paths, open_fn) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function apply_commit(commit, write_fn, remove_fn) {
|
||||
function apply_commit(commit, write_fn, remove_fn, chroot = null) {
|
||||
for (const [path, change] of Object.entries(commit.changes)) {
|
||||
if (change.type === ActionType.DELETE) {
|
||||
remove_fn(path);
|
||||
@@ -634,7 +673,7 @@ function apply_commit(commit, write_fn, remove_fn) {
|
||||
if (change.new_content === null) {
|
||||
throw new DiffError(`UPDATE change for ${path} has no new content`);
|
||||
}
|
||||
const target = change.move_path || path;
|
||||
const target = change.move_path ? resolvePath(chroot, change.move_path) : path;
|
||||
write_fn(target, change.new_content);
|
||||
if (change.move_path) {
|
||||
remove_fn(path);
|
||||
@@ -643,15 +682,15 @@ function apply_commit(commit, write_fn, remove_fn) {
|
||||
}
|
||||
}
|
||||
|
||||
function process_patch(text, open_fn, write_fn, remove_fn) {
|
||||
function process_patch(text, open_fn, write_fn, remove_fn, chroot = null) {
|
||||
if (!text.startsWith("*** Begin Patch")) {
|
||||
throw new DiffError("Patch text must start with *** Begin Patch");
|
||||
}
|
||||
const paths = identify_files_needed(text);
|
||||
const paths = identify_files_needed(text, chroot);
|
||||
const orig = load_files(paths, open_fn);
|
||||
const [patch, _fuzz] = text_to_patch(text, orig);
|
||||
const commit = patch_to_commit(patch, orig);
|
||||
apply_commit(commit, write_fn, remove_fn);
|
||||
const [patch, _fuzz] = text_to_patch(text, orig, chroot);
|
||||
const commit = patch_to_commit(patch, orig, chroot);
|
||||
apply_commit(commit, write_fn, remove_fn, chroot);
|
||||
return "Done!";
|
||||
}
|
||||
|
||||
@@ -683,13 +722,13 @@ function remove_file(filepath) {
|
||||
export default {
|
||||
type: "function",
|
||||
name: "patch_files",
|
||||
description: "Apply a unified diff patch to files within a chroot directory, with option to reverse the patch",
|
||||
description: "Apply a unified diff patch " + desc,
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
patch: {
|
||||
type: "string",
|
||||
description: "The unidiff patch string to apply.",
|
||||
description: "The patch string to apply.",
|
||||
}
|
||||
},
|
||||
required: ["patch"],
|
||||
@@ -706,7 +745,8 @@ export async function run(args) {
|
||||
args.patch,
|
||||
open_file,
|
||||
write_file,
|
||||
remove_file
|
||||
remove_file,
|
||||
'/home/seb/src/aiTools/tmp'
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -715,4 +755,4 @@ export async function run(args) {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user