UNPKG

taylo

Version:

Make changes to a branch a plugin. A command-line tool to manage and apply plugins '.taylored'. Supports applying, removing, verifying plugins, and generating them from branch (GIT).

323 lines (322 loc) 13.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.GitExecutionError = void 0; exports.updatePatchOffsets = updatePatchOffsets; exports.parsePatchHunks = parsePatchHunks; exports.quoteForShell = quoteForShell; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const child_process_1 = require("child_process"); const util = __importStar(require("util")); const apply_logic_1 = require("./apply-logic"); const constants_1 = require("./constants"); const utils_1 = require("./utils"); const execAsync = util.promisify(child_process_1.exec); function quoteForShell(arg) { if (!/[ \t\n\r"'();&|<>*?#~=%\\]/.test(arg)) { return arg; } const escaped = arg .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .replace(/`/g, '\\`') .replace(/\$/g, '\\$'); return `"${escaped}"`; } class GitExecutionError extends Error { originalError; stdout; stderr; code; constructor(message, originalError) { super(message); this.name = 'GitExecutionError'; if (originalError) { this.originalError = originalError; this.stdout = originalError.stdout?.trim(); this.stderr = originalError.stderr?.trim(); this.code = originalError.code; } } } exports.GitExecutionError = GitExecutionError; async function execGit(repoRoot, args, options = {}) { const command = `git ${args.map(quoteForShell).join(' ')}`; const execOptions = { cwd: repoRoot, ...options.execOptions, }; try { const { stdout, stderr } = await execAsync(command, execOptions); return { stdout: stdout.trim(), stderr: stderr.trim(), success: true }; } catch (error) { if (options.allowFailure) { return { stdout: error.stdout ? error.stdout.trim() : '', stderr: error.stderr ? error.stderr.trim() : '', success: false, error: error, }; } const errorMessage = `Error executing git command: ${command}\nRepo: ${repoRoot}\nExit Code: ${error.code}\nStdout: ${error.stdout ? error.stdout.trim() : 'N/A'}\nStderr: ${error.stderr ? error.stderr.trim() : 'N/A'}`; throw new GitExecutionError(errorMessage, error); } } function parsePatchHunks(patchContent) { if (!patchContent) { return []; } const hunks = []; const lines = patchContent.split('\n'); const hunkHeaderRegex = /^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@/; for (const line of lines) { const match = line.match(hunkHeaderRegex); if (match) { const oldStart = parseInt(match[1], 10); const oldLines = match[3] !== undefined ? parseInt(match[3], 10) : 1; const newStart = parseInt(match[4], 10); const newLines = match[6] !== undefined ? parseInt(match[6], 10) : 1; hunks.push({ originalHeaderLine: line, oldStart, oldLines, newStart, newLines, }); } } return hunks; } function embedMessageInContent(diffBody, message) { const trimmedDiffBody = diffBody.trim(); if (message) { const subjectLine = `Subject: [PATCH] ${message}`; let content = subjectLine; if (trimmedDiffBody) { content += `\n\n${trimmedDiffBody}`; } if (!content.endsWith('\n')) { content += '\n'; } return content; } if (trimmedDiffBody === '') { return ''; } let content = trimmedDiffBody; if (!content.endsWith('\n')) { content += '\n'; } return content; } function getActualDiffBody(patchFileContent) { const lines = patchFileContent.split('\n'); if (lines.length > 0 && lines[0].startsWith('Subject: [PATCH]')) { let firstBlankLineIndex = -1; for (let i = 1; i < lines.length; i++) { if (lines[i].trim() === '') { firstBlankLineIndex = i; break; } } if (firstBlankLineIndex !== -1 && firstBlankLineIndex + 1 < lines.length) { return lines.slice(firstBlankLineIndex + 1).join('\n'); } return ''; } return patchFileContent; } async function updatePatchOffsets(patchFileName, repoRoot, _customCommitMessage, branchName) { const baseBranch = branchName || 'main'; const statusResult = await execGit(repoRoot, ['status', '--porcelain']); if (statusResult.stdout.trim() !== '') { throw new Error('CRITICAL ERROR: Uncommitted changes detected in the repository. Please commit or stash them before running --offset.\n' + statusResult.stdout); } const absolutePatchFilePath = path.join(repoRoot, constants_1.TAYLORED_DIR_NAME, patchFileName); if (!fs.existsSync(absolutePatchFilePath) || !fs.statSync(absolutePatchFilePath).isFile()) { throw new Error(`Patch file '${absolutePatchFilePath}' not found or is not a file.`); } const baseBranchExistsResult = await execGit(repoRoot, ['rev-parse', '--verify', baseBranch], { allowFailure: true, ignoreStderr: true }); if (!baseBranchExistsResult.success) { throw new GitExecutionError(`CRITICAL ERROR: The base branch '${baseBranch}' does not exist in the repository. Cannot calculate diff against '${baseBranch}'.`, baseBranchExistsResult.error); } let originalBranchOrCommit = ''; try { const symbolicRefResult = await execGit(repoRoot, ['symbolic-ref', '--short', 'HEAD'], { allowFailure: true, ignoreStderr: true }); if (symbolicRefResult.success && symbolicRefResult.stdout) { originalBranchOrCommit = symbolicRefResult.stdout; } else { originalBranchOrCommit = (await execGit(repoRoot, ['rev-parse', 'HEAD'])) .stdout; } if (!originalBranchOrCommit) { throw new Error('Could not determine the current branch or commit.'); } } catch (e) { throw new GitExecutionError(`Failed to determine current branch/commit: ${e.message}`, e); } const tempBranchName = `temp/offset-automation-${Date.now()}`; let operationSucceeded = false; let cliEquivalentCallSucceeded = false; let finalOutputContentToWrite = null; try { await execGit(repoRoot, [ 'checkout', '-b', tempBranchName, originalBranchOrCommit, '--quiet', ]); try { await (0, apply_logic_1.handleApplyOperation)(patchFileName, false, true, '--remove (invoked by offset)', repoRoot); cliEquivalentCallSucceeded = true; } catch (removeError) { try { await (0, apply_logic_1.handleApplyOperation)(patchFileName, false, false, '--add (invoked by offset, after remove failed)', repoRoot); cliEquivalentCallSucceeded = true; } catch (addError) { cliEquivalentCallSucceeded = false; } } if (cliEquivalentCallSucceeded) { await execGit(repoRoot, ['add', '.']); const tempCommitMessageText = 'Internal: Staged changes for offset update'; await execGit(repoRoot, [ 'commit', '--allow-empty', '-m', tempCommitMessageText, '--quiet', ]); const tayloredDirPath = path.join(repoRoot, constants_1.TAYLORED_DIR_NAME); await fs.ensureDir(tayloredDirPath); const diffCmdResult = await execGit(repoRoot, ['diff', baseBranch, 'HEAD'], { allowFailure: true }); const originalPatchContent = await fs.readFile(absolutePatchFilePath, 'utf-8'); const rawNewDiffContent = diffCmdResult.stdout || ''; const effectiveMessageToEmbed = (0, utils_1.extractMessageFromPatch)(originalPatchContent); if (diffCmdResult.error && diffCmdResult.error.code !== 0 && diffCmdResult.error.code !== 1) { console.error(`ERROR: Execution of 'git diff ${baseBranch} HEAD' command failed with an unexpected exit code ${diffCmdResult.error.code} on the temporary branch.`); if (diffCmdResult.stderr) console.error(` Stderr: ${diffCmdResult.stderr}`); } else { const originalHunks = parsePatchHunks(originalPatchContent); const newHunks = parsePatchHunks(rawNewDiffContent); let allHunksAreConsideredInverted = false; if (originalHunks.length > 0 && originalHunks.length === newHunks.length) { let numInvertedHunks = 0; for (let i = 0; i < originalHunks.length; i++) { const origHunk = originalHunks[i]; const newHunk = newHunks[i]; if (newHunk.oldStart === origHunk.newStart && newHunk.oldLines === origHunk.newLines && newHunk.newStart === origHunk.oldStart && newHunk.newLines === origHunk.oldLines && origHunk.oldLines !== origHunk.newLines) { numInvertedHunks++; } } if (numInvertedHunks > 0 && numInvertedHunks === originalHunks.length) { allHunksAreConsideredInverted = true; } } const cleanedDiffContent = rawNewDiffContent .split('\n') .map((line) => line.trimEnd()) .join('\n'); if (allHunksAreConsideredInverted) { const bodyOfOriginalPatch = getActualDiffBody(originalPatchContent); finalOutputContentToWrite = embedMessageInContent(bodyOfOriginalPatch, effectiveMessageToEmbed); } else { finalOutputContentToWrite = embedMessageInContent(cleanedDiffContent, effectiveMessageToEmbed); } operationSucceeded = true; } } else { console.error(`ERROR: Preliminary internal apply/remove operations for '${patchFileName}' failed on the temporary branch.`); } } catch (error) { console.error(`CRITICAL ERROR during offset update process: ${error.message}`); if (error instanceof GitExecutionError && error.stderr) { console.error(`Git STDERR: ${error.stderr}`); } operationSucceeded = false; } finally { try { await execGit(repoRoot, [ 'checkout', '--force', originalBranchOrCommit, '--quiet', ]); const tempBranchExistsResult = await execGit(repoRoot, ['rev-parse', '--verify', tempBranchName], { allowFailure: true, ignoreStderr: true }); if (tempBranchExistsResult.success) { await execGit(repoRoot, ['branch', '-D', tempBranchName, '--quiet']); } } catch (cleanupErr) { console.warn(`Warning: Failed to cleanup temporary branch: ${cleanupErr.message}`); } } if (operationSucceeded && finalOutputContentToWrite !== null) { try { await fs.writeFile(absolutePatchFilePath, finalOutputContentToWrite); console.log(`Successfully updated patch file: ${patchFileName}`); } catch (writeError) { throw new Error(`Failed to write updated patch content to ${absolutePatchFilePath}. Error: ${writeError.message}`); } } else if (!operationSucceeded) { throw new Error(`WARNING: The taylored file '${patchFileName}' is obsolete or could not be processed for offset update.`); } return { outputPath: absolutePatchFilePath }; }