Merge pull request #611 from pikasTech/fix/gh-merge-table-output-20260621

fix: render gh merge result as table
This commit is contained in:
Lyon
2026-06-21 23:48:20 +08:00
committed by GitHub
+123 -10
View File
@@ -5131,7 +5131,7 @@ async function prMerge(repo: string, token: string, number: number, options: Git
if (isGitHubError(pr)) return commandError("pr merge", repo, pr, { number, phase: "fetch-pr" });
const summary = prSummary(pr);
if (summary.merged === true) {
return {
return withPrMergeRendered({
ok: true,
command: "pr merge",
repo,
@@ -5141,23 +5141,24 @@ async function prMerge(repo: string, token: string, number: number, options: Git
pullRequest: summary,
branchDeletion: { attempted: false, skippedReason: "already-merged" },
rest: true,
};
});
}
const metadata = await prGraphqlMetadata(repo, token, number);
if (isGitHubError(metadata)) return commandError("pr merge", repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest: summary });
const statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, false);
const mergeability = prCloseoutSummary(summary, prMetadataSummary(metadata), statusChecks);
if (mergeability.readyForCommanderMerge !== true) {
return validationError("pr merge", repo, "PR is not ready for merge; preflight blockers or pending states remain", {
const details = {
number,
pullRequest: preflightPullRequestSummary(summary),
mergeability,
statusChecks,
closeoutMetadata: prCloseoutMetadata(metadata),
});
};
return withPrMergeRendered(validationError("pr merge", repo, "PR is not ready for merge; preflight blockers or pending states remain", details), details);
}
if (options.dryRun) {
return {
return withPrMergeRendered({
ok: true,
command: "pr merge",
repo,
@@ -5169,7 +5170,7 @@ async function prMerge(repo: string, token: string, number: number, options: Git
pullRequest: preflightPullRequestSummary(summary),
mergeability,
statusChecks,
};
});
}
const merged = await githubRequest<Record<string, unknown>>(token, "PUT", `/repos/${owner}/${name}/pulls/${number}/merge`, {
merge_method: options.mergeMethod,
@@ -5178,7 +5179,7 @@ async function prMerge(repo: string, token: string, number: number, options: Git
const after = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
if (isGitHubError(after)) return commandError("pr merge", repo, after, { number, phase: "fetch-after-merge", mergeResult: merged });
const branchDeletion = options.deleteBranch ? await deleteHeadBranchAfterMerge(repo, token, after) : { attempted: false, skippedReason: "delete-branch-not-requested" };
return {
return withPrMergeRendered({
ok: true,
command: "pr merge",
repo,
@@ -5188,9 +5189,77 @@ async function prMerge(repo: string, token: string, number: number, options: Git
pullRequest: prSummary(after),
branchDeletion,
rest: true,
});
}
function withPrMergeRendered(result: GitHubCommandResult, fallbackDetails?: Record<string, unknown>): GitHubCommandResult {
return {
...result,
contentType: "text/plain",
renderedText: renderPrMergeTable(result, fallbackDetails),
};
}
function renderPrMergeTable(result: GitHubCommandResult, fallbackDetails?: Record<string, unknown>): string {
const details = fallbackDetails ?? {};
const pullRequest = isRecord(result.pullRequest)
? result.pullRequest
: isRecord(details.pullRequest)
? details.pullRequest
: {};
const mergeability = isRecord(result.mergeability)
? result.mergeability
: isRecord(details.mergeability)
? details.mergeability
: {};
const statusChecks = isRecord(result.statusChecks)
? result.statusChecks
: isRecord(details.statusChecks)
? details.statusChecks
: {};
const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {};
const branchDeletion = isRecord(result.branchDeletion) ? result.branchDeletion : {};
const status = result.ok === true
? result.alreadyMerged === true
? "already-merged"
: result.dryRun === true
? "dry-run"
: "merged"
: ghText(mergeability.conclusion) === "pending"
? "pending"
: "blocked";
const blockers = Array.isArray(mergeability.blockers) ? mergeability.blockers.map(String) : [];
const pending = Array.isArray(mergeability.pending) ? mergeability.pending.map(String) : [];
const rows = [[
`#${ghText(result.number ?? details.number)}`,
status,
ghText(result.method ?? result.mergeMethod),
ghText(mergeability.mergeable),
ghText(mergeability.mergeStateStatus),
`${ghText(statusChecks.totalContexts)} total ${ghText(counts.success)} ok ${ghText(counts.failure)} fail ${ghText(counts.pending)} pending`,
]];
const lines = [
`gh pr merge (${status})`,
"",
ghTable(["PR", "STATUS", "METHOD", "MERGEABLE", "MERGE_STATE", "CHECKS"], rows),
"",
"Summary:",
` repo=${ghText(result.repo)} title=${ghShort(ghText(pullRequest.title), 96)}`,
` blockers=${blockers.length === 0 ? "-" : blockers.join(",")} pending=${pending.length === 0 ? "-" : pending.join(",")}`,
` branchDeletion=${ghText(branchDeletion.ok ?? branchDeletion.skippedReason ?? branchDeletion.attempted)}`,
"",
"Next:",
];
if (result.ok === true && result.alreadyMerged !== true && result.dryRun !== true) {
lines.push(` bun scripts/cli.ts gh pr view ${ghText(result.number ?? details.number)} --repo ${ghText(result.repo)}`);
} else {
lines.push(` bun scripts/cli.ts gh pr preflight ${ghText(result.number ?? details.number)} --repo ${ghText(result.repo)}`);
}
lines.push("", "Disclosure:");
lines.push(" default view is a bounded table; use --full or --raw on read/preflight commands for structured metadata.");
return lines.join("\n");
}
async function prPreflight(repo: string, number: number, commandName: "preflight" | "pr preflight" | "pr closeout", includeRaw: boolean): Promise<GitHubCommandResult> {
const auth = await authStatus(repo);
const authCapability = compactAuthCapability(auth);
@@ -5474,7 +5543,7 @@ async function prCreate(repo: string, token: string, options: GitHubOptions): Pr
const body = bodySource.body;
const planned = prCreatePlannedOperation(repo, options, body, bodySource.bodySource);
if (options.dryRun) {
return {
return withPrCreateRendered({
ok: true,
command: "pr create",
repo,
@@ -5482,7 +5551,7 @@ async function prCreate(repo: string, token: string, options: GitHubOptions): Pr
planned: true,
draft: options.draft,
...planned,
};
});
}
const repoResult = await repoInfo(token, repo);
@@ -5512,7 +5581,7 @@ async function prCreate(repo: string, token: string, options: GitHubOptions): Pr
};
const pr = await githubRequest<GitHubPullRequest>(token, "POST", `/repos/${owner}/${name}/pulls`, payload);
if (isGitHubError(pr)) return commandError("pr create", repo, pr, { base: options.base, head: options.head, planned });
return {
return withPrCreateRendered({
ok: true,
command: "pr create",
repo,
@@ -5534,9 +5603,53 @@ async function prCreate(repo: string, token: string, options: GitHubOptions): Pr
bodyChars: body.length,
},
rest: true,
});
}
function withPrCreateRendered(result: GitHubCommandResult): GitHubCommandResult {
return {
...result,
contentType: "text/plain",
renderedText: renderPrCreateTable(result),
};
}
function renderPrCreateTable(result: GitHubCommandResult): string {
const pr = isRecord(result.pr) ? result.pr : {};
const request = isRecord(result.request) ? result.request : {};
const number = ghText(pr.number ?? result.number ?? result.pr);
const title = ghText(pr.title ?? request.title ?? result.title);
const base = ghText(isRecord(pr.base) ? pr.base.ref : request.base ?? result.base);
const head = ghText(isRecord(pr.head) ? pr.head.ref : request.head ?? result.head);
const status = result.dryRun === true ? "dry-run" : "created";
const rows = [[
number === "-" ? "-" : `#${number}`,
status,
base,
head,
ghText(request.draft ?? result.draft),
ghShort(title, 80),
]];
const lines = [
`gh pr create (${status})`,
"",
ghTable(["PR", "STATUS", "BASE", "HEAD", "DRAFT", "TITLE"], rows),
"",
"Summary:",
` repo=${ghText(result.repo)} url=${ghText(pr.url ?? pr.html_url ?? result.url)}`,
"",
"Next:",
];
if (status === "created" && number !== "-") {
lines.push(` bun scripts/cli.ts gh pr preflight ${number} --repo ${ghText(result.repo)}`);
} else {
lines.push(" rerun without --dry-run to create the PR");
}
lines.push("", "Disclosure:");
lines.push(" default view is a bounded table; use gh pr view/preflight --full for structured metadata.");
return lines.join("\n");
}
async function prComment(repo: string, token: string, issueNumber: number, options: GitHubOptions): Promise<GitHubCommandResult> {
let bodySource: { body: string; bodySource: Record<string, unknown> };
try {