Created
November 3, 2024 02:45
-
-
Save zackiles/d0cf180ac730a9b95595c1795051edb9 to your computer and use it in GitHub Desktop.
Virtualized Patch Command in Javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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