759 lines
25 KiB
JavaScript
759 lines
25 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
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.
|
|
For each snippet of code that needs to be changed, repeat the following:
|
|
[context_before] -> See below for further instructions on context.
|
|
- [old_code] -> Precede the old code with a minus sign.
|
|
+ [new_code] -> Precede the new, replacement code with a plus sign.
|
|
[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 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]
|
|
- [old_code]
|
|
+ [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:
|
|
|
|
@@ class BaseClass
|
|
@@ def method():
|
|
[3 lines of pre-context]
|
|
- [old_code]
|
|
+ [new_code]
|
|
[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
|
|
// --------------------------------------------------------------------------- //
|
|
class ActionType {
|
|
static ADD = "add";
|
|
static DELETE = "delete";
|
|
static UPDATE = "update";
|
|
}
|
|
|
|
class FileChange {
|
|
constructor(type, old_content = null, new_content = null, move_path = null) {
|
|
this.type = type;
|
|
this.old_content = old_content;
|
|
this.new_content = new_content;
|
|
this.move_path = move_path;
|
|
}
|
|
}
|
|
|
|
class Commit {
|
|
constructor() {
|
|
this.changes = {};
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// Exceptions
|
|
// --------------------------------------------------------------------------- //
|
|
class DiffError extends Error {
|
|
/** Any problem detected while parsing or applying a patch. */
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "DiffError";
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// Helper classes used while parsing patches
|
|
// --------------------------------------------------------------------------- //
|
|
class Chunk {
|
|
constructor(orig_index = -1, del_lines = [], ins_lines = []) {
|
|
this.orig_index = orig_index;
|
|
this.del_lines = del_lines;
|
|
this.ins_lines = ins_lines;
|
|
}
|
|
}
|
|
|
|
class PatchAction {
|
|
constructor(type, new_file = null, chunks = [], move_path = null) {
|
|
this.type = type;
|
|
this.new_file = new_file;
|
|
this.chunks = chunks;
|
|
this.move_path = move_path;
|
|
}
|
|
}
|
|
|
|
class Patch {
|
|
constructor() {
|
|
this.actions = {};
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// 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, 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 -------------------------------------- //
|
|
_cur_line() {
|
|
if (this.index >= this.lines.length) {
|
|
throw new DiffError("Unexpected end of input while parsing patch");
|
|
}
|
|
return this.lines[this.index];
|
|
}
|
|
|
|
static _norm(line) {
|
|
/** Strip CR so comparisons work for both LF and CRLF input. */
|
|
return line.replace(/\r$/, "");
|
|
}
|
|
|
|
// ------------- scanning convenience ----------------------------------- //
|
|
is_done(prefixes = null) {
|
|
if (this.index >= this.lines.length) {
|
|
return true;
|
|
}
|
|
if (
|
|
prefixes &&
|
|
prefixes.length > 0
|
|
) {
|
|
const currentLine = Parser._norm(this._cur_line());
|
|
for (const prefix of prefixes) {
|
|
if (currentLine.startsWith(prefix)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
startswith(prefix) {
|
|
return Parser._norm(this._cur_line()).startsWith(prefix);
|
|
}
|
|
|
|
read_str(prefix) {
|
|
/**
|
|
* Consume the current line if it starts with *prefix* and return the text
|
|
* **after** the prefix. Raises if prefix is empty.
|
|
*/
|
|
if (prefix === "") {
|
|
throw new Error("read_str() requires a non-empty prefix");
|
|
}
|
|
if (Parser._norm(this._cur_line()).startsWith(prefix)) {
|
|
const text = this._cur_line().substring(prefix.length);
|
|
this.index += 1;
|
|
return text;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
read_line() {
|
|
/** Return the current raw line and advance. */
|
|
const line = this._cur_line();
|
|
this.index += 1;
|
|
return line;
|
|
}
|
|
|
|
// ------------- public entry point -------------------------------------- //
|
|
parse() {
|
|
while (!this.is_done(["*** End Patch"])) {
|
|
// ---------- UPDATE ---------- //
|
|
let path = this.read_str("*** Update File: ");
|
|
if (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 (!(resolvedPath in this.current_files)) {
|
|
throw new DiffError(`Update File Error - missing file: ${resolvedPath}`);
|
|
}
|
|
const text = this.current_files[resolvedPath];
|
|
const action = this._parse_update_file(text);
|
|
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) {
|
|
// 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 (!(resolvedPath in this.current_files)) {
|
|
throw new DiffError(`Delete File Error - missing file: ${resolvedPath}`);
|
|
}
|
|
this.patch.actions[resolvedPath] = new PatchAction(ActionType.DELETE);
|
|
continue;
|
|
}
|
|
|
|
// ---------- ADD ---------- //
|
|
path = this.read_str("*** Add File: ");
|
|
if (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 (resolvedPath in this.current_files) {
|
|
throw new DiffError(`Add File Error - file already exists: ${resolvedPath}`);
|
|
}
|
|
this.patch.actions[resolvedPath] = this._parse_add_file();
|
|
continue;
|
|
}
|
|
|
|
throw new DiffError(`Unknown line while parsing: ${this._cur_line()}`);
|
|
}
|
|
|
|
if (!this.startswith("*** End Patch")) {
|
|
throw new DiffError("Missing *** End Patch sentinel");
|
|
}
|
|
this.index += 1; // consume sentinel
|
|
}
|
|
|
|
// ------------- section parsers ---------------------------------------- //
|
|
_parse_update_file(text) {
|
|
const action = new PatchAction(ActionType.UPDATE);
|
|
const lines = text.split("\n");
|
|
let index = 0;
|
|
while (!this.is_done([
|
|
"*** End Patch",
|
|
"*** Update File:",
|
|
"*** Delete File:",
|
|
"*** Add File:",
|
|
"*** End of File",
|
|
])) {
|
|
const def_str = this.read_str("@@ ");
|
|
let section_str = "";
|
|
if (!def_str && Parser._norm(this._cur_line()) === "@@") {
|
|
section_str = this.read_line();
|
|
}
|
|
|
|
if (!(def_str || section_str || index === 0)) {
|
|
throw new DiffError(`Invalid line in update section:\n${this._cur_line()}`);
|
|
}
|
|
|
|
if (def_str && def_str.trim()) {
|
|
let found = false;
|
|
if (!lines.slice(0, index).includes(def_str)) {
|
|
for (let i = index; i < lines.length; i++) {
|
|
if (lines[i] === def_str) {
|
|
index = i + 1;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!found && !lines.slice(0, index).map(s => s.trim()).includes(def_str.trim())) {
|
|
for (let i = index; i < lines.length; i++) {
|
|
if (lines[i].trim() === def_str.trim()) {
|
|
index = i + 1;
|
|
this.fuzz += 1;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const [next_ctx, chunks, end_idx, eof] = peek_next_section(this.lines, this.index);
|
|
const [new_index, fuzz] = find_context(lines, next_ctx, index, eof);
|
|
if (new_index === -1) {
|
|
const ctx_txt = next_ctx.join("\n");
|
|
throw new DiffError(
|
|
`Invalid ${eof ? 'EOF ' : ''}context at ${index}:\n${ctx_txt}`
|
|
);
|
|
}
|
|
this.fuzz += fuzz;
|
|
for (const ch of chunks) {
|
|
ch.orig_index += new_index;
|
|
action.chunks.push(ch);
|
|
}
|
|
index = new_index + next_ctx.length;
|
|
this.index = end_idx;
|
|
}
|
|
return action;
|
|
}
|
|
|
|
_parse_add_file() {
|
|
const lines = [];
|
|
while (!this.is_done([
|
|
"*** End Patch",
|
|
"*** Update File:",
|
|
"*** Delete File:",
|
|
"*** Add File:"
|
|
])) {
|
|
const s = this.read_line();
|
|
if (!s.startsWith("+")) {
|
|
throw new DiffError(`Invalid Add File line (missing '+'): ${s}`);
|
|
}
|
|
lines.push(s.substring(1)); // strip leading '+'
|
|
}
|
|
return new PatchAction(ActionType.ADD, lines.join("\n"));
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// Helper functions
|
|
// --------------------------------------------------------------------------- //
|
|
function find_context_core(lines, context, start) {
|
|
if (!context || context.length === 0) {
|
|
return [start, 0];
|
|
}
|
|
|
|
// Exact match
|
|
for (let i = start; i <= lines.length - context.length; i++) {
|
|
let match = true;
|
|
for (let j = 0; j < context.length; j++) {
|
|
if (lines[i + j] !== context[j]) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
return [i, 0];
|
|
}
|
|
}
|
|
|
|
// Rstrip match
|
|
for (let i = start; i <= lines.length - context.length; i++) {
|
|
let match = true;
|
|
for (let j = 0; j < context.length; j++) {
|
|
if (lines[i + j].replace(/\s+$/, "") !== context[j].replace(/\s+$/, "")) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
return [i, 1];
|
|
}
|
|
}
|
|
|
|
// Trim match
|
|
for (let i = start; i <= lines.length - context.length; i++) {
|
|
let match = true;
|
|
for (let j = 0; j < context.length; j++) {
|
|
if (lines[i + j].trim() !== context[j].trim()) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) {
|
|
return [i, 100];
|
|
}
|
|
}
|
|
|
|
return [-1, 0];
|
|
}
|
|
|
|
function find_context(lines, context, start, eof) {
|
|
if (eof) {
|
|
let [new_index, fuzz] = find_context_core(lines, context, Math.max(0, lines.length - context.length));
|
|
if (new_index !== -1) {
|
|
return [new_index, fuzz];
|
|
}
|
|
[new_index, fuzz] = find_context_core(lines, context, start);
|
|
return [new_index, fuzz + 10000];
|
|
}
|
|
return find_context_core(lines, context, start);
|
|
}
|
|
|
|
function peek_next_section(lines, index) {
|
|
const old = [];
|
|
let del_lines = [];
|
|
let ins_lines = [];
|
|
const chunks = [];
|
|
let mode = "keep";
|
|
const orig_index = index;
|
|
|
|
while (index < lines.length) {
|
|
let s = lines[index];
|
|
if (s.startsWith("@@") ||
|
|
s === "*** End Patch" ||
|
|
s.startsWith("*** Update File:") ||
|
|
s.startsWith("*** Delete File:") ||
|
|
s.startsWith("*** Add File:") ||
|
|
s === "*** End of File") {
|
|
break;
|
|
}
|
|
if (s === "***") {
|
|
break;
|
|
}
|
|
if (s.startsWith("***")) {
|
|
throw new DiffError(`Invalid Line: ${s}`);
|
|
}
|
|
index += 1;
|
|
|
|
const last_mode = mode;
|
|
if (s === "") {
|
|
s = " ";
|
|
}
|
|
if (s[0] === "+") {
|
|
mode = "add";
|
|
} else if (s[0] === "-") {
|
|
mode = "delete";
|
|
} else if (s[0] === " ") {
|
|
mode = "keep";
|
|
} else {
|
|
throw new DiffError(`Invalid Line: ${s}`);
|
|
}
|
|
s = s.substring(1);
|
|
|
|
if (mode === "keep" && last_mode !== mode) {
|
|
if (ins_lines.length > 0 || del_lines.length > 0) {
|
|
chunks.push(
|
|
new Chunk(
|
|
old.length - del_lines.length,
|
|
[...del_lines],
|
|
[...ins_lines]
|
|
)
|
|
);
|
|
}
|
|
del_lines = [];
|
|
ins_lines = [];
|
|
}
|
|
|
|
if (mode === "delete") {
|
|
del_lines.push(s);
|
|
old.push(s);
|
|
} else if (mode === "add") {
|
|
ins_lines.push(s);
|
|
} else if (mode === "keep") {
|
|
old.push(s);
|
|
}
|
|
}
|
|
|
|
if (ins_lines.length > 0 || del_lines.length > 0) {
|
|
chunks.push(
|
|
new Chunk(
|
|
old.length - del_lines.length,
|
|
[...del_lines],
|
|
[...ins_lines]
|
|
)
|
|
);
|
|
}
|
|
|
|
if (index < lines.length && lines[index] === "*** End of File") {
|
|
index += 1;
|
|
return [old, chunks, index, true];
|
|
}
|
|
|
|
if (index === orig_index) {
|
|
throw new DiffError("Nothing in this section");
|
|
}
|
|
return [old, chunks, index, false];
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// Patch → Commit and Commit application
|
|
// --------------------------------------------------------------------------- //
|
|
function _get_updated_file(text, action, path) {
|
|
if (action.type !== ActionType.UPDATE) {
|
|
throw new DiffError("_get_updated_file called with non-update action");
|
|
}
|
|
const orig_lines = text.split("\n");
|
|
const dest_lines = [];
|
|
let orig_index = 0;
|
|
|
|
for (const chunk of action.chunks) {
|
|
if (chunk.orig_index > orig_lines.length) {
|
|
throw new DiffError(
|
|
`${path}: chunk.orig_index ${chunk.orig_index} exceeds file length`
|
|
);
|
|
}
|
|
if (orig_index > chunk.orig_index) {
|
|
throw new DiffError(
|
|
`${path}: overlapping chunks at ${orig_index} > ${chunk.orig_index}`
|
|
);
|
|
}
|
|
|
|
// Add lines before the chunk
|
|
for (let i = orig_index; i < chunk.orig_index; i++) {
|
|
dest_lines.push(orig_lines[i]);
|
|
}
|
|
orig_index = chunk.orig_index;
|
|
|
|
// Add inserted lines
|
|
dest_lines.push(...chunk.ins_lines);
|
|
|
|
// Skip deleted lines
|
|
orig_index += chunk.del_lines.length;
|
|
}
|
|
|
|
// Add remaining lines
|
|
for (let i = orig_index; i < orig_lines.length; i++) {
|
|
dest_lines.push(orig_lines[i]);
|
|
}
|
|
|
|
return dest_lines.join("\n");
|
|
}
|
|
|
|
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) {
|
|
commit.changes[path] = new FileChange(
|
|
ActionType.DELETE,
|
|
orig[path],
|
|
null,
|
|
null
|
|
);
|
|
} else if (action.type === ActionType.ADD) {
|
|
if (action.new_file === null) {
|
|
throw new DiffError("ADD action without file content");
|
|
}
|
|
commit.changes[path] = new FileChange(
|
|
ActionType.ADD,
|
|
null,
|
|
action.new_file,
|
|
null
|
|
);
|
|
} 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,
|
|
move_path
|
|
);
|
|
}
|
|
}
|
|
return commit;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// User-facing helpers
|
|
// --------------------------------------------------------------------------- //
|
|
function text_to_patch(text, orig, chroot = null) {
|
|
// Handle different line ending scenarios
|
|
let lines = text.split(/\r?\n/);
|
|
|
|
// Remove trailing empty line if it exists (from trailing newline)
|
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
lines = lines.slice(0, -1);
|
|
}
|
|
|
|
// Debug logging
|
|
console.log("Lines count:", lines.length);
|
|
console.log("First line:", JSON.stringify(lines[0]));
|
|
if (lines.length > 0) {
|
|
console.log("Last line:", JSON.stringify(lines[lines.length - 1]));
|
|
console.log("First line normalized:", JSON.stringify(Parser._norm(lines[0])));
|
|
console.log("Last line normalized:", JSON.stringify(Parser._norm(lines[lines.length - 1])));
|
|
}
|
|
|
|
if (
|
|
lines.length < 2 ||
|
|
!Parser._norm(lines[0]).startsWith("*** Begin Patch") ||
|
|
(lines.length > 0 && Parser._norm(lines[lines.length - 1]) !== "*** End Patch")
|
|
) {
|
|
throw new DiffError("Invalid patch text - missing sentinels");
|
|
}
|
|
|
|
const parser = new Parser(orig, lines, chroot);
|
|
parser.index = 1;
|
|
parser.parse();
|
|
return [parser.patch, parser.fuzz];
|
|
}
|
|
|
|
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] === "") {
|
|
lines = lines.slice(0, -1);
|
|
}
|
|
|
|
const updateFiles = lines
|
|
.filter(line => line.startsWith("*** Update File: "))
|
|
.map(line => resolvePath(chroot, line.substring("*** Update File: ".length)));
|
|
|
|
const deleteFiles = lines
|
|
.filter(line => line.startsWith("*** Delete File: "))
|
|
.map(line => resolvePath(chroot, line.substring("*** Delete File: ".length)));
|
|
|
|
return [...updateFiles, ...deleteFiles];
|
|
}
|
|
|
|
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] === "") {
|
|
lines = lines.slice(0, -1);
|
|
}
|
|
|
|
return lines
|
|
.filter(line => line.startsWith("*** Add File: "))
|
|
.map(line => resolvePath(chroot, line.substring("*** Add File: ".length)));
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// File-system helpers
|
|
// --------------------------------------------------------------------------- //
|
|
function load_files(paths, open_fn) {
|
|
const result = {};
|
|
for (const path of paths) {
|
|
result[path] = open_fn(path);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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);
|
|
} else if (change.type === ActionType.ADD) {
|
|
if (change.new_content === null) {
|
|
throw new DiffError(`ADD change for ${path} has no content`);
|
|
}
|
|
write_fn(path, change.new_content);
|
|
} else if (change.type === ActionType.UPDATE) {
|
|
if (change.new_content === null) {
|
|
throw new DiffError(`UPDATE change for ${path} has no new content`);
|
|
}
|
|
const target = change.move_path ? resolvePath(chroot, change.move_path) : path;
|
|
write_fn(target, change.new_content);
|
|
if (change.move_path) {
|
|
remove_fn(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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, chroot);
|
|
const orig = load_files(paths, open_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!";
|
|
}
|
|
|
|
// --------------------------------------------------------------------------- //
|
|
// Default FS helpers
|
|
// --------------------------------------------------------------------------- //
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
function open_file(filepath) {
|
|
return fs.readFileSync(filepath, 'utf8');
|
|
}
|
|
|
|
function write_file(filepath, content) {
|
|
const dir = path.dirname(filepath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(filepath, content, 'utf8');
|
|
}
|
|
|
|
function remove_file(filepath) {
|
|
if (fs.existsSync(filepath)) {
|
|
fs.unlinkSync(filepath);
|
|
}
|
|
}
|
|
|
|
// Export for the new interface
|
|
export default {
|
|
type: "function",
|
|
name: "patch_files",
|
|
description: "Apply a unified diff patch " + desc,
|
|
parameters: {
|
|
type: "object",
|
|
properties: {
|
|
patch: {
|
|
type: "string",
|
|
description: "The patch string to apply.",
|
|
}
|
|
},
|
|
required: ["patch"],
|
|
additionalProperties: false,
|
|
},
|
|
strict: true,
|
|
};
|
|
|
|
export async function run(args) {
|
|
console.log('patch_files:', args);
|
|
|
|
try {
|
|
const result = process_patch(
|
|
args.patch,
|
|
open_file,
|
|
write_file,
|
|
remove_file,
|
|
'/home/seb/src/aiTools/tmp'
|
|
);
|
|
return result;
|
|
} catch (error) {
|
|
if (error instanceof DiffError) {
|
|
throw new Error(`Patch error: ${error.message}`);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|