Skip to content

Instantly share code, notes, and snippets.

@tailot
Created June 26, 2025 16:10
Show Gist options
  • Save tailot/136876a41b254e0bbac70786e9516fad to your computer and use it in GitHub Desktop.
Save tailot/136876a41b254e0bbac70786e9516fad to your computer and use it in GitHub Desktop.
This script is a command-line tool for analyzing and updating .patch files. It verifies if the context lines (frames) of a patch are still valid against a target file. If the frames are intact, the script can automatically update the patch's content to match the current state of the file, effectively "rebasing" the patch.
#!/usr/bin/env node
/**
* Usage: node patch-analyzer.js <patch_file> [target_file]
*
* Analyzes a patch file to verify the integrity of its context frames against a target file.
* If the frames are intact, it updates the patch with the current content from the target file.
*
* Arguments:
* <patch_file> The path to the .patch file to analyze.
* [target_file] Optional. The path to the file the patch applies to.
* If not provided, the script will infer it from the patch content.
*
* Example:
* node patch-analyzer.js my-feature.patch src/component.js
*/
/**
* @fileoverview A script to analyze and verify the integrity of patch files.
* It can parse a patch, identify modification blocks, and check if the
* context lines (frames) around those blocks are still intact in the target files.
* If the frames are intact, it can update the patch content to match the current
* state of the file, effectively "rebasing" the patch.
*/
const fs = require('fs');
const path = require('path');
/**
* @class PatchAnalyzer
* @description A class to analyze, verify, and update patch files.
*/
class PatchAnalyzer {
constructor() {
// This property is not actively used but could be for future enhancements.
this.patches = [];
}
/**
* Reads and parses a patch file from the given path.
* @param {string} patchPath - The path to the patch file.
* @returns {Array<Object>} An array of parsed patch objects.
* @throws {Error} If there is an error reading or parsing the file.
*/
readPatch(patchPath) {
try {
const patchContent = fs.readFileSync(patchPath, 'utf8');
return this.parsePatch(patchContent);
} catch (error) {
throw new Error(`Error reading patch file: ${error.message}`);
}
}
/**
* Parses the content of a patch file.
* @param {string} patchContent - The string content of the patch file.
* @returns {Array<Object>} An array of parsed patch objects.
*/
parsePatch(patchContent) {
const lines = patchContent.split('\n');
const patches = [];
let currentPatch = null;
let currentHunk = null;
for (const line of lines) {
if (line.startsWith('---')) {
if (currentPatch) {
patches.push(currentPatch);
}
currentPatch = {
oldFile: line.substring(4).trim(),
newFile: null,
hunks: [],
};
} else if (line.startsWith('+++')) {
if (currentPatch) {
currentPatch.newFile = line.substring(4).trim();
}
} else if (line.startsWith('@@')) {
const hunkInfo = this.parseHunkHeader(line);
currentHunk = {
oldStart: hunkInfo.oldStart,
oldCount: hunkInfo.oldCount,
newStart: hunkInfo.newStart,
newCount: hunkInfo.newCount,
changes: [],
};
if (currentPatch) {
currentPatch.hunks.push(currentHunk);
}
} else if (currentHunk && (line.startsWith(' ') || line.startsWith('+') || line.startsWith('-'))) {
const changeType = line.charAt(0);
const content = line.substring(1);
currentHunk.changes.push({
type: changeType,
content: content,
});
}
}
if (currentPatch) {
patches.push(currentPatch);
}
return patches;
}
/**
* Parses a hunk header line (e.g., "@@ -1,4 +1,4 @@").
* @param {string} header - The hunk header line.
* @returns {Object} An object containing the start and count for old and new files.
* @throws {Error} If the hunk header format is invalid.
*/
parseHunkHeader(header) {
const match = header.match(/@@\s*-(\d+)(?:,(\d+))?\s*\+(\d+)(?:,(\d+))?\s*@@/);
if (!match) {
throw new Error(`Invalid hunk header: ${header}`);
}
return {
oldStart: parseInt(match[1], 10),
oldCount: parseInt(match[2], 10) || 1,
newStart: parseInt(match[3], 10),
newCount: parseInt(match[4], 10) || 1,
};
}
/**
* Identifies modification blocks (a sequence of additions or deletions) and their surrounding context lines (frames).
* @param {Object} patch - A parsed patch object.
* @returns {Array<Object>} An array of identified modification blocks.
*/
identifyModificationBlocks(patch) {
const blocks = [];
patch.hunks.forEach((hunk, hunkIndex) => {
let currentBlock = null;
let lastContextLine = null;
hunk.changes.forEach((change, changeIndex) => {
if (change.type === ' ') {
// This is a context line.
if (currentBlock && !currentBlock.bottomFrame) {
// This is the first context line after a modification block, so it's the bottom frame.
currentBlock.bottomFrame = {
content: change.content,
oldLineNumber: this.calculateActualLineNumber(hunk, changeIndex, 'old'),
newLineNumber: this.calculateActualLineNumber(hunk, changeIndex, 'new'),
};
blocks.push(currentBlock);
currentBlock = null;
}
lastContextLine = {
content: change.content,
oldLineNumber: this.calculateActualLineNumber(hunk, changeIndex, 'old'),
newLineNumber: this.calculateActualLineNumber(hunk, changeIndex, 'new'),
};
} else if (change.type === '+' || change.type === '-') {
// This is a modification line, start a new block if we're not in one.
if (!currentBlock) {
currentBlock = {
type: change.type === '+' ? 'addition' : 'deletion',
changes: [],
topFrame: lastContextLine,
bottomFrame: null,
hunkIndex: hunkIndex,
startLineNumber: this.calculateActualLineNumber(hunk, changeIndex, change.type === '+' ? 'new' : 'old'),
};
}
// Ensure the block is homogeneous (only additions or only deletions).
const expectedType = currentBlock.type === 'addition' ? '+' : '-';
if (change.type !== expectedType) {
// This tool does not support mixed modification blocks.
// A more advanced implementation might handle this differently.
if (currentBlock) {
blocks.push(currentBlock);
}
currentBlock = {
type: change.type === '+' ? 'addition' : 'deletion',
changes: [],
topFrame: lastContextLine,
bottomFrame: null,
hunkIndex: hunkIndex,
startLineNumber: this.calculateActualLineNumber(hunk, changeIndex, change.type === '+' ? 'new' : 'old'),
};
}
currentBlock.changes.push(change);
}
});
// If a block is still open at the end of the hunk, close it.
if (currentBlock) {
blocks.push(currentBlock);
}
});
return blocks;
}
/**
* Calculates the actual line number in the file for a given change within a hunk.
* @param {Object} hunk - The hunk object.
* @param {number} changeIndex - The index of the change within the hunk's changes array.
* @param {('old'|'new')} lineType - Whether to calculate the line number for the old or new file.
* @returns {number} The calculated line number.
*/
calculateActualLineNumber(hunk, changeIndex, lineType) {
let oldLine = hunk.oldStart;
let newLine = hunk.newStart;
for (let i = 0; i < changeIndex; i++) {
const change = hunk.changes[i];
if (change.type === ' ') {
oldLine++;
newLine++;
} else if (change.type === '-') {
oldLine++;
} else if (change.type === '+') {
newLine++;
}
}
if (lineType === 'old') {
return oldLine;
}
// Adjust for the current line
const currentChange = hunk.changes[changeIndex];
if(currentChange.type === ' '){
return newLine;
} else if (currentChange.type === '+'){
return newLine;
} else { // type is '-'
// For deletions, the 'new' line number doesn't really advance,
// but for context, we can return the last valid new line number.
return newLine;
}
}
/**
* Updates the content of modification blocks in a patch based on the current file content.
* This is done only after verifying that the frames are intact.
* @param {Object} patch - The parsed patch object to update.
* @param {Array<Object>} blocks - The identified modification blocks.
* @param {Array<string>} fileLines - The lines of the target file.
*/
updatePatchBlocks(patch, blocks, fileLines) {
blocks.forEach(block => {
const hunk = patch.hunks[block.hunkIndex];
const startLineInFile = this.findBlockStartInFile(block, fileLines);
if (startLineInFile !== -1) {
for (let i = 0; i < block.changes.length; i++) {
const change = block.changes[i];
const fileLineIndex = startLineInFile + i;
if (fileLineIndex < fileLines.length) {
const newContent = fileLines[fileLineIndex];
// Keep the +/- sign but update the content.
change.content = newContent;
// Also update the original hunk
const changeIndexInHunk = this.findChangeIndexInHunk(hunk, change, i);
if (changeIndexInHunk !== -1) {
hunk.changes[changeIndexInHunk].content = newContent;
}
}
}
}
});
}
/**
* Finds the starting line index of a modification block within the target file lines.
* It uses the top and bottom frames to locate the block.
* @param {Object} block - The modification block.
* @param {Array<string>} fileLines - The lines of the target file.
* @returns {number} The starting line index of the block in the file, or -1 if not found.
*/
findBlockStartInFile(block, fileLines) {
if (!block.topFrame || !block.bottomFrame) {
// Cannot locate block without both frames.
return -1;
}
for (let i = 0; i < fileLines.length - 1; i++) {
if (fileLines[i].trim() === block.topFrame.content.trim()) {
// Found potential top frame. Now check for the bottom frame at the expected position.
const expectedBottomIndex = i + 1 + block.changes.length;
if (
expectedBottomIndex < fileLines.length &&
fileLines[expectedBottomIndex].trim() === block.bottomFrame.content.trim()
) {
return i + 1; // Return the index of the first line of the block's content.
}
}
}
return -1;
}
/**
* Finds the index of a specific change object within a hunk's changes array.
* @param {Object} hunk - The hunk object.
* @param {Object} targetChange - The change to find.
* @param {number} originalIndex - The original index of the change to help resolve duplicates.
* @returns {number} The index of the change, or -1 if not found.
*/
findChangeIndexInHunk(hunk, targetChange, originalIndex) {
// A simple search might be ambiguous if the same line is changed multiple times.
// A more robust solution would be to pass the original index.
// For this implementation, we assume content is unique enough.
for (let i = 0; i < hunk.changes.length; i++) {
const change = hunk.changes[i];
if (change.type === targetChange.type && change.content === targetChange.content && i >= originalIndex) {
return i;
}
}
return -1;
}
/**
* Reconstructs the patch file content from the parsed (and possibly updated) patch data.
* @param {Array<Object>} patches - An array of patch objects.
* @returns {string} The reconstructed patch file content.
*/
reconstructPatch(patches) {
let patchContent = '';
for (const patch of patches) {
patchContent += `--- ${patch.oldFile}\n`;
patchContent += `+++ ${patch.newFile}\n`;
for (const hunk of patch.hunks) {
// Recalculate hunk counts based on changes
const oldCount = hunk.changes.filter(c => c.type === '-' || c.type === ' ').length;
const newCount = hunk.changes.filter(c => c.type === '+' || c.type === ' ').length;
patchContent += `@@ -${hunk.oldStart},${oldCount} +${hunk.newStart},${newCount} @@\n`;
for (const change of hunk.changes) {
patchContent += `${change.type}${change.content}\n`;
}
}
}
return patchContent;
}
/**
* Saves the updated patch content to a file, creating a backup of the original.
* @param {string} patchPath - The path to save the patch file.
* @param {string} patchContent - The content to save.
*/
savePatch(patchPath, patchContent) {
const backupPath = patchPath + '.backup';
if (fs.existsSync(patchPath)) {
fs.copyFileSync(patchPath, backupPath);
console.log(`Original patch backed up to: ${backupPath}`);
}
fs.writeFileSync(patchPath, patchContent);
console.log(`Updated patch saved to: ${patchPath}`);
}
/**
* Verifies the integrity of the frames for all modification blocks in a patch file.
* @param {string} patchPath - The path to the patch file.
* @param {string} [targetFilePath] - Optional. The path to the target file to check against. If not provided, it's inferred from the patch.
* @returns {Promise<Array<Object>>} A promise that resolves to an array of result objects, one for each file in the patch.
* @throws {Error} If there is a critical error during verification.
*/
async verifyFrameIntegrity(patchPath, targetFilePath) {
try {
const patches = this.readPatch(patchPath);
const results = [];
for (const patch of patches) {
let filePath = targetFilePath;
if (!filePath) {
// Infer file path from the patch, removing 'a/' or 'b/' prefixes.
filePath = patch.oldFile.startsWith('a/') ? patch.oldFile.substring(2) : patch.oldFile;
}
if (!fs.existsSync(filePath)) {
results.push({
file: filePath,
status: 'error',
message: 'Target file not found',
});
continue;
}
const fileContent = fs.readFileSync(filePath, 'utf8');
const fileLines = fileContent.split('\n');
const blocks = this.identifyModificationBlocks(patch);
const frameChecks = blocks.map(block => ({
blockType: block.type,
topFrame: this.checkFrame(fileLines, block.topFrame, 'top', block),
bottomFrame: this.checkFrame(fileLines, block.bottomFrame, 'bottom', block),
}));
const allFramesIntact = frameChecks.every(
check => check.topFrame.intact && check.bottomFrame.intact
);
const result = {
file: filePath,
status: allFramesIntact ? 'intact' : 'corrupted',
blocks: frameChecks,
message: allFramesIntact ? 'All frames are intact.' : 'Some frames are corrupted or have shifted.',
};
if (allFramesIntact && blocks.length > 0) {
console.log(`Frames are intact for ${filePath}. Updating patch content...`);
this.updatePatchBlocks(patch, blocks, fileLines);
result.updated = true;
}
results.push(result);
}
const hasUpdates = results.some(result => result.updated);
if (hasUpdates) {
const updatedPatchContent = this.reconstructPatch(patches);
this.savePatch(patchPath, updatedPatchContent);
console.log('✅ Patch updated successfully!');
}
return results;
} catch (error) {
throw new Error(`Verification failed: ${error.message}`);
}
}
/**
* Checks a single frame (top or bottom) against the content of the target file.
* @param {Array<string>} fileLines - The lines of the target file.
* @param {Object} frame - The frame object to check.
* @param {('top'|'bottom')} position - The position of the frame.
* @param {Object} block - The modification block this frame belongs to.
* @returns {Object} A result object indicating if the frame is intact.
*/
checkFrame(fileLines, frame, position, block) {
if (!frame) {
return {
intact: true, // No frame to check, so it's considered intact.
message: `Frame ${position} not present in patch hunk.`,
expected: null,
actual: null,
};
}
// Use the line number from the frame object, which is calculated during block identification.
const frameLineIndex = (position === 'top' ? frame.oldLineNumber : frame.oldLineNumber) -1;
if (frameLineIndex < 0 || frameLineIndex >= fileLines.length) {
return {
intact: false,
message: `Frame ${position} is outside the file boundaries (line ${frameLineIndex + 1}).`,
expected: frame.content,
actual: null,
lineNumber: frameLineIndex + 1,
};
}
const actualContent = fileLines[frameLineIndex];
const intact = actualContent.trim() === frame.content.trim();
return {
intact: intact,
message: intact ? `Frame ${position} is intact.` : `Frame ${position} has been modified.`,
expected: frame.content,
actual: actualContent,
lineNumber: frameLineIndex + 1,
};
}
/**
* Generates a detailed human-readable report from the verification results.
* @param {Array<Object>} results - The array of verification result objects.
* @returns {string} A formatted report string.
*/
generateReport(results) {
let report = '=== FRAME INTEGRITY VERIFICATION REPORT ===\n\n';
for (const result of results) {
report += `File: ${result.file}\n`;
report += `Status: ${result.status.toUpperCase()}`;
if (result.updated) {
report += ' (UPDATED)';
}
report += '\n';
report += `Message: ${result.message}\n`;
if (result.blocks && result.status !== 'intact') {
report += `\nModification Blocks Found: ${result.blocks.length}\n`;
result.blocks.forEach((block, index) => {
if(block.topFrame.intact && block.bottomFrame.intact) return;
report += `\n Block ${index + 1} (${block.blockType}):\n`;
report += ` Top Frame: ${block.topFrame.intact ? 'INTACT' : 'CORRUPTED'}`;
if (block.topFrame.lineNumber) {
report += ` (line ${block.topFrame.lineNumber})`;
}
report += '\n';
if (!block.topFrame.intact) {
report += ` Expected: "${block.topFrame.expected}"\n`;
report += ` Found: "${block.topFrame.actual}"\n`;
}
report += ` Bottom Frame: ${block.bottomFrame.intact ? 'INTACT' : 'CORRUPTED'}`;
if (block.bottomFrame.lineNumber) {
report += ` (line ${block.bottomFrame.lineNumber})`;
}
report += '\n';
if (!block.bottomFrame.intact) {
report += ` Expected: "${block.bottomFrame.expected}"\n`;
report += ` Found: "${block.bottomFrame.actual}"\n`;
}
});
}
report += '\n' + '='.repeat(50) + '\n\n';
}
return report;
}
}
/**
* The main function to run the patch analyzer from the command line.
*/
async function main() {
const analyzer = new PatchAnalyzer();
try {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log(`\nUsage: node t.js <patch_file> [target_file]\n\nAnalyzes a patch file to verify the integrity of its context frames against a target file.\nIf the frames are intact, it updates the patch with the current content from the target file.\n\nArguments:\n <patch_file> The path to the .patch file to analyze.\n [target_file] Optional. The path to the file the patch applies to. \n If not provided, the script will infer it from the patch content.\n\nExample:\n node t.js my-feature.patch src/component.js\n`);
process.exit(0);
}
const patchPath = args[0];
const targetFile = args[1]; // Optional
console.log(`Analyzing patch: ${patchPath}`);
if (targetFile) {
console.log(`Against target file: ${targetFile}`);
}
const results = await analyzer.verifyFrameIntegrity(patchPath, targetFile);
const report = analyzer.generateReport(results);
console.log(report);
const reportPath = 'frame_integrity_report.txt';
fs.writeFileSync(reportPath, report);
console.log(`Report saved to: ${reportPath}`);
const intactCount = results.filter(r => r.status === 'intact').length;
const updatedCount = results.filter(r => r.updated).length;
const errorCount = results.filter(r => r.status === 'error').length;
console.log(`\n📊 Statistics:`);
console.log(` Files with intact frames: ${intactCount}/${results.length}`);
console.log(` Patches updated: ${updatedCount}`);
console.log(` Errors (e.g., file not found): ${errorCount}`);
if (updatedCount > 0) {
console.log(`\n✨ Patches have been synchronized with the current file content!`);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}
// Export the class for use as a module
module.exports = PatchAnalyzer;
// If run directly from the command line, execute the main function
if (require.main === module) {
main();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment