Parse, manipulate, and convert diff patches between different formats. Essential utilities for working with patch files, merging changes, and transforming patch data structures.
Parses a unified diff string into structured patch objects for programmatic manipulation.
/**
* Parse unified diff string into structured patch objects
* @param diffStr - Unified diff string content
* @returns Array of structured patch objects
*/
function parsePatch(diffStr);Reverses a patch by swapping old and new file information and inverting add/remove operations.
/**
* Reverse a patch (swap old/new and invert operations)
* @param patch - Structured patch object or array of patches
* @returns Reversed patch with swapped file references and inverted operations
*/
function reversePatch(patch);Merges two patches that modify the same base content, handling conflicts and overlapping changes.
/**
* Merge two patches with conflict detection
* @param mine - First patch (my changes)
* @param theirs - Second patch (their changes)
* @param base - Base content that both patches modify (optional)
* @returns Merged patch with conflict markers where applicable
*/
function merge(mine, theirs, base);import { parsePatch } from "diff";
// Parse a patch file
const patchContent = `--- file1.txt
+++ file1.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+modified line 2
line 3
--- file2.txt
+++ file2.txt
@@ -1,2 +1,3 @@
existing line
+new line
another line`;
const patches = parsePatch(patchContent);
console.log(`Found ${patches.length} file patches`);
patches.forEach((patch, index) => {
console.log(`\nPatch ${index + 1}:`);
console.log(` File: ${patch.oldFileName} -> ${patch.newFileName}`);
console.log(` Hunks: ${patch.hunks.length}`);
patch.hunks.forEach((hunk, hunkIndex) => {
console.log(` Hunk ${hunkIndex + 1}: lines ${hunk.oldStart}-${hunk.oldStart + hunk.oldLines - 1}`);
console.log(` Changes: ${hunk.lines.filter(l => l.startsWith('+') || l.startsWith('-')).length}`);
});
});import { reversePatch, parsePatch, formatPatch } from "diff";
// Create a reverse patch to undo changes
const originalPatch = parsePatch(patchString)[0];
const undoPatch = reversePatch(originalPatch);
console.log("Original patch changes file from A to B");
console.log("Reverse patch changes file from B to A");
// Format back to string
const undoPatchString = formatPatch(undoPatch);
console.log("Undo patch:\n", undoPatchString);
// Reverse multiple patches
const multiplePatches = parsePatch(multiFilePatchString);
const reversedPatches = reversePatch(multiplePatches);import { merge, parsePatch } from "diff";
// Two developers made different changes to the same file
const myPatch = `--- config.json
+++ config.json
@@ -1,4 +1,4 @@
{
"version": "1.0",
- "debug": false,
+ "debug": true,
"features": []
}`;
const theirPatch = `--- config.json
+++ config.json
@@ -1,4 +1,5 @@
{
"version": "1.0",
"debug": false,
+ "newFeature": "enabled",
"features": []
}`;
const myParsed = parsePatch(myPatch)[0];
const theirParsed = parsePatch(theirPatch)[0];
// Merge the patches
const merged = merge(myParsed, theirParsed);
if (merged.conflict) {
console.log("Merge has conflicts that need manual resolution");
} else {
console.log("Patches merged successfully");
}
console.log("Merged hunks:", merged.hunks.length);import { parsePatch } from "diff";
function analyzePatchFile(patchContent) {
const patches = parsePatch(patchContent);
const analysis = {
files: patches.length,
totalHunks: 0,
linesAdded: 0,
linesRemoved: 0,
linesContext: 0,
fileTypes: {},
largestHunk: 0,
conflictingFiles: []
};
patches.forEach(patch => {
analysis.totalHunks += patch.hunks.length;
// Analyze file type
const extension = patch.oldFileName.split('.').pop() || 'no-extension';
analysis.fileTypes[extension] = (analysis.fileTypes[extension] || 0) + 1;
patch.hunks.forEach(hunk => {
const hunkSize = hunk.lines.length;
analysis.largestHunk = Math.max(analysis.largestHunk, hunkSize);
hunk.lines.forEach(line => {
if (line.startsWith('+')) {
analysis.linesAdded++;
} else if (line.startsWith('-')) {
analysis.linesRemoved++;
} else if (line.startsWith(' ')) {
analysis.linesContext++;
}
});
});
// Check for potential conflicts (placeholder logic)
if (patch.hunks.some(h => h.lines.length > 50)) {
analysis.conflictingFiles.push(patch.oldFileName);
}
});
return analysis;
}
// Usage
const patchAnalysis = analyzePatchFile(largePatchString);
console.log(`Patch affects ${patchAnalysis.files} files`);
console.log(`Changes: +${patchAnalysis.linesAdded} -${patchAnalysis.linesRemoved}`);
console.log(`File types:`, patchAnalysis.fileTypes);import { parsePatch, formatPatch } from "diff";
function transformPatch(patchContent, transformer) {
const patches = parsePatch(patchContent);
const transformed = patches.map(patch => ({
...patch,
oldFileName: transformer.transformPath(patch.oldFileName),
newFileName: transformer.transformPath(patch.newFileName),
hunks: patch.hunks.map(hunk => ({
...hunk,
lines: hunk.lines.map(line => transformer.transformLine(line))
}))
}));
return formatPatch(transformed);
}
// Example transformer: update import paths
const pathUpdateTransformer = {
transformPath: (path) => path.replace(/^src\//, 'lib/'),
transformLine: (line) => {
if (line.startsWith('+') || line.startsWith('-')) {
const prefix = line[0];
const content = line.slice(1);
const updated = content.replace(/import.*from ['"]\.\.\/src\//g,
match => match.replace('../src/', '../lib/'));
return prefix + updated;
}
return line;
}
};
const updatedPatch = transformPatch(originalPatch, pathUpdateTransformer);import { merge, parsePatch } from "diff";
function resolveConflicts(mergedPatch, resolutionStrategy) {
if (!mergedPatch.conflict) {
return mergedPatch;
}
const resolved = { ...mergedPatch };
resolved.hunks = mergedPatch.hunks.map(hunk => {
if (!hunk.conflict) {
return hunk;
}
const resolvedLines = [];
hunk.lines.forEach(line => {
if (line.conflict) {
// Apply resolution strategy
switch (resolutionStrategy) {
case 'mine':
resolvedLines.push(...line.mine);
break;
case 'theirs':
resolvedLines.push(...line.theirs);
break;
case 'both':
resolvedLines.push(...line.mine);
resolvedLines.push(...line.theirs);
break;
case 'markers':
resolvedLines.push('<<<<<<< mine');
resolvedLines.push(...line.mine);
resolvedLines.push('=======');
resolvedLines.push(...line.theirs);
resolvedLines.push('>>>>>>> theirs');
break;
}
} else {
resolvedLines.push(line);
}
});
return {
...hunk,
lines: resolvedLines,
conflict: false
};
});
resolved.conflict = false;
return resolved;
}
// Usage
const conflictedMerge = merge(patch1, patch2);
if (conflictedMerge.conflict) {
const resolvedWithMarkers = resolveConflicts(conflictedMerge, 'markers');
const resolvedMine = resolveConflicts(conflictedMerge, 'mine');
const resolvedTheirs = resolveConflicts(conflictedMerge, 'theirs');
}import { parsePatch } from "diff";
function validatePatch(patchContent) {
const validation = {
isValid: true,
errors: [],
warnings: [],
patches: []
};
try {
const patches = parsePatch(patchContent);
validation.patches = patches;
patches.forEach((patch, patchIndex) => {
// Check for missing file names
if (!patch.oldFileName || !patch.newFileName) {
validation.errors.push(`Patch ${patchIndex}: Missing file names`);
validation.isValid = false;
}
// Validate hunks
patch.hunks.forEach((hunk, hunkIndex) => {
// Check hunk integrity
const addLines = hunk.lines.filter(l => l.startsWith('+')).length;
const removeLines = hunk.lines.filter(l => l.startsWith('-')).length;
const contextLines = hunk.lines.filter(l => l.startsWith(' ')).length;
const expectedOldLines = removeLines + contextLines;
const expectedNewLines = addLines + contextLines;
if (hunk.oldLines !== expectedOldLines) {
validation.warnings.push(
`Patch ${patchIndex}, hunk ${hunkIndex}: Old line count mismatch`
);
}
if (hunk.newLines !== expectedNewLines) {
validation.warnings.push(
`Patch ${patchIndex}, hunk ${hunkIndex}: New line count mismatch`
);
}
// Check for overlapping hunks
if (hunkIndex > 0) {
const prevHunk = patch.hunks[hunkIndex - 1];
if (hunk.oldStart <= prevHunk.oldStart + prevHunk.oldLines) {
validation.errors.push(
`Patch ${patchIndex}: Overlapping hunks ${hunkIndex - 1} and ${hunkIndex}`
);
validation.isValid = false;
}
}
});
});
} catch (error) {
validation.isValid = false;
validation.errors.push(`Parse error: ${error.message}`);
}
return validation;
}
// Usage
const validation = validatePatch(suspiciousPatchContent);
if (!validation.isValid) {
console.error("Invalid patch:", validation.errors);
} else if (validation.warnings.length > 0) {
console.warn("Patch warnings:", validation.warnings);
} else {
console.log("Patch is valid");
}import { parsePatch, formatPatch } from "diff";
function convertPatchFormat(patchContent, targetFormat) {
const patches = parsePatch(patchContent);
switch (targetFormat) {
case 'git':
return formatPatch(patches.map(patch => ({
...patch,
oldFileName: `a/${patch.oldFileName}`,
newFileName: `b/${patch.newFileName}`
})));
case 'svn':
// SVN-style format conversion
return patches.map(patch => {
const lines = [`Index: ${patch.oldFileName}`, '==================================================================='];
lines.push(...formatPatch(patch).split('\n').slice(2)); // Skip standard headers
return lines.join('\n');
}).join('\n\n');
case 'context':
// Convert to context diff format (simplified)
return patches.map(patch =>
`*** ${patch.oldFileName}\n--- ${patch.newFileName}\n${formatContextDiff(patch)}`
).join('\n\n');
default:
return formatPatch(patches);
}
}
function formatContextDiff(patch) {
// Simplified context diff format
return patch.hunks.map(hunk => {
const contextLines = [`***************`];
contextLines.push(`*** ${hunk.oldStart},${hunk.oldStart + hunk.oldLines - 1} ****`);
// Add old lines
hunk.lines.forEach(line => {
if (line.startsWith('-') || line.startsWith(' ')) {
contextLines.push(line.startsWith('-') ? `- ${line.slice(1)}` : ` ${line.slice(1)}`);
}
});
contextLines.push(`--- ${hunk.newStart},${hunk.newStart + hunk.newLines - 1} ----`);
// Add new lines
hunk.lines.forEach(line => {
if (line.startsWith('+') || line.startsWith(' ')) {
contextLines.push(line.startsWith('+') ? `+ ${line.slice(1)}` : ` ${line.slice(1)}`);
}
});
return contextLines.join('\n');
}).join('\n');
}
// Usage
const gitPatch = convertPatchFormat(standardPatch, 'git');
const svnPatch = convertPatchFormat(standardPatch, 'svn');