radashi-helper
Version:
Help with managing your own Radashi
364 lines (356 loc) • 10.5 kB
JavaScript
import {
dedent
} from "./chunk-V32CUL4H.js";
import {
addOverride,
assertRepoClean
} from "./chunk-QRYWERVT.js";
import "./chunk-GDVGEOPF.js";
import {
loadRewired
} from "./chunk-OCHPTCP5.js";
import {
pullRadashi
} from "./chunk-FEUFDYD5.js";
import {
botCommit
} from "./chunk-CQEIEK2X.js";
import "./chunk-GGEXN2BI.js";
import {
findSources
} from "./chunk-652CQLOF.js";
import "./chunk-BKS73LZK.js";
import {
prompt
} from "./chunk-MJC327DM.js";
import {
debug,
getEnv,
isPreReleaseRadashiTag
} from "./chunk-DSXY7YVB.js";
import {
EarlyExitError,
RadashiError,
exec,
forwardStderrAndRethrow,
log
} from "./chunk-JLKIE3A2.js";
import {
__commonJS,
__toESM
} from "./chunk-VOEQFXI5.js";
// ../../node_modules/.pnpm/glob-regex@0.3.2/node_modules/glob-regex/index.js
var require_glob_regex = __commonJS({
"../../node_modules/.pnpm/glob-regex@0.3.2/node_modules/glob-regex/index.js"(exports, module) {
"use strict";
var dotRE = /\./g;
var dotPattern = "\\.";
var restRE = /\*\*$/g;
var restPattern = "(.+)";
var globRE = /(?:\*\*\/|\*\*|\*)/g;
var globPatterns = {
"*": "([^/]+)",
// no backslashes
"**": "(.+/)?([^/]+)",
// short for "**/*"
"**/": "(.+/)?"
// one or more directories
};
function mapToPattern(str) {
return globPatterns[str];
}
function replace(glob) {
return glob.replace(dotRE, dotPattern).replace(restRE, restPattern).replace(globRE, mapToPattern);
}
function join3(globs) {
return "((" + globs.map(replace).join(")|(") + "))";
}
function globRegex2(glob) {
return new RegExp("^" + (Array.isArray(glob) ? join3 : replace)(glob) + "$");
}
globRegex2.replace = replace;
Object.defineProperty(globRegex2, "default", { value: globRegex2 });
module.exports = globRegex2;
}
});
// src/pr-import.ts
var import_glob_regex = __toESM(require_glob_regex(), 1);
import { yellow } from "kleur/colors";
import { existsSync } from "node:fs";
import { copyFile, mkdir } from "node:fs/promises";
import { basename, dirname, extname, join as join2, relative } from "node:path";
// src/rewired/undoRewire.ts
import fs, { writeFile } from "node:fs/promises";
import { join } from "node:path";
async function undoRewire(funcPath, env) {
debug(`Removing rewired file for "${funcPath}"`);
const rewiredFile = join(env.overrideDir, "rewired", funcPath + ".ts");
await fs.rm(rewiredFile);
const rewired = await loadRewired(env);
const newRewired = rewired.filter((path) => path !== funcPath);
await writeFile(rewiredFile, JSON.stringify(newRewired, null, 2));
}
// src/util/checkCommand.ts
import { exec as exec2 } from "node:child_process";
async function checkCommand(cmd) {
return new Promise((resolve) => {
exec2(`command -v ${cmd}`, (error) => {
resolve(!error);
});
});
}
// src/pr-import.ts
async function importPullRequest(prNumber, options = {}) {
if (Number.isNaN(+prNumber)) {
throw new RadashiError(`Invalid PR number "${prNumber}"`);
}
if (!await checkCommand("gh")) {
throw new RadashiError(
dedent`
gh command is not installed.
You can install it using Homebrew:
brew install gh
Or using the official website:
https://cli.github.com/
`
);
}
const env = options.env ?? getEnv(options.dir);
if (!env.radashiDir) {
throw new RadashiError("No upstream repository exists");
}
await assertRepoClean(env.root);
await pullRadashi(env);
await exec("git", ["branch", "-D", "pr-" + prNumber], {
cwd: env.radashiDir,
reject: false
});
log("Checking out PR...");
await exec("gh", ["pr", "checkout", prNumber, "-b", "pr-" + prNumber], {
cwd: env.radashiDir
}).catch((error) => {
log.error(error.stderr);
let reason = "";
if (error.message.includes("Could not resolve")) {
reason = `Could not find any PR with number ${prNumber}.`;
} else if (error.message.includes("couldn't find remote ref")) {
reason = "The author appears to have deleted the PR branch.";
}
throw new RadashiError(
"Failed to checkout PR" + (reason ? `: ${reason}` : "")
);
});
const targetBranch = await getTargetBranch(env);
debug("Target branch of the PR:", targetBranch);
await exec(
"git",
["fetch"].concat(
targetBranch.includes("/") ? targetBranch.split("/") : ["origin", targetBranch]
),
{
cwd: env.radashiDir
}
).catch(forwardStderrAndRethrow);
const baseCommit = await exec("git", ["merge-base", "HEAD", targetBranch], {
cwd: env.radashiDir
}).then((r) => r.stdout);
debug("Base commit of the PR:", baseCommit);
const changes = await parseGitDiff(baseCommit, {
cwd: env.radashiDir
});
debug("Parsed changes from git diff:", changes);
const pathsIn = await findSources(env);
const srcGlob = (0, import_glob_regex.default)("src/*/*.ts");
changes.sort((a, b) => {
return srcGlob.test(a.file) && !srcGlob.test(b.file) ? -1 : srcGlob.test(b.file) && !srcGlob.test(a.file) ? 1 : a.file.localeCompare(b.file);
});
const addedFiles = [];
const modifiedFiles = [];
for (const change of changes) {
const file = basename(change.file, extname(change.file));
if (options.files && !options.files.includes(file)) {
continue;
}
if (change.status === "A") {
addedFiles.push(change.file);
if (change.file.startsWith("src/")) {
const srcPath = join2(env.root, change.file);
if (pathsIn.src.includes(srcPath)) {
throw new RadashiError(
`Cannot import PR. File named "${change.file}" is already a source file created by you.`
);
}
}
} else if (change.status === "M" && change.file !== "src/mod.ts") {
modifiedFiles.push(change.file);
if (change.file.startsWith("src/")) {
const overridePath = join2(
env.root,
change.file.replace("src/", "overrides/src/")
);
if (pathsIn.overrides.includes(overridePath)) {
throw new RadashiError(
`Cannot import PR. File named "${change.file}" already exists in the overrides folder.`
);
}
const funcPath = relative(env.overrideDir, overridePath).slice(0, -3);
const rewiredPath = overridePath.replace("/src/", "/rewired/");
if (pathsIn.rewired.includes(rewiredPath)) {
await undoRewire(funcPath, env);
}
await addOverride(funcPath, {
env,
exactMatch: true,
fromBranch: baseCommit
});
}
}
}
for (const file of addedFiles) {
debug(`Adding "${file}" to project`);
await tryCopyFile(join2(env.radashiDir, file), join2(env.root, file));
}
for (const file of modifiedFiles) {
debug(`Modifying "${file}" override in project`);
await tryCopyFile(
join2(env.radashiDir, file),
join2(env.root, "overrides", file)
);
}
let prTitle = await exec(
"gh",
["pr", "view", "--json", "title", "--jq", ".title"],
{ cwd: env.radashiDir }
).then((result) => result.stdout.trim());
const validTitleRE = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([^):]+\))?: /;
if (!validTitleRE.test(prTitle)) {
log.error("");
log.error(
yellow("ATTN"),
"The PR title does not follow the Conventional Commits format."
);
log.error("Please select the type of change this PR introduces:\n");
const type = await prompt({
type: "autocomplete",
name: "type",
message: "Select the type of change:",
choices: getConventionalCommitTypes()
});
if (!type) {
throw new EarlyExitError("No change type selected. Exiting...");
}
const description = await prompt({
type: "text",
name: "description",
message: "Enter a short description of the change:"
});
if (!description) {
throw new EarlyExitError("No description provided. Exiting...");
}
prTitle = `${type}: ${description}`;
}
const { default: build } = await import("./build-7F3PTR5C.js");
await build({ env });
log("");
await botCommit(prTitle, {
cwd: env.root,
add: ["-A"]
});
}
async function tryCopyFile(src, dst) {
if (existsSync(src)) {
try {
await mkdir(dirname(dst), { recursive: true });
await copyFile(src, dst);
return true;
} catch {
}
}
return false;
}
async function parseGitDiff(ref, opts) {
const { stdout: nameStatus } = await exec(
"git",
["diff", ref, "--name-status"],
opts
);
return nameStatus.trim().split("\n").map((line) => {
const [status, file] = line.split(" ");
return { status, file };
});
}
async function getTargetBranch(env) {
const { stdout } = await exec(
"gh",
["pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"],
{ cwd: env.radashiDir }
);
const targetBranch = stdout?.trim() ?? "main";
const radashiRef = await env.radashiRef;
if (isPreReleaseRadashiTag(radashiRef)) {
return "radashi/" + targetBranch;
}
return targetBranch;
}
function getConventionalCommitTypes() {
return [
{
title: "feat",
description: "A new feature",
value: "feat"
},
{
title: "fix",
description: "A bug fix",
value: "fix"
},
{
title: "docs",
description: "Documentation only changes",
value: "docs"
},
{
title: "style",
description: "Changes that do not affect the meaning of the code",
value: "style"
},
{
title: "refactor",
description: "A code change that neither fixes a bug nor adds a feature",
value: "refactor"
},
{
title: "perf",
description: "A code change that improves performance",
value: "perf"
},
{
title: "test",
description: "Adding missing tests or correcting existing tests",
value: "test"
},
{
title: "build",
description: "Changes that affect the build system or external dependencies",
value: "build"
},
{
title: "ci",
description: "Changes to our CI configuration files and scripts",
value: "ci"
},
{
title: "chore",
description: "Other changes that don't modify src or test files",
value: "chore"
},
{
title: "revert",
description: "Reverts a previous commit",
value: "revert"
}
];
}
export {
importPullRequest
};