When applying Git patches, use applyPatches with a pattern that handles file renames and deletions using a pendingWrites Map. This prevents issues with file swaps (e.g., a -> b and b -> a) and ensures renames are handled correctly.
Key properties available on the patch object:
patch.isRename: Boolean indicating a rename.patch.isCopy: Boolean indicating a copy.patch.isDelete: Boolean indicating a deletion.patch.isCreate: Boolean indicating a new file creation.patch.oldFileName: The original file path (often prefixed with a/ in Git).patch.newFileName: The new file path (often prefixed with b/ in Git).patch.newMode: The new file mode/permissions.
const {applyPatches} = require('diff');
const fs = require('fs'); // Note: fs must be required
const patch = fs.readFileSync("git-diff.patch").toString();
const DELETE = Symbol('delete');
const pendingWrites = new Map(); // filePath → {content, mode} or DELETE sentinel
applyPatches(patch, {
loadFile: (patch, callback) => {
if (patch.isCreate) {
// Newly created file — no old content to load
callback(undefined, '');
return;
}
try {
// Git diffs use a/ and b/ prefixes; strip them to get the real path
const filePath = patch.oldFileName.replace(/^a\//, '');
callback(undefined, fs.readFileSync(filePath).toString());
} catch (e) {
callback(`No such file: ${patch.oldFileName}`);
}
},
patched: (patch, patchedContent, callback) => {
if (patchedContent === false) {
callback(`Failed to apply patch to ${patch.oldFileName}`);
return;
}
const oldPath = patch.oldFileName.replace(/^a\//, '');
const newPath = patch.newFileName.replace(/^b\//, '');
if (patch.isDelete) {
if (!pendingWrites.has(oldPath)) {
pendingWrites.set(oldPath, DELETE);
}
} else {
pendingWrites.set(newPath, {content: patchedContent, mode: patch.newMode});
// For renames, delete the old file (but not for copies,
// where the old file should be kept)
if (patch.isRename && !pendingWrites.has(oldPath)) {
pendingWrites.set(oldPath, DELETE);
}
}
callback();
},
complete: (err) => {
if (err) {
console.log("Failed with error:", err);
return;
}
for (const [filePath, entry] of pendingWrites) {
if (entry === DELETE) {
fs.unlinkSync(filePath);
} else {
fs.writeFileSync(filePath, entry.content);
if (entry.mode) {
fs.chmodSync(filePath, entry.mode.slice(-3));
}
}
}
}
});