11 KiB
11 KiB
Patch Application
Apply unified diff patches to source text with fuzzy matching and error handling. Supports both single patches and batch patch application with configurable tolerance for mismatches.
Capabilities
applyPatch Function
Applies a unified diff patch to source text with intelligent matching and fuzzy logic.
/**
* Apply a unified diff patch to source text
* @param source - Original text to patch
* @param patch - Unified diff patch string or structured patch object
* @param options - Configuration options
* @returns Patched text on success, false on failure
*/
function applyPatch(source, patch, options);
applyPatches Function
Applies multiple patches using callback-based file loading and saving.
/**
* Apply multiple patches with callback-based file operations
* @param patches - Array of patches or multi-file patch string
* @param options - Configuration with callback functions
*/
function applyPatches(patches, options);
Options and Types
interface ApplyPatchOptions {
fuzzFactor?: number; // Maximum line mismatches allowed (default: 0)
autoConvertLineEndings?: boolean; // Auto-convert line endings (default: true)
compareLine?: (lineNumber: number, line: string, operation: string, patchContent: string) => boolean;
}
interface ApplyPatchesOptions {
loadFile: (patch: any, callback: (error: Error | null, content?: string) => void) => void;
patched: (patch: any, content: string | false, callback: (error?: Error) => void) => void;
complete: (error?: Error) => void;
}
Usage Examples
Basic Patch Application
import { applyPatch, createPatch } from "diff";
// Create a patch
const originalText = "Hello World\nLine 2\nLine 3";
const modifiedText = "Hello Universe\nLine 2\nLine 3";
const patch = createPatch("file.txt", originalText, modifiedText);
// Apply the patch
const result = applyPatch(originalText, patch);
console.log(result === modifiedText); // true
// Failed application returns false
const badResult = applyPatch("completely different text", patch);
console.log(badResult); // false
Fuzzy Patch Application
import { applyPatch } from "diff";
// Source text has slight differences from when patch was created
const sourceText = `
function hello() {
console.log("Hello World");
return true;
}
function goodbye() {
console.log("Goodbye");
}
`.trim();
// Patch expects slightly different context
const patch = `--- file.js
+++ file.js
@@ -1,4 +1,4 @@
function hello() {
- console.log("Hello World");
+ console.log("Hello Universe");
return true;
}`;
// Apply with fuzzy matching
const result = applyPatch(sourceText, patch, {
fuzzFactor: 2 // Allow up to 2 mismatched context lines
});
console.log(result); // Successfully patched despite context differences
Cross-Platform Line Endings
import { applyPatch } from "diff";
// Source has Unix line endings
const unixSource = "line1\nline2\nline3\n";
// Patch was created from Windows line endings
const windowsPatch = `--- file.txt
+++ file.txt
@@ -1,3 +1,3 @@
line1\r
-line2\r
+modified line2\r
line3\r`;
// Auto-convert line endings (default behavior)
const result = applyPatch(unixSource, windowsPatch, {
autoConvertLineEndings: true
});
console.log(result); // Successfully applied despite line ending differences
Multiple File Patches
import { applyPatches } from "diff";
import fs from "fs";
// Multi-file patch string
const multiPatch = `--- file1.txt
+++ file1.txt
@@ -1,2 +1,2 @@
-old content
+new content
unchanged line
--- file2.txt
+++ file2.txt
@@ -1,1 +1,2 @@
existing line
+added line`;
applyPatches(multiPatch, {
loadFile: (patch, callback) => {
try {
const content = fs.readFileSync(patch.oldFileName, 'utf8');
callback(null, content);
} catch (error) {
callback(error);
}
},
patched: (patch, patchedContent, callback) => {
if (patchedContent === false) {
callback(new Error(`Failed to apply patch to ${patch.oldFileName}`));
return;
}
try {
fs.writeFileSync(patch.oldFileName, patchedContent);
console.log(`Successfully patched ${patch.oldFileName}`);
callback();
} catch (error) {
callback(error);
}
},
complete: (error) => {
if (error) {
console.error("Patch application failed:", error);
} else {
console.log("All patches applied successfully");
}
}
});
Advanced Usage
Custom Line Comparison
import { applyPatch } from "diff";
// Custom comparison function for case-insensitive matching
const result = applyPatch(source, patch, {
compareLine: (lineNumber, line, operation, patchContent) => {
// Case-insensitive comparison
return line.toLowerCase() === patchContent.toLowerCase();
}
});
// Whitespace-tolerant comparison
const whitespaceResult = applyPatch(source, patch, {
compareLine: (lineNumber, line, operation, patchContent) => {
return line.trim() === patchContent.trim();
}
});
Batch File Processing
import { applyPatches } from "diff";
import path from "path";
function applyPatchesToDirectory(patchContent, baseDirectory) {
const results = {
successful: [],
failed: [],
errors: []
};
return new Promise((resolve, reject) => {
applyPatches(patchContent, {
loadFile: (patch, callback) => {
const filePath = path.join(baseDirectory, patch.oldFileName);
fs.readFile(filePath, 'utf8', (error, content) => {
if (error) {
results.errors.push({ file: patch.oldFileName, error: error.message });
callback(error);
} else {
callback(null, content);
}
});
},
patched: (patch, patchedContent, callback) => {
if (patchedContent === false) {
results.failed.push(patch.oldFileName);
callback(new Error(`Patch failed for ${patch.oldFileName}`));
return;
}
const filePath = path.join(baseDirectory, patch.oldFileName);
fs.writeFile(filePath, patchedContent, 'utf8', (error) => {
if (error) {
results.errors.push({ file: patch.oldFileName, error: error.message });
callback(error);
} else {
results.successful.push(patch.oldFileName);
callback();
}
});
},
complete: (error) => {
if (error && results.successful.length === 0) {
reject(error);
} else {
resolve(results);
}
}
});
});
}
// Usage
applyPatchesToDirectory(largePatch, './src')
.then(results => {
console.log(`Applied patches to ${results.successful.length} files`);
if (results.failed.length > 0) {
console.log(`Failed to patch: ${results.failed.join(', ')}`);
}
})
.catch(error => {
console.error("Batch patch application failed:", error);
});
Dry Run and Validation
import { applyPatch } from "diff";
function validatePatch(source, patch, options = {}) {
const validation = {
canApply: false,
requiredFuzzFactor: 0,
issues: []
};
// Try applying with increasing fuzz factors
for (let fuzz = 0; fuzz <= 10; fuzz++) {
const result = applyPatch(source, patch, {
...options,
fuzzFactor: fuzz
});
if (result !== false) {
validation.canApply = true;
validation.requiredFuzzFactor = fuzz;
validation.result = result;
break;
}
}
if (!validation.canApply) {
validation.issues.push("Patch cannot be applied even with maximum fuzz factor");
} else if (validation.requiredFuzzFactor > 0) {
validation.issues.push(`Requires fuzz factor of ${validation.requiredFuzzFactor}`);
}
return validation;
}
// Usage
const patchValidation = validatePatch(sourceCode, patch);
if (patchValidation.canApply) {
console.log("Patch can be applied");
if (patchValidation.requiredFuzzFactor > 0) {
console.log(`Warning: Requires fuzzy matching (${patchValidation.requiredFuzzFactor})`);
}
} else {
console.log("Patch cannot be applied:", patchValidation.issues);
}
Error Handling and Recovery
import { applyPatch } from "diff";
function robustPatchApplication(source, patch, options = {}) {
const attempts = [
// First attempt: strict application
{ ...options, fuzzFactor: 0 },
// Second attempt: allow minor mismatches
{ ...options, fuzzFactor: 2 },
// Third attempt: more tolerant matching
{
...options,
fuzzFactor: 5,
compareLine: (lineNumber, line, operation, patchContent) => {
return line.trim() === patchContent.trim();
}
},
// Last attempt: very fuzzy with case insensitive
{
...options,
fuzzFactor: 10,
compareLine: (lineNumber, line, operation, patchContent) => {
return line.toLowerCase().trim() === patchContent.toLowerCase().trim();
}
}
];
for (let i = 0; i < attempts.length; i++) {
const result = applyPatch(source, patch, attempts[i]);
if (result !== false) {
return {
success: true,
result: result,
attempt: i + 1,
strategy: attempts[i]
};
}
}
return {
success: false,
error: "All patch application strategies failed"
};
}
// Usage
const robustResult = robustPatchApplication(sourceText, problemPatch);
if (robustResult.success) {
console.log(`Patch applied using strategy ${robustResult.attempt}`);
} else {
console.error("Patch could not be applied:", robustResult.error);
}
Version Control Integration
import { applyPatch } from "diff";
import { execSync } from "child_process";
function applyGitPatch(patchFile, options = {}) {
try {
// Try using git apply first
execSync(`git apply --check ${patchFile}`);
execSync(`git apply ${patchFile}`);
return { success: true, method: 'git' };
} catch (gitError) {
console.log("Git apply failed, trying manual application...");
// Fallback to manual patch application
const patchContent = fs.readFileSync(patchFile, 'utf8');
const patches = parsePatch(patchContent);
const results = [];
for (const patch of patches) {
const sourceContent = fs.readFileSync(patch.oldFileName, 'utf8');
const result = applyPatch(sourceContent, patch, {
fuzzFactor: 3,
...options
});
if (result !== false) {
fs.writeFileSync(patch.oldFileName, result);
results.push({ file: patch.oldFileName, success: true });
} else {
results.push({ file: patch.oldFileName, success: false });
}
}
return {
success: results.every(r => r.success),
method: 'manual',
results: results
};
}
}