Skip to content

Instantly share code, notes, and snippets.

@zackiles
Created November 3, 2024 02:45
Show Gist options
  • Save zackiles/d0cf180ac730a9b95595c1795051edb9 to your computer and use it in GitHub Desktop.
Save zackiles/d0cf180ac730a9b95595c1795051edb9 to your computer and use it in GitHub Desktop.
Virtualized Patch Command in Javascript
import fs from 'node:fs/promises';
import path from 'node:path';
/**
* Applies a unified diff to a file system, emulating the Unix 'patch' command.
* @param {string} jsonString - The JSON string containing 'target_file' and 'diff'.
* @param {Object} [fsModule] - Optional file system module (e.g., memfs) for virtual file systems.
*/
async function applyPatch(jsonString, fsModule = fs) {
try {
const { target_file, diff, error } = JSON.parse(jsonString);
if (error) {
console.error(`Error from AI: ${error}`);
return;
}
if (!target_file || !diff) {
throw new Error('Invalid JSON format: Missing required fields');
}
const projectRoot = process.cwd();
const sanitizedPath = path.posix.normalize('/' + target_file).substring(1);
const resolvedPath = path.join(projectRoot, sanitizedPath);
if (!resolvedPath.startsWith(projectRoot)) {
throw new Error('Invalid file path');
}
const targetDir = path.dirname(resolvedPath);
try {
await fsModule.access(targetDir);
} catch {
await fsModule.mkdir(targetDir, { recursive: true });
}
let originalContent = '';
try {
await fsModule.access(resolvedPath);
originalContent = await fsModule.readFile(resolvedPath, 'utf8');
} catch {
if (!diff.startsWith('--- a/dev/null')) {
throw new Error('Target file does not exist');
}
}
const patchedContent = applyUnifiedDiff(originalContent, diff);
await fsModule.writeFile(resolvedPath, patchedContent, 'utf8');
console.log(`Patch applied successfully to ${target_file}`);
} catch (err) {
console.error('Error applying patch:', err.message);
}
}
function applyUnifiedDiff(originalContent, diff) {
const originalLines = originalContent.split('\n');
const diffLines = diff.replace(/\\n/g, '\n').split('\n');
let patchedLines = [...originalLines];
let diffIndex = 0;
while (diffIndex < diffLines.length) {
const line = diffLines[diffIndex];
if (line.startsWith('@@')) {
const hunkInfo = parseHunkHeader(line);
const { origStart } = hunkInfo;
diffIndex++;
const hunkLines = [];
while (diffIndex < diffLines.length && !diffLines[diffIndex].startsWith('@@')) {
hunkLines.push(diffLines[diffIndex]);
diffIndex++;
}
patchedLines = applyHunk(patchedLines, hunkInfo, hunkLines);
} else {
diffIndex++;
}
}
return patchedLines.join('\n');
}
function parseHunkHeader(headerLine) {
const match = /@@ \-(\d+),?(\d*) \+(\d+),?(\d*) @@/.exec(headerLine);
if (!match) {
throw new Error(`Invalid hunk header: ${headerLine}`);
}
return {
origStart: parseInt(match[1], 10) - 1,
origCount: parseInt(match[2] || '0', 10),
newStart: parseInt(match[3], 10) - 1,
newCount: parseInt(match[4] || '0', 10),
};
}
function applyHunk(fileLines, hunkInfo, hunkLines) {
const { origStart } = hunkInfo;
let fileIndex = origStart;
const newLines = [];
for (const line of hunkLines) {
if (line.startsWith(' ')) {
if (fileLines[fileIndex] !== line.substring(1)) {
throw new Error('Hunk does not apply cleanly (context mismatch)');
}
newLines.push(fileLines[fileIndex]);
fileIndex++;
} else if (line.startsWith('-')) {
if (fileLines[fileIndex] !== line.substring(1)) {
throw new Error('Hunk does not apply cleanly (deletion mismatch)');
}
fileIndex++;
} else if (line.startsWith('+')) {
newLines.push(line.substring(1));
} else {
throw new Error(`Invalid hunk line: ${line}`);
}
}
return [
...fileLines.slice(0, origStart),
...newLines,
...fileLines.slice(fileIndex),
];
}
export { applyPatch };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment