A JavaScript text diff implementation based on the Myers algorithm for comparing text at different granularities.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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');