@bensandee/forgejo-cli 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin.mjs +237 -52
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ command takes `--json`, which prints exactly one JSON document, failures include
|
|
|
55
55
|
| `pr:files` | List the files a pull request changes |
|
|
56
56
|
| `pr:merge` | Merge an open, non-draft, mergeable PR pinned to the head it read; refuses closed, draft, conflicted, unconfirmed or non-green checks (zero statuses included) unless `--ignore-checks`; `--delete-branch` is opt-in |
|
|
57
57
|
| `pr:ready` | Mark a draft pull request ready, dropping one `WIP:` prefix or taking `--title`, and confirm the forge no longer reports a draft |
|
|
58
|
+
| `pr:close` | Close an open pull request, optionally posting `--comment`/`--comment-file` first; reports an already-closed or merged PR without writing; refuses an issue |
|
|
58
59
|
| `pr:retarget` | Repoint an open pull request at a new base branch, confirm the forge took it, and report its mergeability as advice |
|
|
59
60
|
| `workflow:dispatch` | Dispatch a workflow with `--workflow`, `--ref` and repeatable `--input`; takes `--repo owner/repo`, not a URL |
|
|
60
61
|
| `ci:runs` | List a repository's workflow runs with their jobs, filtered by workflow, ref, pull request, head SHA or status |
|
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
-
import * as path$1 from "node:path";
|
|
4
|
-
import path from "node:path";
|
|
3
|
+
import path, * as path$1 from "node:path";
|
|
5
4
|
import { parseArgs, parseEnv } from "node:util";
|
|
6
5
|
import { FatalError, TransientError, UnexpectedError, describeError, redact, redact as redact$1 } from "@bensandee/common";
|
|
7
6
|
import { z } from "zod";
|
|
@@ -163,7 +162,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
163
162
|
//#region src/version.ts
|
|
164
163
|
const parsed = z.object({ version: z.string() }).safeParse({
|
|
165
164
|
name: "@bensandee/forgejo-cli",
|
|
166
|
-
version: "0.
|
|
165
|
+
version: "0.5.0",
|
|
167
166
|
description: "bst-fj: Forgejo API commands without the rest of bst",
|
|
168
167
|
license: "MIT",
|
|
169
168
|
repository: {
|
|
@@ -2337,8 +2336,8 @@ async function createRelease(executor, conn, tag, options = {}) {
|
|
|
2337
2336
|
const url = `${conn.serverUrl}/api/v1/repos/${conn.repository}/releases`;
|
|
2338
2337
|
const payload = {
|
|
2339
2338
|
tag_name: tag,
|
|
2340
|
-
name: tag,
|
|
2341
|
-
body: `Published ${tag}`,
|
|
2339
|
+
name: options.name ?? tag,
|
|
2340
|
+
body: options.body ?? `Published ${tag}`,
|
|
2342
2341
|
prerelease: options.prerelease ?? false
|
|
2343
2342
|
};
|
|
2344
2343
|
if (options.target) payload["target_commitish"] = options.target;
|
|
@@ -2434,7 +2433,9 @@ async function ensureRelease(executor, conn, tag, options = {}) {
|
|
|
2434
2433
|
try {
|
|
2435
2434
|
await createRelease(executor, conn, tag, {
|
|
2436
2435
|
target: options.target,
|
|
2437
|
-
prerelease: options.prerelease
|
|
2436
|
+
prerelease: options.prerelease,
|
|
2437
|
+
name: options.name,
|
|
2438
|
+
body: options.body
|
|
2438
2439
|
});
|
|
2439
2440
|
} catch (error) {
|
|
2440
2441
|
const raced = await getReleaseByTag(executor, conn, tag);
|
|
@@ -2458,6 +2459,8 @@ async function ensureRelease(executor, conn, tag, options = {}) {
|
|
|
2458
2459
|
const recreated = options.target ? await recreateTagViaApi(executor, conn, tag, {
|
|
2459
2460
|
target: options.target,
|
|
2460
2461
|
prerelease: options.prerelease,
|
|
2462
|
+
name: options.name,
|
|
2463
|
+
body: options.body,
|
|
2461
2464
|
current,
|
|
2462
2465
|
attempts,
|
|
2463
2466
|
baseDelayMs
|
|
@@ -2469,7 +2472,7 @@ async function ensureRelease(executor, conn, tag, options = {}) {
|
|
|
2469
2472
|
throw new TransientError(`Release for ${tag} never got a real (non-epoch) created date after ${String(attempts)} attempts. This is an intermittent Forgejo bug: a pushed tag's date settles asynchronously and a racing release creation freezes it at the Unix epoch (1969/1970). ` + (options.target ? `Recreating the tag via the API did not help -- investigate the server or upgrade Forgejo.` : `Pass the release tag's target commit so the tag can be recreated, or retry the release.`));
|
|
2470
2473
|
}
|
|
2471
2474
|
async function recreateTagViaApi(executor, conn, tag, opts) {
|
|
2472
|
-
const { target, prerelease, current, attempts, baseDelayMs } = opts;
|
|
2475
|
+
const { target, prerelease, name, body, current, attempts, baseDelayMs } = opts;
|
|
2473
2476
|
log.warn(`Release for ${tag} stayed epoch-dated -- recreating the tag via the API so Forgejo stamps a real date (the tag becomes lightweight)`);
|
|
2474
2477
|
if (current) await deleteRelease(executor, conn, current.id);
|
|
2475
2478
|
await deleteTag(executor, conn, tag);
|
|
@@ -2477,7 +2480,9 @@ async function recreateTagViaApi(executor, conn, tag, opts) {
|
|
|
2477
2480
|
try {
|
|
2478
2481
|
await createRelease(executor, conn, tag, {
|
|
2479
2482
|
target,
|
|
2480
|
-
prerelease
|
|
2483
|
+
prerelease,
|
|
2484
|
+
name,
|
|
2485
|
+
body
|
|
2481
2486
|
});
|
|
2482
2487
|
} catch (error) {
|
|
2483
2488
|
if (attempt >= attempts) throw new TransientError(`Release for ${tag} was deleted to repair its epoch date, but recreating it from ${target} failed: ${describeError(error)}. The tag no longer exists on the server -- re-push it (or re-run the release) to restore it.`);
|
|
@@ -2501,6 +2506,24 @@ async function settleBackoff(attempt, baseDelayMs) {
|
|
|
2501
2506
|
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1), SETTLE_MAX_DELAY_MS);
|
|
2502
2507
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2503
2508
|
}
|
|
2509
|
+
z.looseObject({
|
|
2510
|
+
id: z.number(),
|
|
2511
|
+
tag_name: z.string(),
|
|
2512
|
+
name: z.string().nullish(),
|
|
2513
|
+
target_commitish: z.string().nullish(),
|
|
2514
|
+
created_at: z.string().nullish(),
|
|
2515
|
+
html_url: z.string().nullish(),
|
|
2516
|
+
draft: z.boolean().optional(),
|
|
2517
|
+
prerelease: z.boolean().optional(),
|
|
2518
|
+
assets: z.array(z.unknown()).nullish()
|
|
2519
|
+
});
|
|
2520
|
+
z.looseObject({
|
|
2521
|
+
name: z.string(),
|
|
2522
|
+
commit: z.looseObject({
|
|
2523
|
+
sha: z.string(),
|
|
2524
|
+
created: z.string().nullish()
|
|
2525
|
+
})
|
|
2526
|
+
});
|
|
2504
2527
|
//#endregion
|
|
2505
2528
|
//#region src/commands/release-create.ts
|
|
2506
2529
|
const releaseCreateCommand = defineCommand({
|
|
@@ -5040,6 +5063,9 @@ var EmptyCommentError = class extends FatalError {
|
|
|
5040
5063
|
super("The comment resolved to empty text.");
|
|
5041
5064
|
}
|
|
5042
5065
|
};
|
|
5066
|
+
function rejectEmptyComment(comment) {
|
|
5067
|
+
if (comment !== void 0 && comment.trim() === "") throw new EmptyCommentError();
|
|
5068
|
+
}
|
|
5043
5069
|
var PullRequestStateError = class extends FatalError {
|
|
5044
5070
|
number;
|
|
5045
5071
|
action;
|
|
@@ -5049,20 +5075,35 @@ var PullRequestStateError = class extends FatalError {
|
|
|
5049
5075
|
this.action = action;
|
|
5050
5076
|
}
|
|
5051
5077
|
};
|
|
5078
|
+
var NotAPullRequestError = class extends FatalError {
|
|
5079
|
+
number;
|
|
5080
|
+
constructor(number) {
|
|
5081
|
+
super(`#${String(number)} is an issue, not a pull request.`);
|
|
5082
|
+
this.number = number;
|
|
5083
|
+
}
|
|
5084
|
+
};
|
|
5085
|
+
var StateNotTakenError = class extends FatalError {
|
|
5086
|
+
number;
|
|
5087
|
+
expected;
|
|
5088
|
+
actual;
|
|
5089
|
+
constructor(number, expected, actual) {
|
|
5090
|
+
super(`The forge did not change #${String(number)}: it is still ${actual}, not ${expected}.`);
|
|
5091
|
+
this.number = number;
|
|
5092
|
+
this.expected = expected;
|
|
5093
|
+
this.actual = actual;
|
|
5094
|
+
}
|
|
5095
|
+
};
|
|
5052
5096
|
const TARGET_STATE = {
|
|
5053
5097
|
close: "closed",
|
|
5054
5098
|
reopen: "open"
|
|
5055
5099
|
};
|
|
5056
|
-
async function
|
|
5057
|
-
const ref = parseIssueRef(request.ref);
|
|
5058
|
-
const number = requireIssueNumber(ref);
|
|
5059
|
-
if (request.comment !== void 0 && request.comment.trim() === "") throw new EmptyCommentError();
|
|
5060
|
-
const commentGiven = request.comment !== void 0;
|
|
5061
|
-
const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
|
|
5100
|
+
async function changeState(api, number, request, subject) {
|
|
5062
5101
|
const before = await api.get(number);
|
|
5063
|
-
if (before.isPullRequest) throw new PullRequestStateError(number, request.action);
|
|
5102
|
+
if (subject === "issue" && before.isPullRequest) throw new PullRequestStateError(number, request.action);
|
|
5103
|
+
if (subject === "pull-request" && !before.isPullRequest) throw new NotAPullRequestError(number);
|
|
5104
|
+
const commentGiven = request.comment !== void 0;
|
|
5064
5105
|
const targetState = TARGET_STATE[request.action];
|
|
5065
|
-
const buildResult = (url, current, changed, posted) => ({
|
|
5106
|
+
const buildResult = (url, current, changed, posted, merged) => ({
|
|
5066
5107
|
number,
|
|
5067
5108
|
url,
|
|
5068
5109
|
action: request.action,
|
|
@@ -5071,20 +5112,32 @@ async function setIssueState(resolve, request) {
|
|
|
5071
5112
|
current
|
|
5072
5113
|
},
|
|
5073
5114
|
changed,
|
|
5074
|
-
...commentGiven ? { comment: { posted } } : {}
|
|
5115
|
+
...commentGiven ? { comment: { posted } } : {},
|
|
5116
|
+
...merged === void 0 ? {} : { merged }
|
|
5075
5117
|
});
|
|
5076
|
-
if (before.state === targetState)
|
|
5118
|
+
if (before.state === targetState) {
|
|
5119
|
+
const merged = subject === "pull-request" ? (await api.pullRequest(number)).merged : void 0;
|
|
5120
|
+
return buildResult(before.url, before.state, false, false, merged);
|
|
5121
|
+
}
|
|
5077
5122
|
if (request.dryRun) return buildResult(before.url, targetState, true, true);
|
|
5078
5123
|
if (commentGiven) await api.addComment(number, request.comment ?? "");
|
|
5079
5124
|
let after;
|
|
5080
5125
|
try {
|
|
5081
5126
|
after = await api.setState(number, targetState);
|
|
5127
|
+
if (after.state !== targetState) throw new StateNotTakenError(number, targetState, after.state);
|
|
5082
5128
|
} catch (error) {
|
|
5083
5129
|
if (commentGiven && error instanceof Error) error.message = `The comment on #${String(number)} was already posted; a retry will duplicate it. ` + error.message;
|
|
5084
5130
|
throw error;
|
|
5085
5131
|
}
|
|
5086
5132
|
return buildResult(after.url, after.state, true, true);
|
|
5087
5133
|
}
|
|
5134
|
+
async function setIssueState(resolve, request) {
|
|
5135
|
+
const ref = parseIssueRef(request.ref);
|
|
5136
|
+
const number = requireIssueNumber(ref);
|
|
5137
|
+
rejectEmptyComment(request.comment);
|
|
5138
|
+
const { api } = resolve(resolveIssueTarget(ref.target, request.repo));
|
|
5139
|
+
return changeState(api, number, request, "issue");
|
|
5140
|
+
}
|
|
5088
5141
|
const PAST_TENSE = {
|
|
5089
5142
|
close: "Closed",
|
|
5090
5143
|
reopen: "Reopened"
|
|
@@ -5163,10 +5216,13 @@ function defineIssueStateCommand(action) {
|
|
|
5163
5216
|
} catch (error) {
|
|
5164
5217
|
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`bst-fj ${name} 273`));
|
|
5165
5218
|
if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment(flags));
|
|
5166
|
-
if (error instanceof PullRequestStateError)
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5219
|
+
if (error instanceof PullRequestStateError) {
|
|
5220
|
+
const advice = action === "close" ? `Run \`bst-fj pr:close ${String(error.number)}\` to close it.` : "Use the forge UI to change a pull request's state.";
|
|
5221
|
+
throw new FatalError(describePullRequestState(error, {
|
|
5222
|
+
command: `bst-fj ${name}`,
|
|
5223
|
+
advice
|
|
5224
|
+
}));
|
|
5225
|
+
}
|
|
5170
5226
|
throw error;
|
|
5171
5227
|
}
|
|
5172
5228
|
}
|
|
@@ -5636,6 +5692,95 @@ const prChecksCommand = defineCommand({
|
|
|
5636
5692
|
}
|
|
5637
5693
|
});
|
|
5638
5694
|
//#endregion
|
|
5695
|
+
//#region ../forgejo/src/pr-close.ts
|
|
5696
|
+
async function closePullRequest(api, request) {
|
|
5697
|
+
rejectEmptyComment(request.comment);
|
|
5698
|
+
return changeState(api, request.number, {
|
|
5699
|
+
action: "close",
|
|
5700
|
+
dryRun: request.dryRun,
|
|
5701
|
+
...request.comment === void 0 ? {} : { comment: request.comment }
|
|
5702
|
+
}, "pull-request");
|
|
5703
|
+
}
|
|
5704
|
+
function formatPrClose(result, dryRun) {
|
|
5705
|
+
if (result.changed) return formatIssueState(result, dryRun);
|
|
5706
|
+
const what = result.merged === true ? "merged" : "closed";
|
|
5707
|
+
const lines = [`#${String(result.number)} is already ${what} -- nothing to close. (${result.url})`];
|
|
5708
|
+
if (result.comment !== void 0) lines.push(" comment: not posted (pull request was already closed)");
|
|
5709
|
+
return lines.join("\n");
|
|
5710
|
+
}
|
|
5711
|
+
const PR_CLOSE_JSON_HELP = "Emit one JSON document: number, url, action (close), state ({previous, current}), changed, merged? (present only when the pull request was already closed), comment? ({posted}; present only when a comment was given).";
|
|
5712
|
+
//#endregion
|
|
5713
|
+
//#region ../forgejo/src/pr-number.ts
|
|
5714
|
+
function parsePrNumber(raw) {
|
|
5715
|
+
const value = Number(raw);
|
|
5716
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1) throw new FatalError(`Invalid pull request number "${raw}" (expected a positive integer).`);
|
|
5717
|
+
return value;
|
|
5718
|
+
}
|
|
5719
|
+
//#endregion
|
|
5720
|
+
//#region src/commands/pr-close.ts
|
|
5721
|
+
const prCloseCommand = defineCommand({
|
|
5722
|
+
meta: {
|
|
5723
|
+
name: "pr:close",
|
|
5724
|
+
description: "Close an open pull request, optionally posting a comment first. A pull request already closed or merged is reported, not an error, and no comment is posted. An issue is refused (Forgejo-only; use gh pr close on GitHub)"
|
|
5725
|
+
},
|
|
5726
|
+
args: {
|
|
5727
|
+
number: {
|
|
5728
|
+
type: "positional",
|
|
5729
|
+
required: true,
|
|
5730
|
+
description: "Pull request number"
|
|
5731
|
+
},
|
|
5732
|
+
comment: {
|
|
5733
|
+
type: "string",
|
|
5734
|
+
description: "Post this comment before writing state"
|
|
5735
|
+
},
|
|
5736
|
+
"comment-file": {
|
|
5737
|
+
type: "string",
|
|
5738
|
+
description: "Read the comment from a file ('-' for stdin)"
|
|
5739
|
+
},
|
|
5740
|
+
repo: {
|
|
5741
|
+
type: "string",
|
|
5742
|
+
description: "Target repository URL (default: this repo's origin)"
|
|
5743
|
+
},
|
|
5744
|
+
"dry-run": {
|
|
5745
|
+
type: "boolean",
|
|
5746
|
+
description: "Report what would change, mutate nothing"
|
|
5747
|
+
},
|
|
5748
|
+
json: {
|
|
5749
|
+
type: "boolean",
|
|
5750
|
+
description: PR_CLOSE_JSON_HELP
|
|
5751
|
+
},
|
|
5752
|
+
verbose: {
|
|
5753
|
+
type: "boolean",
|
|
5754
|
+
description: "Emit detailed debug output"
|
|
5755
|
+
}
|
|
5756
|
+
},
|
|
5757
|
+
async run({ args }) {
|
|
5758
|
+
const out = createCliOutput(args.json === true);
|
|
5759
|
+
const flags = {
|
|
5760
|
+
text: "--comment",
|
|
5761
|
+
file: "--comment-file"
|
|
5762
|
+
};
|
|
5763
|
+
const number = parsePrNumber(args.number);
|
|
5764
|
+
const comment = readBodySource(args.comment ?? void 0, args["comment-file"] ?? void 0, { flags });
|
|
5765
|
+
const conn = requireForgejoTargetFromRepo(process.cwd(), args.repo ?? void 0, "pr:close is Forgejo-only -- use gh pr close <n> on GitHub.");
|
|
5766
|
+
const dryRun = args["dry-run"] === true;
|
|
5767
|
+
try {
|
|
5768
|
+
const result = await closePullRequest(forgejoIssueApi(cliFetcher(), conn), {
|
|
5769
|
+
number,
|
|
5770
|
+
dryRun,
|
|
5771
|
+
...comment === void 0 ? {} : { comment }
|
|
5772
|
+
});
|
|
5773
|
+
out.json(result);
|
|
5774
|
+
out.text(formatPrClose(result, dryRun));
|
|
5775
|
+
if (dryRun && comment !== void 0 && result.comment?.posted === true) out.text(`[dry-run] Comment that would be posted:\n${comment}`);
|
|
5776
|
+
} catch (error) {
|
|
5777
|
+
if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment(flags));
|
|
5778
|
+
if (error instanceof NotAPullRequestError) throw new FatalError(`#${String(error.number)} is an issue, not a pull request -- pr:close refuses to write issue state. Use \`bst-fj issue:close ${String(error.number)}\` to close it.`);
|
|
5779
|
+
throw error;
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
});
|
|
5783
|
+
//#endregion
|
|
5639
5784
|
//#region ../forgejo/src/closing-refs.ts
|
|
5640
5785
|
const CLOSING_REF = /(?:^|[\s([])(clos(?:e|es|ed)|fix(?:es|ed)?|resolv(?:e|es|ed))(?::[ \t]*|[ \t]+)((?:[\w.-]+\/[\w.-]+)?#\d+)(?![\w-])/gim;
|
|
5641
5786
|
function capitalize(word) {
|
|
@@ -6315,13 +6460,6 @@ async function readPrFilesPage(executor, conn, prNumber, page, limit) {
|
|
|
6315
6460
|
};
|
|
6316
6461
|
}
|
|
6317
6462
|
//#endregion
|
|
6318
|
-
//#region ../forgejo/src/pr-number.ts
|
|
6319
|
-
function parsePrNumber(raw) {
|
|
6320
|
-
const value = Number(raw);
|
|
6321
|
-
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < 1) throw new FatalError(`Invalid pull request number "${raw}" (expected a positive integer).`);
|
|
6322
|
-
return value;
|
|
6323
|
-
}
|
|
6324
|
-
//#endregion
|
|
6325
6463
|
//#region src/pr-files.ts
|
|
6326
6464
|
function runPrFiles(fetcher, options) {
|
|
6327
6465
|
const number = parsePrNumber(options.number);
|
|
@@ -6565,36 +6703,52 @@ z.object({
|
|
|
6565
6703
|
})
|
|
6566
6704
|
});
|
|
6567
6705
|
//#endregion
|
|
6568
|
-
//#region src/pr-merge.ts
|
|
6569
|
-
const
|
|
6706
|
+
//#region ../forgejo/src/pr-merge-run.ts
|
|
6707
|
+
const MERGE_METHODS = [
|
|
6570
6708
|
"merge",
|
|
6571
6709
|
"rebase",
|
|
6572
6710
|
"squash"
|
|
6573
6711
|
];
|
|
6574
6712
|
const RECHECK_DELAY_MS = 2e3;
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6713
|
+
const MIN_SHA_PREFIX = 7;
|
|
6714
|
+
function sleepDefault(ms) {
|
|
6715
|
+
return new Promise((resolve) => {
|
|
6716
|
+
setTimeout(resolve, ms);
|
|
6717
|
+
});
|
|
6580
6718
|
}
|
|
6581
|
-
|
|
6582
|
-
const
|
|
6583
|
-
|
|
6584
|
-
|
|
6719
|
+
function headMatches(expected, actual) {
|
|
6720
|
+
const want = expected.trim().toLowerCase();
|
|
6721
|
+
return want.length >= MIN_SHA_PREFIX && actual.toLowerCase().startsWith(want);
|
|
6722
|
+
}
|
|
6723
|
+
async function runPrMergeCore(fetcher, conn, number, options = {}) {
|
|
6585
6724
|
const pr = `PR #${String(number)}`;
|
|
6725
|
+
const method = options.method ?? "merge";
|
|
6726
|
+
const expected = options.expectedHeadSha;
|
|
6727
|
+
const moved = (headSha) => expected !== void 0 && !headMatches(expected, headSha);
|
|
6586
6728
|
const confirmation = await confirmMergeable(fetcher, conn, number, {
|
|
6587
6729
|
delayMs: RECHECK_DELAY_MS,
|
|
6588
|
-
sleep: options.sleep ??
|
|
6589
|
-
settleOnFirstRead: (gate) => !gate.open || gate.draft
|
|
6730
|
+
sleep: options.sleep ?? sleepDefault,
|
|
6731
|
+
settleOnFirstRead: (gate) => !gate.open || gate.draft || moved(gate.headSha)
|
|
6590
6732
|
});
|
|
6591
6733
|
const { gate, verdict } = confirmation;
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
if (
|
|
6734
|
+
const refuse = (why) => ({
|
|
6735
|
+
kind: "refused",
|
|
6736
|
+
reason: `Refusing to merge ${pr}: ${why}`
|
|
6737
|
+
});
|
|
6738
|
+
if (!gate.open) return refuse("it is closed.");
|
|
6739
|
+
if (gate.draft) return refuse("it is a draft.");
|
|
6740
|
+
if (options.refuseEmpty === true && (gate.headSha === gate.mergeBase || gate.changedFiles === 0)) return refuse("its diff against its base is empty, so its branch is left alone.");
|
|
6741
|
+
if (expected !== void 0 && moved(gate.headSha)) return refuse(`its head is ${gate.headSha}, not the approved ${expected}.`);
|
|
6742
|
+
if (verdict === "conflicted") return refuse(`it has conflicts. ${confirmation.evidence ?? ""}`.trimEnd());
|
|
6743
|
+
if (verdict === "unconfirmed") return refuse(`the forge could not confirm it is mergeable. ${confirmation.evidence ?? ""}`.trimEnd());
|
|
6744
|
+
const read = await readCommitChecks(fetcher, conn, gate.headSha);
|
|
6745
|
+
const checks = classifyBaseStatus(read);
|
|
6746
|
+
const skippedOnly = read.parsed && read.contexts.length > 0 && read.contexts.every((context) => context.verdict === "skipped");
|
|
6747
|
+
if (!(checks === "green" || options.ignoreChecks === true || (checks === "no-status" || skippedOnly) && options.allowNoStatus === true)) return {
|
|
6748
|
+
kind: "refused",
|
|
6749
|
+
reason: `Refusing to merge ${pr}: its checks at ${gate.headSha} are ${checks}, not green.`,
|
|
6750
|
+
checks
|
|
6751
|
+
};
|
|
6598
6752
|
const result = {
|
|
6599
6753
|
number,
|
|
6600
6754
|
merged: false,
|
|
@@ -6603,17 +6757,47 @@ async function runPrMerge(fetcher, options) {
|
|
|
6603
6757
|
verdict,
|
|
6604
6758
|
checks
|
|
6605
6759
|
};
|
|
6606
|
-
if (options.dryRun) return
|
|
6760
|
+
if (options.dryRun === true) return {
|
|
6761
|
+
kind: "ok",
|
|
6762
|
+
result
|
|
6763
|
+
};
|
|
6607
6764
|
await mergePr(fetcher, conn, number, {
|
|
6608
6765
|
method,
|
|
6609
|
-
deleteBranch: options.deleteBranch,
|
|
6766
|
+
deleteBranch: options.deleteBranch ?? false,
|
|
6610
6767
|
expectedHeadSha: gate.headSha
|
|
6611
6768
|
});
|
|
6612
6769
|
return {
|
|
6613
|
-
|
|
6614
|
-
|
|
6770
|
+
kind: "ok",
|
|
6771
|
+
result: {
|
|
6772
|
+
...result,
|
|
6773
|
+
merged: true
|
|
6774
|
+
}
|
|
6615
6775
|
};
|
|
6616
6776
|
}
|
|
6777
|
+
//#endregion
|
|
6778
|
+
//#region src/pr-merge.ts
|
|
6779
|
+
function parseMethod(raw) {
|
|
6780
|
+
if (raw === void 0) return "merge";
|
|
6781
|
+
const method = MERGE_METHODS.find((candidate) => candidate === raw);
|
|
6782
|
+
if (method === void 0) throw new FatalError(`Invalid --method "${raw}" (expected one of: ${MERGE_METHODS.join(", ")}).`);
|
|
6783
|
+
return method;
|
|
6784
|
+
}
|
|
6785
|
+
async function runPrMerge(fetcher, options) {
|
|
6786
|
+
const number = parsePrNumber(options.number);
|
|
6787
|
+
const method = parseMethod(options.method);
|
|
6788
|
+
const outcome = await runPrMergeCore(fetcher, requireForgejoTarget(options.cwd, parseRepoTarget(options.repo), "pr:merge is Forgejo-only -- use gh pr merge on GitHub."), number, {
|
|
6789
|
+
method,
|
|
6790
|
+
deleteBranch: options.deleteBranch,
|
|
6791
|
+
ignoreChecks: options.ignoreChecks,
|
|
6792
|
+
dryRun: options.dryRun,
|
|
6793
|
+
sleep: options.sleep ?? sleep
|
|
6794
|
+
});
|
|
6795
|
+
if (outcome.kind === "refused") {
|
|
6796
|
+
const hint = outcome.checks === void 0 ? "" : " Pass --ignore-checks to merge anyway.";
|
|
6797
|
+
throw new FatalError(`${outcome.reason}${hint}`);
|
|
6798
|
+
}
|
|
6799
|
+
return outcome.result;
|
|
6800
|
+
}
|
|
6617
6801
|
function renderPrMerge(result) {
|
|
6618
6802
|
const what = `PR #${String(result.number)} (${result.method}) at ${result.headSha.slice(0, 12)}, checks ${result.checks}`;
|
|
6619
6803
|
return result.merged ? `Merged ${what}.` : `Dry run: would merge ${what}.`;
|
|
@@ -6807,6 +6991,7 @@ function renderPrReady(result) {
|
|
|
6807
6991
|
//#region src/registry-pr.ts
|
|
6808
6992
|
const prSubCommands = {
|
|
6809
6993
|
"pr:checks": prChecksCommand,
|
|
6994
|
+
"pr:close": prCloseCommand,
|
|
6810
6995
|
"pr:create": prCreateCommand,
|
|
6811
6996
|
"pr:files": prFilesCommand,
|
|
6812
6997
|
"pr:list": prListCommand,
|