chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Thin wrappers around `gh` and `git` CLIs.
|
||||
* Functions return parsed JSON or strings. They throw on non-zero exit.
|
||||
* Tests inject fakes via the `deps` parameter pattern (see classify.mjs etc.).
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
function runJson(cmd, args) {
|
||||
const out = execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
||||
try {
|
||||
return JSON.parse(out);
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to parse JSON from ${cmd}: ${e.message}\nOutput (first 200 chars): ${out.slice(0, 200)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runText(cmd, args) {
|
||||
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
||||
}
|
||||
|
||||
export function ghIssueListNumbers(owner, repo, label) {
|
||||
return runJson("gh", [
|
||||
"issue",
|
||||
"list",
|
||||
"--repo",
|
||||
`${owner}/${repo}`,
|
||||
"--state",
|
||||
"open",
|
||||
"-l",
|
||||
label,
|
||||
"--limit",
|
||||
"500",
|
||||
"--json",
|
||||
"number",
|
||||
]).map((x) => x.number);
|
||||
}
|
||||
|
||||
export function ghIssueListFeatureTitled(owner, repo) {
|
||||
const items = runJson("gh", [
|
||||
"issue",
|
||||
"list",
|
||||
"--repo",
|
||||
`${owner}/${repo}`,
|
||||
"--state",
|
||||
"open",
|
||||
"--limit",
|
||||
"500",
|
||||
"--json",
|
||||
"number,title",
|
||||
]);
|
||||
return items.filter((i) => /\[feature\]|feature request/i.test(i.title)).map((i) => i.number);
|
||||
}
|
||||
|
||||
export function ghIssueView(owner, repo, number) {
|
||||
return runJson("gh", [
|
||||
"issue",
|
||||
"view",
|
||||
String(number),
|
||||
"--repo",
|
||||
`${owner}/${repo}`,
|
||||
"--json",
|
||||
"number,title,url,body,state,stateReason,labels,author,assignees,createdAt,closedAt,comments,reactionGroups",
|
||||
]);
|
||||
}
|
||||
|
||||
export function ghPrSearchOpen(owner, repo, issueNumber) {
|
||||
return runJson("gh", [
|
||||
"pr",
|
||||
"list",
|
||||
"--repo",
|
||||
`${owner}/${repo}`,
|
||||
"--state",
|
||||
"open",
|
||||
"--search",
|
||||
`#${issueNumber}`,
|
||||
"--json",
|
||||
"number,title,body",
|
||||
"--limit",
|
||||
"10",
|
||||
]);
|
||||
}
|
||||
|
||||
export function ghPrSearchMerged(owner, repo, issueNumber) {
|
||||
return runJson("gh", [
|
||||
"pr",
|
||||
"list",
|
||||
"--repo",
|
||||
`${owner}/${repo}`,
|
||||
"--state",
|
||||
"merged",
|
||||
"--search",
|
||||
`#${issueNumber}`,
|
||||
"--json",
|
||||
"number,title,body,mergedAt,mergeCommit",
|
||||
"--limit",
|
||||
"20",
|
||||
]);
|
||||
}
|
||||
|
||||
export function gitTagsByDate() {
|
||||
const out = runText("git", [
|
||||
"tag",
|
||||
"--sort=creatordate",
|
||||
"--format=%(creatordate:iso8601)|%(refname:short)",
|
||||
]);
|
||||
return out
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [date, name] = line.split("|");
|
||||
return { date: new Date(date), name };
|
||||
})
|
||||
.filter((t) => /^v\d+\.\d+\.\d+$/.test(t.name));
|
||||
}
|
||||
|
||||
export function gitLogGrep(pattern) {
|
||||
try {
|
||||
const out = runText("git", [
|
||||
"log",
|
||||
"--all",
|
||||
`--grep=${pattern}`,
|
||||
"--format=%H|%cI|%s",
|
||||
"--regexp-ignore-case",
|
||||
]);
|
||||
if (!out) return [];
|
||||
return out.split("\n").map((line) => {
|
||||
const [hash, date, ...rest] = line.split("|");
|
||||
return { hash, date: new Date(date), subject: rest.join("|") };
|
||||
});
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function gitIsAncestor(hash, ref) {
|
||||
try {
|
||||
execFileSync("git", ["merge-base", "--is-ancestor", hash, ref], { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function gitCurrentReleaseBranch() {
|
||||
try {
|
||||
const out = runText("git", ["branch", "--format=%(refname:short)"]);
|
||||
const branches = out.split("\n").filter((b) => /^release\/v\d+\.\d+\.\d+$/.test(b));
|
||||
if (branches.length === 0) return null;
|
||||
const current = runText("git", ["branch", "--show-current"]);
|
||||
if (branches.includes(current)) return current;
|
||||
branches.sort((a, b) => {
|
||||
const av = a.replace("release/v", "").split(".").map(Number);
|
||||
const bv = b.replace("release/v", "").split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (av[i] !== bv[i]) return bv[i] - av[i];
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return branches[0];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultDeps = {
|
||||
ghIssueListNumbers,
|
||||
ghIssueListFeatureTitled,
|
||||
ghIssueView,
|
||||
ghPrSearchMerged,
|
||||
ghPrSearchOpen,
|
||||
gitTagsByDate,
|
||||
gitLogGrep,
|
||||
gitIsAncestor,
|
||||
gitCurrentReleaseBranch,
|
||||
};
|
||||
Reference in New Issue
Block a user