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
JavaScript
;
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 };
}