From 1f2734921924b029f5aefaef20392d705e8d623c Mon Sep 17 00:00:00 2001 From: David Sheldrick Date: Thu, 13 Jul 2023 16:37:17 +0100 Subject: [PATCH] add rebase command to undo patches --- src/applyPatches.ts | 20 ++-- src/index.ts | 28 ++++- src/makePatch.ts | 6 +- src/rebase.ts | 244 ++++++++++++++++++++++++++++++++++++++++++++ src/stateFile.ts | 35 +++++-- 5 files changed, 312 insertions(+), 21 deletions(-) create mode 100644 src/rebase.ts diff --git a/src/applyPatches.ts b/src/applyPatches.ts index 7a856934..af4c599a 100644 --- a/src/applyPatches.ts +++ b/src/applyPatches.ts @@ -283,28 +283,32 @@ export function applyPatchesForApp({ ) } - savePatchApplicationState( - patches[0], - patches.slice(0, lastReversedPatchIndex).map((patch) => ({ + savePatchApplicationState({ + packageDetails: patches[0], + patches: patches.slice(0, lastReversedPatchIndex).map((patch) => ({ didApply: true, patchContentHash: hashFile( join(appPath, patchDir, patch.patchFilename), ), patchFilename: patch.patchFilename, })), - ) + isRebasing: false, + }) } } else { - savePatchApplicationState( - patches[0], - appliedPatches.map((patch) => ({ + const allPatchesSucceeded = + unappliedPatches.length === appliedPatches.length + savePatchApplicationState({ + packageDetails: patches[0], + patches: appliedPatches.map((patch) => ({ didApply: true, patchContentHash: hashFile( join(appPath, patchDir, patch.patchFilename), ), patchFilename: patch.patchFilename, })), - ) + isRebasing: !allPatchesSucceeded, + }) } } } diff --git a/src/index.ts b/src/index.ts index 886e7189..703a45ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { join } from "./path" import { normalize, sep } from "path" import slash = require("slash") import { isCI } from "ci-info" +import { rebase } from "./rebase" const appPath = getAppRootPath() const argv = minimist(process.argv.slice(2), { @@ -25,7 +26,7 @@ const argv = minimist(process.argv.slice(2), { "create-issue", "", ], - string: ["patch-dir", "append"], + string: ["patch-dir", "append", "rebase"], }) const packageNames = argv._ @@ -44,7 +45,30 @@ if (argv.version || argv.v) { if (patchDir.startsWith("/")) { throw new Error("--patch-dir must be a relative path") } - if (packageNames.length) { + if ("rebase" in argv) { + if (!argv.rebase) { + console.error( + chalk.red( + "You must specify a patch file name or number when rebasing patches", + ), + ) + process.exit(1) + } + if (packageNames.length !== 1) { + console.error( + chalk.red( + "You must specify exactly one package name when rebasing patches", + ), + ) + process.exit(1) + } + rebase({ + appPath, + packagePathSpecifier: packageNames[0], + patchDir, + targetPatch: argv.rebase, + }) + } else if (packageNames.length) { const includePaths = makeRegExp( argv.include, "include", diff --git a/src/makePatch.ts b/src/makePatch.ts index 499ee63f..a14d772c 100644 --- a/src/makePatch.ts +++ b/src/makePatch.ts @@ -415,7 +415,11 @@ export function makePatch({ }, ] if (nextState.length > 1) { - savePatchApplicationState(packageDetails, nextState) + savePatchApplicationState({ + packageDetails, + patches: nextState, + isRebasing: false, + }) } else { clearPatchApplicationState(packageDetails) } diff --git a/src/rebase.ts b/src/rebase.ts new file mode 100644 index 00000000..eb0d4d9a --- /dev/null +++ b/src/rebase.ts @@ -0,0 +1,244 @@ +import chalk from "chalk" +import { existsSync } from "fs" +import { join, resolve } from "path" +import { applyPatch } from "./applyPatches" +import { hashFile } from "./hash" +import { PatchedPackageDetails } from "./PackageDetails" +import { getGroupedPatches } from "./patchFs" +import { + getPatchApplicationState, + savePatchApplicationState, +} from "./stateFile" + +export function rebase({ + appPath, + patchDir, + packagePathSpecifier, + targetPatch, +}: { + appPath: string + patchDir: string + packagePathSpecifier: string + targetPatch: string +}): void { + const patchesDirectory = join(appPath, patchDir) + const groupedPatches = getGroupedPatches(patchesDirectory) + + if (groupedPatches.numPatchFiles === 0) { + console.error(chalk.blueBright("No patch files found")) + process.exit(1) + } + + const packagePatches = + groupedPatches.pathSpecifierToPatchFiles[packagePathSpecifier] + if (!packagePatches) { + console.error( + chalk.blueBright("No patch files found for package"), + packagePathSpecifier, + ) + process.exit(1) + } + + const state = getPatchApplicationState(packagePatches[0]) + + if (!state) { + console.error( + chalk.blueBright("No patch state found"), + "Did you forget to run", + chalk.bold("patch-package"), + "(without arguments) first?", + ) + process.exit(1) + } + if (state.isRebasing) { + console.error( + chalk.blueBright("Already rebasing"), + "Make changes to the files in", + chalk.bold(packagePatches[0].path), + "and then run `patch-package", + packagePathSpecifier, + "--continue` to", + packagePatches.length === state.patches.length + ? "append a patch file" + : `update the ${ + packagePatches[packagePatches.length - 1].patchFilename + } file`, + ) + console.error( + `💡 To remove a broken patch file, delete it and reinstall node_modules`, + ) + process.exit(1) + } + if (state.patches.length !== packagePatches.length) { + console.error( + chalk.blueBright("Some patches have not been applied."), + "Reinstall node_modules and try again.", + ) + } + // check hashes + for (let i = 0; i < state.patches.length; i++) { + const patch = state.patches[i] + const fullPatchPath = join( + patchesDirectory, + packagePatches[i].patchFilename, + ) + if (!existsSync(fullPatchPath)) { + console.error( + chalk.blueBright("Expected patch file"), + fullPatchPath, + "to exist but it is missing. Try completely reinstalling node_modules first.", + ) + process.exit(1) + } + if (patch.patchContentHash !== hashFile(fullPatchPath)) { + console.error( + chalk.blueBright("Patch file"), + fullPatchPath, + "has changed since it was applied. Try completely reinstalling node_modules first.", + ) + } + } + + if (targetPatch === "0") { + // unapply all + unApplyPatches({ + patches: packagePatches, + appPath, + patchDir, + }) + savePatchApplicationState({ + packageDetails: packagePatches[0], + isRebasing: true, + patches: [], + }) + console.log(` +Make any changes you need inside ${chalk.bold(packagePatches[0].path)} + +When you are done, run + + ${chalk.bold( + `patch-package ${packagePathSpecifier} --append 'MyChangeDescription'`, + )} + +to insert a new patch file. +`) + return + } + + // find target patch + const target = packagePatches.find((p) => { + if (p.patchFilename === targetPatch) { + return true + } + if ( + resolve(process.cwd(), targetPatch) === + join(patchesDirectory, p.patchFilename) + ) { + return true + } + + if (targetPatch === p.sequenceName) { + return true + } + const n = Number(targetPatch.replace(/^0+/g, "")) + if (!isNaN(n) && n === p.sequenceNumber) { + return true + } + return false + }) + + if (!target) { + console.error( + chalk.red("Could not find target patch file"), + chalk.bold(targetPatch), + ) + console.error() + console.error("The list of available patch files is:") + packagePatches.forEach((p) => { + console.error(` - ${p.patchFilename}`) + }) + + process.exit(1) + } + const currentHash = hashFile(join(patchesDirectory, target.patchFilename)) + + const prevApplication = state.patches.find( + (p) => p.patchContentHash === currentHash, + ) + if (!prevApplication) { + console.error( + chalk.red("Could not find previous application of patch file"), + chalk.bold(target.patchFilename), + ) + console.error() + console.error("You should reinstall node_modules and try again.") + process.exit(1) + } + + // ok, we are good to start undoing all the patches that were applied up to but not including the target patch + const targetIdx = state.patches.indexOf(prevApplication) + + unApplyPatches({ + patches: packagePatches.slice(targetIdx + 1), + appPath, + patchDir, + }) + savePatchApplicationState({ + packageDetails: packagePatches[0], + isRebasing: true, + patches: packagePatches.slice(0, targetIdx + 1).map((p) => ({ + patchFilename: p.patchFilename, + patchContentHash: hashFile(join(patchesDirectory, p.patchFilename)), + didApply: true, + })), + }) + + console.log(` +Make any changes you need inside ${chalk.bold(packagePatches[0].path)} + +When you are done, do one of the following: + + To update ${chalk.bold(packagePatches[targetIdx].patchFilename)} run + + ${chalk.bold(`patch-package ${packagePathSpecifier}`)} + + To create a new patch file after ${chalk.bold( + packagePatches[targetIdx].patchFilename, + )} run + + ${chalk.bold( + `patch-package ${packagePathSpecifier} --append 'MyChangeDescription'`, + )} + + `) +} + +function unApplyPatches({ + patches, + appPath, + patchDir, +}: { + patches: PatchedPackageDetails[] + appPath: string + patchDir: string +}) { + for (const patch of patches.slice().reverse()) { + if ( + !applyPatch({ + patchFilePath: join(appPath, patchDir, patch.patchFilename) as string, + reverse: true, + patchDetails: patch, + patchDir, + cwd: process.cwd(), + }) + ) { + console.error( + chalk.red("Failed to un-apply patch file"), + chalk.bold(patch.patchFilename), + "Try completely reinstalling node_modules.", + ) + process.exit(1) + } + console.log(chalk.green("Un-applied patch file"), patch.patchFilename) + } +} diff --git a/src/stateFile.ts b/src/stateFile.ts index 9bcd30f4..1c2677fd 100644 --- a/src/stateFile.ts +++ b/src/stateFile.ts @@ -8,10 +8,11 @@ export interface PatchState { didApply: true } -const version = 0 +const version = 1 export interface PatchApplicationState { version: number patches: PatchState[] + isRebasing: boolean } export const STATE_FILE_NAME = ".patch-package.json" @@ -21,25 +22,39 @@ export function getPatchApplicationState( ): PatchApplicationState | null { const fileName = join(packageDetails.path, STATE_FILE_NAME) + let state: null | PatchApplicationState = null try { - const state = JSON.parse(readFileSync(fileName, "utf8")) - if (state.version !== version) { - return null - } - return state + state = JSON.parse(readFileSync(fileName, "utf8")) } catch (e) { + // noop + } + if (!state) { return null } + if (state.version !== version) { + console.error( + `You upgraded patch-package and need to fully reinstall node_modules to continue.`, + ) + process.exit(1) + } + return state } -export function savePatchApplicationState( - packageDetails: PackageDetails, - patches: PatchState[], -) { + +export function savePatchApplicationState({ + packageDetails, + patches, + isRebasing, +}: { + packageDetails: PackageDetails + patches: PatchState[] + isRebasing: boolean +}) { const fileName = join(packageDetails.path, STATE_FILE_NAME) const state: PatchApplicationState = { patches, version, + isRebasing, } writeFileSync(fileName, stringify(state, { space: 4 }), "utf8")