nugit-cli 0.0.1 → 0.1.1
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/package.json +1 -1
- package/src/api-client.js +10 -23
- package/src/github-device-flow.js +1 -1
- package/src/github-oauth-client-id.js +11 -0
- package/src/github-pr-social.js +42 -0
- package/src/github-rest.js +149 -6
- package/src/nugit-config.js +84 -0
- package/src/nugit-stack.js +40 -257
- package/src/nugit.js +104 -647
- package/src/review-hub/review-autoapprove.js +95 -0
- package/src/review-hub/review-hub-back.js +10 -0
- package/src/review-hub/review-hub-ink.js +169 -0
- package/src/review-hub/run-review-hub.js +131 -0
- package/src/services/repo-branches.js +151 -0
- package/src/services/stack-inference.js +90 -0
- package/src/split-view/run-split.js +14 -76
- package/src/split-view/split-ink.js +2 -2
- package/src/stack-infer-from-prs.js +71 -0
- package/src/stack-view/diff-line-map.js +62 -0
- package/src/stack-view/fetch-pr-data.js +104 -4
- package/src/stack-view/infer-chains-to-pick-stacks.js +80 -0
- package/src/stack-view/ink-app.js +3 -421
- package/src/stack-view/loader.js +19 -93
- package/src/stack-view/loading-ink.js +2 -0
- package/src/stack-view/merge-alternate-pick-stacks.js +245 -0
- package/src/stack-view/patch-preview-merge.js +108 -0
- package/src/stack-view/remote-infer-doc.js +76 -0
- package/src/stack-view/repo-picker-back.js +10 -0
- package/src/stack-view/run-stack-view.js +508 -150
- package/src/stack-view/run-view-entry.js +115 -0
- package/src/stack-view/sgr-mouse.js +56 -0
- package/src/stack-view/stack-branch-graph.js +95 -0
- package/src/stack-view/stack-pick-graph.js +93 -0
- package/src/stack-view/stack-pick-ink.js +308 -0
- package/src/stack-view/stack-pick-layout.js +19 -0
- package/src/stack-view/stack-pick-sort.js +188 -0
- package/src/stack-view/stack-picker-graph-pane.js +118 -0
- package/src/stack-view/terminal-fullscreen.js +7 -0
- package/src/stack-view/tree-ascii.js +73 -0
- package/src/stack-view/view-md-plain.js +23 -0
- package/src/stack-view/view-repo-picker-ink.js +293 -0
- package/src/stack-view/view-tui-sequential.js +126 -0
- package/src/tui/pages/home.js +122 -0
- package/src/tui/pages/repo-actions.js +81 -0
- package/src/tui/pages/repo-branches.js +259 -0
- package/src/tui/pages/viewer.js +2129 -0
- package/src/tui/router.js +40 -0
- package/src/tui/run-tui.js +281 -0
- package/src/utilities/loading.js +37 -0
- package/src/utilities/terminal.js +31 -0
- package/src/cli-output.js +0 -228
- package/src/nugit-start.js +0 -211
- package/src/stack-discover.js +0 -284
- package/src/stack-discovery-config.js +0 -91
- package/src/stack-extra-commands.js +0 -353
- package/src/stack-graph.js +0 -214
- package/src/stack-helpers.js +0 -58
- package/src/stack-propagate.js +0 -422
|
@@ -1,23 +1,52 @@
|
|
|
1
|
+
import fs from "fs";
|
|
1
2
|
import React from "react";
|
|
2
3
|
import { render } from "ink";
|
|
3
4
|
import chalk from "chalk";
|
|
5
|
+
import { resolveGithubToken } from "../auth-token.js";
|
|
4
6
|
import {
|
|
5
7
|
githubListAssignableUsers,
|
|
6
8
|
githubPostIssueComment,
|
|
9
|
+
githubPostPullReview,
|
|
7
10
|
githubPostPullReviewCommentReply,
|
|
8
|
-
githubPostRequestedReviewers
|
|
11
|
+
githubPostRequestedReviewers,
|
|
12
|
+
githubPostPullReviewLineComment
|
|
9
13
|
} from "../github-pr-social.js";
|
|
10
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
findGitRoot,
|
|
16
|
+
parseRepoFullName,
|
|
17
|
+
createInferredStackDoc
|
|
18
|
+
} from "../nugit-stack.js";
|
|
11
19
|
import { getRepoFullNameFromGitRoot } from "../git-info.js";
|
|
12
|
-
import { discoverStacksInRepo } from "../stack-discover.js";
|
|
13
|
-
import { getStackDiscoveryOpts, effectiveMaxOpenPrs } from "../stack-discovery-config.js";
|
|
14
|
-
import { tryLoadStackIndex, writeStackIndex } from "../stack-graph.js";
|
|
15
|
-
import { formatStacksListHuman } from "../cli-output.js";
|
|
16
20
|
import { fetchStackPrDetails } from "./fetch-pr-data.js";
|
|
17
|
-
import {
|
|
21
|
+
import { inferStackDocForRemoteView } from "./remote-infer-doc.js";
|
|
22
|
+
import { getRepoMetadata } from "../api-client.js";
|
|
18
23
|
import { StackInkApp, createExitPayload } from "./ink-app.js";
|
|
19
24
|
import { renderStaticStackView } from "./static-render.js";
|
|
20
25
|
import { questionLine } from "./prompt-line.js";
|
|
26
|
+
import {
|
|
27
|
+
loadReviewAutoapproveConfig,
|
|
28
|
+
isRepoHeadAutoapproveEligible
|
|
29
|
+
} from "../review-hub/review-autoapprove.js";
|
|
30
|
+
import {
|
|
31
|
+
pickStackIndexWithInk,
|
|
32
|
+
pickInferChainIndexWithInk,
|
|
33
|
+
STACK_PICK_BACK_TO_REPO
|
|
34
|
+
} from "./view-tui-sequential.js";
|
|
35
|
+
import { clearInkScreen } from "./terminal-fullscreen.js";
|
|
36
|
+
import { RepoPickerBackError } from "./repo-picker-back.js";
|
|
37
|
+
import { ReviewHubBackError } from "../review-hub/review-hub-back.js";
|
|
38
|
+
import {
|
|
39
|
+
augmentAlternatePickStacksWithInfer,
|
|
40
|
+
ensureDocRepresentedInPickStacks,
|
|
41
|
+
stackDocToPickStackRow
|
|
42
|
+
} from "./merge-alternate-pick-stacks.js";
|
|
43
|
+
import { stackTipPrNumber } from "./merge-alternate-pick-stacks.js";
|
|
44
|
+
import { withStackLoadInkScreen } from "./loading-ink.js";
|
|
45
|
+
import { discoverStacksByInference } from "../services/stack-inference.js";
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Spinner helper (stderr, non-TUI)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
21
50
|
|
|
22
51
|
function createSpinner(prefix) {
|
|
23
52
|
const frames = [chalk.cyan("⠋"), chalk.cyan("⠙"), chalk.cyan("⠹"), chalk.cyan("⠸"), chalk.cyan("⠼"), chalk.cyan("⠴")];
|
|
@@ -25,13 +54,9 @@ function createSpinner(prefix) {
|
|
|
25
54
|
let msg = "starting...";
|
|
26
55
|
let timer = null;
|
|
27
56
|
return {
|
|
28
|
-
update(nextMsg) {
|
|
29
|
-
msg = nextMsg || msg;
|
|
30
|
-
},
|
|
57
|
+
update(nextMsg) { msg = nextMsg || msg; },
|
|
31
58
|
start() {
|
|
32
|
-
if (!process.stderr.isTTY)
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
59
|
+
if (!process.stderr.isTTY) return;
|
|
35
60
|
timer = setInterval(() => {
|
|
36
61
|
const frame = frames[i % frames.length];
|
|
37
62
|
i += 1;
|
|
@@ -40,24 +65,20 @@ function createSpinner(prefix) {
|
|
|
40
65
|
},
|
|
41
66
|
stop(finalMsg) {
|
|
42
67
|
if (!process.stderr.isTTY) {
|
|
43
|
-
if (finalMsg) {
|
|
44
|
-
console.error(`${prefix} ${finalMsg}`);
|
|
45
|
-
}
|
|
68
|
+
if (finalMsg) console.error(`${prefix} ${finalMsg}`);
|
|
46
69
|
return;
|
|
47
70
|
}
|
|
48
|
-
if (timer)
|
|
49
|
-
clearInterval(timer);
|
|
50
|
-
}
|
|
71
|
+
if (timer) clearInterval(timer);
|
|
51
72
|
const done = finalMsg ? `${chalk.bold(prefix)} ${chalk.green(finalMsg)}` : "";
|
|
52
73
|
process.stderr.write(`\r${done}${" ".repeat(30)}\n`);
|
|
53
74
|
}
|
|
54
75
|
};
|
|
55
76
|
}
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Non-TUI stack picker (stdin prompt fallback)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
61
82
|
async function pickDiscoveredStackIndex(stacks) {
|
|
62
83
|
const stackTree = (s) => {
|
|
63
84
|
const prs = Array.isArray(s.prs) ? s.prs : [];
|
|
@@ -84,26 +105,18 @@ async function pickDiscoveredStackIndex(stacks) {
|
|
|
84
105
|
);
|
|
85
106
|
console.error(stackTree(s));
|
|
86
107
|
}
|
|
87
|
-
const ans = String(
|
|
88
|
-
|
|
89
|
-
).trim();
|
|
90
|
-
if (!ans) {
|
|
91
|
-
return -1;
|
|
92
|
-
}
|
|
108
|
+
const ans = String(await questionLine(chalk.green("Select stack number (empty=cancel): "))).trim();
|
|
109
|
+
if (!ans) return -1;
|
|
93
110
|
const n = Number.parseInt(ans, 10);
|
|
94
|
-
if (Number.isInteger(n) && n >= 1 && n <= stacks.length)
|
|
95
|
-
return n - 1;
|
|
96
|
-
}
|
|
111
|
+
if (Number.isInteger(n) && n >= 1 && n <= stacks.length) return n - 1;
|
|
97
112
|
console.error("Invalid choice.");
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
* @returns {Promise<string[]>}
|
|
106
|
-
*/
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Reviewer prompt
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
107
120
|
async function promptReviewers(owner, repo, prNumber) {
|
|
108
121
|
let candidates = [];
|
|
109
122
|
const spin = createSpinner("Loading reviewers");
|
|
@@ -111,9 +124,7 @@ async function promptReviewers(owner, repo, prNumber) {
|
|
|
111
124
|
try {
|
|
112
125
|
const users = await githubListAssignableUsers(owner, repo);
|
|
113
126
|
candidates = Array.isArray(users)
|
|
114
|
-
? users
|
|
115
|
-
.map((u) => (u && typeof u === "object" ? String(u.login || "") : ""))
|
|
116
|
-
.filter(Boolean)
|
|
127
|
+
? users.map((u) => (u && typeof u === "object" ? String(u.login || "") : "")).filter(Boolean)
|
|
117
128
|
: [];
|
|
118
129
|
} catch {
|
|
119
130
|
candidates = [];
|
|
@@ -129,10 +140,7 @@ async function promptReviewers(owner, repo, prNumber) {
|
|
|
129
140
|
console.error(chalk.yellow("No assignable users listed by GitHub. You can still type logins."));
|
|
130
141
|
}
|
|
131
142
|
const raw = await questionLine(chalk.green("Assign Reviewers (empty=cancel): "));
|
|
132
|
-
const parts = raw
|
|
133
|
-
.split(",")
|
|
134
|
-
.map((s) => s.trim())
|
|
135
|
-
.filter(Boolean);
|
|
143
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
136
144
|
const chosen = [];
|
|
137
145
|
for (const p of parts) {
|
|
138
146
|
const n = Number.parseInt(p, 10);
|
|
@@ -145,106 +153,292 @@ async function promptReviewers(owner, repo, prNumber) {
|
|
|
145
153
|
return [...new Set(chosen)];
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Auto-approve helper
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} owner
|
|
162
|
+
* @param {string} repoName
|
|
163
|
+
* @param {Record<string, unknown>} doc
|
|
164
|
+
* @param {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} mergedReviewFetch
|
|
165
|
+
* @param {boolean} [reviewAutoapply]
|
|
166
|
+
*/
|
|
167
|
+
export async function fetchStackRowsWithAutoapply(owner, repoName, doc, mergedReviewFetch, reviewAutoapply) {
|
|
168
|
+
let rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
169
|
+
|
|
170
|
+
if (reviewAutoapply && resolveGithubToken() && mergedReviewFetch.viewerLogin) {
|
|
171
|
+
const cfg = loadReviewAutoapproveConfig();
|
|
172
|
+
if (cfg) {
|
|
173
|
+
let approvedAny = false;
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
if (row.error || !row.pull) continue;
|
|
176
|
+
const head = row.pull.head && typeof row.pull.head === "object" ? row.pull.head : {};
|
|
177
|
+
const headRef = typeof head.ref === "string" ? head.ref : "";
|
|
178
|
+
if (!isRepoHeadAutoapproveEligible(cfg, owner, repoName, headRef)) continue;
|
|
179
|
+
const m = row.viewerReviewMeta;
|
|
180
|
+
if (!m?.staleApproval || m.riskyChangeAfterApproval) continue;
|
|
181
|
+
const ms = row.pull.mergeable_state;
|
|
182
|
+
if (ms && ms !== "clean" && ms !== "unstable") continue;
|
|
183
|
+
try {
|
|
184
|
+
await githubPostPullReview(owner, repoName, row.entry.pr_number, {
|
|
185
|
+
event: "APPROVE",
|
|
186
|
+
body: "nugit auto-approve: merge-only delta (review-autoapprove.json)"
|
|
187
|
+
});
|
|
188
|
+
approvedAny = true;
|
|
189
|
+
} catch { /* ignore per-PR */ }
|
|
190
|
+
}
|
|
191
|
+
if (approvedAny) {
|
|
192
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return rows;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Alternate stack picker hydration (inference-only — no stack-index.json)
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ensure the alternate picker has discoverable stacks; uses inference only.
|
|
205
|
+
*/
|
|
206
|
+
async function hydrateDiscoverableStacks(repoFull, owner, repoName, current, tuiSession) {
|
|
207
|
+
const wrap = (stacks, openPullNumbers = null) => ({ stacks, openPullNumbers });
|
|
208
|
+
|
|
209
|
+
if (!repoFull || !resolveGithubToken()) return wrap(current);
|
|
210
|
+
if (current && Array.isArray(current) && current.length > 1) {
|
|
211
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, current);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const spinner = createSpinner("Scanning stacks");
|
|
215
|
+
if (!tuiSession) spinner.start();
|
|
216
|
+
try {
|
|
217
|
+
const { stacks, openPullNumbers } = await discoverStacksByInference(owner, repoName);
|
|
218
|
+
if (!tuiSession) {
|
|
219
|
+
spinner.stop(`found ${stacks.length} stack(s)`);
|
|
220
|
+
}
|
|
221
|
+
return { stacks, openPullNumbers };
|
|
222
|
+
} catch {
|
|
223
|
+
if (!tuiSession) spinner.stop("scan failed");
|
|
224
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, Array.isArray(current) ? current : []);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Main entry
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
148
232
|
/**
|
|
149
233
|
* @param {object} opts
|
|
150
234
|
* @param {boolean} [opts.noTui]
|
|
151
235
|
* @param {string} [opts.repo]
|
|
152
236
|
* @param {string} [opts.ref]
|
|
153
237
|
* @param {string} [opts.file]
|
|
238
|
+
* @param {Record<string, unknown>} [opts.explicitDoc] skip inference / load
|
|
239
|
+
* @param {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} [opts.reviewFetchOpts]
|
|
240
|
+
* @param {string} [opts.viewTitle]
|
|
241
|
+
* @param {boolean} [opts.reviewAutoapply]
|
|
242
|
+
* @param {boolean} [opts.shellMode]
|
|
243
|
+
* @param {boolean} [opts.allowBackToRepoPicker]
|
|
244
|
+
* @param {boolean} [opts.allowBackToReviewHub]
|
|
154
245
|
*/
|
|
155
246
|
export async function runStackViewCommand(opts) {
|
|
247
|
+
const tuiSession = !opts.noTui && process.stdin.isTTY && process.stdout.isTTY;
|
|
248
|
+
/** @type {unknown[] | null} */
|
|
249
|
+
let discoverableStacks = null;
|
|
250
|
+
|
|
251
|
+
if (!resolveGithubToken()) {
|
|
252
|
+
console.error(
|
|
253
|
+
chalk.dim(
|
|
254
|
+
"No NUGIT_USER_TOKEN: using unauthenticated GitHub reads (low rate limit; public repos only). " +
|
|
255
|
+
"Set a PAT or run `nugit auth login` for private repos and higher limits."
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
156
260
|
const root = findGitRoot();
|
|
157
261
|
let repo = opts.repo;
|
|
158
262
|
let ref = opts.ref;
|
|
159
|
-
|
|
263
|
+
let explicitDoc = opts.explicitDoc || null;
|
|
264
|
+
|
|
265
|
+
// If no explicit doc/file and no ref, discover stacks by inference
|
|
266
|
+
if (!explicitDoc && !opts.file) {
|
|
160
267
|
let repoFull = repo || null;
|
|
161
268
|
if (!repoFull && root) {
|
|
162
|
-
try {
|
|
163
|
-
repoFull = getRepoFullNameFromGitRoot(root);
|
|
164
|
-
} catch {
|
|
165
|
-
repoFull = null;
|
|
166
|
-
}
|
|
269
|
+
try { repoFull = getRepoFullNameFromGitRoot(root); } catch { repoFull = null; }
|
|
167
270
|
}
|
|
168
271
|
if (repoFull) {
|
|
169
|
-
const { owner, repo:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (cached) {
|
|
186
|
-
discovered = cached;
|
|
187
|
-
usedCache = true;
|
|
188
|
-
}
|
|
272
|
+
const { owner: discO, repo: discR } = parseRepoFullName(repoFull);
|
|
273
|
+
/** @type {object[]} */
|
|
274
|
+
let mergedForPick = [];
|
|
275
|
+
|
|
276
|
+
const doDiscover = async () => {
|
|
277
|
+
const { stacks } = await discoverStacksByInference(discO, discR);
|
|
278
|
+
mergedForPick = stacks;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (tuiSession) {
|
|
282
|
+
await withStackLoadInkScreen("Loading stacks…", doDiscover);
|
|
283
|
+
} else {
|
|
284
|
+
const sp = createSpinner("Scanning stacks");
|
|
285
|
+
sp.start();
|
|
286
|
+
await doDiscover();
|
|
287
|
+
sp.stop(`found ${mergedForPick.length} stack(s)`);
|
|
189
288
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
discovered = await discoverStacksInRepo(owner, repoName, {
|
|
194
|
-
maxOpenPrs: effectiveMaxOpenPrs(discovery, full),
|
|
195
|
-
enrich: false,
|
|
196
|
-
fetchConcurrency: discovery.fetchConcurrency,
|
|
197
|
-
onProgress: (m) => spinner.update(m)
|
|
198
|
-
});
|
|
199
|
-
spinner.stop(
|
|
200
|
-
`found ${discovered.stacks_found} stack(s) across ${discovered.scanned_open_prs} open PR(s)`
|
|
201
|
-
);
|
|
202
|
-
if (root) {
|
|
203
|
-
try {
|
|
204
|
-
if (discovery.mode === "eager" || discovery.mode === "lazy") {
|
|
205
|
-
writeStackIndex(root, discovered);
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
/* ignore index write */
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} else if (usedCache) {
|
|
212
|
-
console.error(chalk.dim("Using .nugit/stack-index.json — set NUGIT_STACK_DISCOVERY_FULL=1 to rescan GitHub."));
|
|
289
|
+
|
|
290
|
+
if (mergedForPick.length === 0 && !ref) {
|
|
291
|
+
throw new Error(`No open PR stacks found in ${repoFull}. Open some stacked PRs and try again.`);
|
|
213
292
|
}
|
|
214
|
-
|
|
293
|
+
|
|
294
|
+
if (mergedForPick.length > 1) {
|
|
215
295
|
if (opts.noTui) {
|
|
216
|
-
console.log(
|
|
296
|
+
console.log(chalk.bold.cyan(`Stacks in ${repoFull}`) + chalk.dim(` · ${mergedForPick.length} candidate(s) (inferred from open PR chains)`));
|
|
297
|
+
console.log("");
|
|
298
|
+
for (const s of mergedForPick) {
|
|
299
|
+
const row = /** @type {{ tip_pr_number: number, pr_count: number, tip_head_branch: string }} */ (s);
|
|
300
|
+
console.log(` ${chalk.yellow("tip #" + row.tip_pr_number)} · ${row.pr_count} PR(s) · ${chalk.magenta(row.tip_head_branch)}`);
|
|
301
|
+
}
|
|
302
|
+
console.log("");
|
|
303
|
+
console.log(chalk.dim("Re-run with a TTY to pick interactively, or pass --repo/--ref to open a specific stack."));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
discoverableStacks = mergedForPick;
|
|
308
|
+
const hasBack = !!(opts.allowBackToRepoPicker || opts.allowBackToReviewHub);
|
|
309
|
+
const picked = tuiSession
|
|
310
|
+
? await pickStackIndexWithInk(mergedForPick, { allowBackToRepo: hasBack, escapeToRepo: hasBack })
|
|
311
|
+
: null;
|
|
312
|
+
|
|
313
|
+
if (picked === STACK_PICK_BACK_TO_REPO) {
|
|
314
|
+
if (opts.allowBackToReviewHub) throw new ReviewHubBackError();
|
|
315
|
+
if (opts.allowBackToRepoPicker) throw new RepoPickerBackError();
|
|
217
316
|
return;
|
|
218
317
|
}
|
|
219
|
-
|
|
220
|
-
|
|
318
|
+
|
|
319
|
+
let pickedStack;
|
|
320
|
+
if (!tuiSession) {
|
|
321
|
+
const idx = await pickDiscoveredStackIndex(
|
|
322
|
+
/** @type {{ tip_head_branch: string, tip_pr_number: number, pr_count: number, created_by: string, prs: unknown[] }[]} */ (
|
|
323
|
+
mergedForPick
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
if (idx < 0) { console.error("Cancelled."); return; }
|
|
327
|
+
pickedStack = mergedForPick[idx];
|
|
328
|
+
} else {
|
|
329
|
+
pickedStack = picked;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!pickedStack) {
|
|
333
|
+
if (opts.allowBackToReviewHub) throw new ReviewHubBackError();
|
|
334
|
+
if (opts.allowBackToRepoPicker) throw new RepoPickerBackError();
|
|
221
335
|
console.error("Cancelled.");
|
|
222
336
|
return;
|
|
223
337
|
}
|
|
338
|
+
|
|
224
339
|
repo = repoFull;
|
|
225
|
-
ref =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
340
|
+
ref = pickedStack.tip_head_branch;
|
|
341
|
+
const prNums = Array.isArray(pickedStack.prs)
|
|
342
|
+
? pickedStack.prs
|
|
343
|
+
.filter((p) => p && typeof p === "object" && typeof p.pr_number === "number")
|
|
344
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
345
|
+
.map((p) => p.pr_number)
|
|
346
|
+
: [];
|
|
347
|
+
if (prNums.length > 0) {
|
|
348
|
+
explicitDoc = createInferredStackDoc(repoFull, String(pickedStack.created_by || ""), prNums);
|
|
349
|
+
}
|
|
350
|
+
if (!tuiSession) console.error(`Viewing stack tip: ${repo}@${ref}`);
|
|
351
|
+
|
|
352
|
+
} else if (mergedForPick.length === 1) {
|
|
353
|
+
const only = /** @type {{ tip_head_branch: string, created_by?: string, prs?: { pr_number: number, position?: number }[] }} */ (mergedForPick[0]);
|
|
229
354
|
repo = repoFull;
|
|
230
|
-
ref =
|
|
231
|
-
|
|
355
|
+
ref = only.tip_head_branch;
|
|
356
|
+
discoverableStacks = mergedForPick;
|
|
357
|
+
const prNums = Array.isArray(only.prs)
|
|
358
|
+
? only.prs
|
|
359
|
+
.filter((p) => p && typeof p === "object" && typeof p.pr_number === "number")
|
|
360
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
361
|
+
.map((p) => p.pr_number)
|
|
362
|
+
: [];
|
|
363
|
+
if (prNums.length > 0) {
|
|
364
|
+
explicitDoc = createInferredStackDoc(repoFull, String(only.created_by || ""), prNums);
|
|
365
|
+
}
|
|
366
|
+
if (!tuiSession) console.error(`Viewing stack tip: ${repo}@${ref}`);
|
|
232
367
|
}
|
|
233
368
|
}
|
|
234
369
|
}
|
|
235
370
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
repo
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
371
|
+
// Resolve default branch if needed
|
|
372
|
+
if (!ref && repo && !explicitDoc) {
|
|
373
|
+
const { owner: dbO, repo: dbR } = parseRepoFullName(repo);
|
|
374
|
+
const dbMeta = await getRepoMetadata(dbO, dbR);
|
|
375
|
+
ref = typeof dbMeta.default_branch === "string" ? dbMeta.default_branch : "main";
|
|
376
|
+
}
|
|
242
377
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
378
|
+
if (explicitDoc && repo && !opts.reviewFetchOpts?.defaultBranch) {
|
|
379
|
+
try {
|
|
380
|
+
const { owner: dbO, repo: dbR } = parseRepoFullName(repo);
|
|
381
|
+
const dbMeta = await getRepoMetadata(dbO, dbR);
|
|
382
|
+
const db = typeof dbMeta.default_branch === "string" ? dbMeta.default_branch : "main";
|
|
383
|
+
opts = { ...opts, reviewFetchOpts: { ...(opts.reviewFetchOpts || {}), defaultBranch: db } };
|
|
384
|
+
} catch { /* non-critical */ }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let doc;
|
|
388
|
+
/** @type {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} */
|
|
389
|
+
let mergedReviewFetch = { ...(opts.reviewFetchOpts || {}) };
|
|
390
|
+
|
|
391
|
+
if (explicitDoc) {
|
|
392
|
+
doc = explicitDoc;
|
|
393
|
+
} else if (opts.file) {
|
|
394
|
+
const raw = fs.readFileSync(opts.file, "utf8");
|
|
395
|
+
doc = JSON.parse(raw);
|
|
396
|
+
} else {
|
|
397
|
+
// Inference: no explicit doc, repo must be set
|
|
398
|
+
if (!repo) {
|
|
399
|
+
throw new Error("Cannot load stack: no repo specified. Pass --repo owner/repo or run inside a git clone.");
|
|
400
|
+
}
|
|
401
|
+
const useInkInfer = !opts.noTui && process.stdin.isTTY && process.stdout.isTTY;
|
|
402
|
+
const { doc: inferred, viewerLogin } = await inferStackDocForRemoteView(repo, {
|
|
403
|
+
interactivePick: !opts.noTui,
|
|
404
|
+
tuiChainPick: useInkInfer
|
|
405
|
+
? (ch, pl) => pickInferChainIndexWithInk(ch, pl, { allowBackToRepo: !!(opts.allowBackToRepoPicker || opts.allowBackToReviewHub) })
|
|
406
|
+
: undefined
|
|
407
|
+
});
|
|
408
|
+
doc = inferred;
|
|
409
|
+
const { owner: oInf, repo: rInf } = parseRepoFullName(doc.repo_full_name);
|
|
410
|
+
const meta = await getRepoMetadata(oInf, rInf);
|
|
411
|
+
const defaultBranch = typeof meta.default_branch === "string" ? meta.default_branch : "main";
|
|
412
|
+
mergedReviewFetch = {
|
|
413
|
+
...mergedReviewFetch,
|
|
414
|
+
viewerLogin: mergedReviewFetch?.viewerLogin || viewerLogin,
|
|
415
|
+
defaultBranch: mergedReviewFetch?.defaultBranch || defaultBranch,
|
|
416
|
+
fullReviewFetch: mergedReviewFetch?.fullReviewFetch ?? true
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let { owner, repo: repoName } = parseRepoFullName(doc.repo_full_name);
|
|
421
|
+
let rows;
|
|
422
|
+
const loadRowsOnce = async () => {
|
|
423
|
+
rows = await fetchStackRowsWithAutoapply(owner, repoName, doc, mergedReviewFetch, !!opts.reviewAutoapply);
|
|
424
|
+
};
|
|
425
|
+
if (tuiSession) {
|
|
426
|
+
await withStackLoadInkScreen("Loading stack…", loadRowsOnce);
|
|
427
|
+
} else {
|
|
428
|
+
const loadSpinner = createSpinner("Loading stack");
|
|
429
|
+
loadSpinner.start();
|
|
430
|
+
await loadRowsOnce();
|
|
431
|
+
loadSpinner.stop(`loaded ${rows.length} PR(s)`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const repoFullStr = String(doc.repo_full_name || "");
|
|
435
|
+
const hydrated = await hydrateDiscoverableStacks(repoFullStr, owner, repoName, discoverableStacks, tuiSession);
|
|
436
|
+
discoverableStacks = ensureDocRepresentedInPickStacks(
|
|
437
|
+
hydrated.stacks,
|
|
438
|
+
/** @type {Record<string, unknown>} */ (doc),
|
|
439
|
+
ref,
|
|
440
|
+
hydrated.openPullNumbers
|
|
441
|
+
);
|
|
248
442
|
|
|
249
443
|
if (opts.noTui) {
|
|
250
444
|
renderStaticStackView(rows);
|
|
@@ -253,34 +447,51 @@ export async function runStackViewCommand(opts) {
|
|
|
253
447
|
|
|
254
448
|
let running = true;
|
|
255
449
|
while (running) {
|
|
450
|
+
if (tuiSession) clearInkScreen();
|
|
256
451
|
const exitPayload = createExitPayload();
|
|
257
452
|
const { waitUntilExit } = render(
|
|
258
|
-
React.createElement(StackInkApp, {
|
|
453
|
+
React.createElement(StackInkApp, {
|
|
454
|
+
rows,
|
|
455
|
+
exitPayload,
|
|
456
|
+
viewTitle: opts.viewTitle || "nugit view",
|
|
457
|
+
browseOwner: owner,
|
|
458
|
+
browseRepo: repoName,
|
|
459
|
+
repoFullName: String(doc.repo_full_name || ""),
|
|
460
|
+
shellMode: opts.shellMode !== false,
|
|
461
|
+
alternateStacks: discoverableStacks
|
|
462
|
+
})
|
|
259
463
|
);
|
|
260
464
|
await waitUntilExit();
|
|
261
|
-
// Give terminal mode a short moment to settle before readline prompts.
|
|
262
465
|
await new Promise((r) => setTimeout(r, 25));
|
|
263
466
|
|
|
264
467
|
const next = exitPayload.next;
|
|
265
|
-
if (!next || next.type === "quit") {
|
|
266
|
-
running = false;
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
468
|
+
if (!next || next.type === "quit") { running = false; break; }
|
|
269
469
|
|
|
270
470
|
if (next.type === "issue_comment") {
|
|
271
471
|
try {
|
|
272
472
|
const body = await questionLine(`New issue comment on PR #${next.prNumber} (empty=cancel): `);
|
|
273
473
|
if (body.trim()) {
|
|
274
|
-
await githubPostIssueComment(
|
|
275
|
-
owner,
|
|
276
|
-
repoName,
|
|
277
|
-
/** @type {number} */ (next.prNumber),
|
|
278
|
-
body.trim()
|
|
279
|
-
);
|
|
474
|
+
await githubPostIssueComment(owner, repoName, /** @type {number} */ (next.prNumber), body.trim());
|
|
280
475
|
}
|
|
281
476
|
const refresh = createSpinner("Refreshing stack");
|
|
282
477
|
refresh.start();
|
|
283
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
478
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
479
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
482
|
+
}
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (next.type === "review_approve") {
|
|
487
|
+
try {
|
|
488
|
+
await githubPostPullReview(owner, repoName, /** @type {number} */ (next.prNumber), {
|
|
489
|
+
event: "APPROVE",
|
|
490
|
+
body: typeof next.body === "string" ? next.body : ""
|
|
491
|
+
});
|
|
492
|
+
const refresh = createSpinner("Refreshing stack");
|
|
493
|
+
refresh.start();
|
|
494
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
284
495
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
285
496
|
} catch (e) {
|
|
286
497
|
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
@@ -290,18 +501,13 @@ export async function runStackViewCommand(opts) {
|
|
|
290
501
|
|
|
291
502
|
if (next.type === "review_reply") {
|
|
292
503
|
try {
|
|
293
|
-
const body = await questionLine(
|
|
504
|
+
const body = await questionLine("Reply in review thread (empty=cancel): ");
|
|
294
505
|
if (body.trim()) {
|
|
295
|
-
await githubPostPullReviewCommentReply(
|
|
296
|
-
owner,
|
|
297
|
-
repoName,
|
|
298
|
-
/** @type {number} */ (next.commentId),
|
|
299
|
-
body.trim()
|
|
300
|
-
);
|
|
506
|
+
await githubPostPullReviewCommentReply(owner, repoName, /** @type {number} */ (next.commentId), body.trim());
|
|
301
507
|
}
|
|
302
508
|
const refresh = createSpinner("Refreshing stack");
|
|
303
509
|
refresh.start();
|
|
304
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
510
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
305
511
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
306
512
|
} catch (e) {
|
|
307
513
|
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
@@ -313,13 +519,11 @@ export async function runStackViewCommand(opts) {
|
|
|
313
519
|
try {
|
|
314
520
|
const logins = await promptReviewers(owner, repoName, /** @type {number} */ (next.prNumber));
|
|
315
521
|
if (logins.length) {
|
|
316
|
-
await githubPostRequestedReviewers(owner, repoName, /** @type {number} */ (next.prNumber), {
|
|
317
|
-
reviewers: logins
|
|
318
|
-
});
|
|
522
|
+
await githubPostRequestedReviewers(owner, repoName, /** @type {number} */ (next.prNumber), { reviewers: logins });
|
|
319
523
|
}
|
|
320
524
|
const refresh = createSpinner("Refreshing stack");
|
|
321
525
|
refresh.start();
|
|
322
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
526
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
323
527
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
324
528
|
} catch (e) {
|
|
325
529
|
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
@@ -327,38 +531,192 @@ export async function runStackViewCommand(opts) {
|
|
|
327
531
|
continue;
|
|
328
532
|
}
|
|
329
533
|
|
|
534
|
+
if (next.type === "pull_review_line_comment") {
|
|
535
|
+
try {
|
|
536
|
+
const { diffLineToGitHub } = await import("./diff-line-map.js");
|
|
537
|
+
const prNum = /** @type {number} */ (next.prNumber);
|
|
538
|
+
const filePath = String(next.path || "");
|
|
539
|
+
const commitId = String(next.commitId || "");
|
|
540
|
+
const patchIdx = typeof next.patchLineIndex === "number" ? next.patchLineIndex : -1;
|
|
541
|
+
const prRow = rows.find((r) => r.entry.pr_number === prNum);
|
|
542
|
+
const fileObj = Array.isArray(prRow?.files) ? prRow.files.find((f) => f && String(f.filename) === filePath) : null;
|
|
543
|
+
const pLines = fileObj && typeof fileObj.patch === "string" ? String(fileObj.patch).split("\n") : [];
|
|
544
|
+
const mapped = pLines.length > 0 && patchIdx >= 0 ? diffLineToGitHub(pLines, patchIdx) : null;
|
|
545
|
+
if (!mapped) {
|
|
546
|
+
console.error(chalk.yellow("Cannot comment on this line (header or unmappable)."));
|
|
547
|
+
} else {
|
|
548
|
+
const body = await questionLine(`Comment on ${filePath}:${mapped.line} (${mapped.side}) (empty=cancel): `);
|
|
549
|
+
if (body.trim()) {
|
|
550
|
+
await githubPostPullReviewLineComment(owner, repoName, prNum, {
|
|
551
|
+
body: body.trim(), commit_id: commitId, path: filePath, line: mapped.line, side: mapped.side
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
const refresh = createSpinner("Refreshing stack");
|
|
555
|
+
refresh.start();
|
|
556
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
557
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
558
|
+
}
|
|
559
|
+
} catch (e) {
|
|
560
|
+
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
561
|
+
}
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (next.type === "pick_stack") {
|
|
566
|
+
const repoFullForPick = String(doc.repo_full_name || "");
|
|
567
|
+
const hydratedPick = await hydrateDiscoverableStacks(repoFullForPick, owner, repoName, discoverableStacks, tuiSession);
|
|
568
|
+
discoverableStacks = ensureDocRepresentedInPickStacks(
|
|
569
|
+
hydratedPick.stacks,
|
|
570
|
+
/** @type {Record<string, unknown>} */ (doc),
|
|
571
|
+
ref,
|
|
572
|
+
hydratedPick.openPullNumbers
|
|
573
|
+
);
|
|
574
|
+
if (!discoverableStacks || discoverableStacks.length < 2) continue;
|
|
575
|
+
|
|
576
|
+
const viewingTip = stackTipPrNumber(doc);
|
|
577
|
+
const viewingHeadRef = typeof ref === "string" && ref.trim() ? ref.trim() : undefined;
|
|
578
|
+
const hasBackFromViewer = !!(opts.allowBackToRepoPicker || opts.allowBackToReviewHub);
|
|
579
|
+
const picked = await pickStackIndexWithInk(
|
|
580
|
+
/** @type {{ tip_head_branch: string, tip_pr_number: number }[]} */ (discoverableStacks),
|
|
581
|
+
{ allowBackToRepo: hasBackFromViewer, escapeToRepo: hasBackFromViewer, viewingTipPrNumber: viewingTip ?? undefined, viewingHeadRef }
|
|
582
|
+
);
|
|
583
|
+
if (picked === STACK_PICK_BACK_TO_REPO) {
|
|
584
|
+
if (opts.allowBackToReviewHub) throw new ReviewHubBackError();
|
|
585
|
+
if (opts.allowBackToRepoPicker) throw new RepoPickerBackError();
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (!picked) continue;
|
|
589
|
+
|
|
590
|
+
ref = picked.tip_head_branch;
|
|
591
|
+
const prNums = Array.isArray(picked.prs)
|
|
592
|
+
? picked.prs.filter((p) => p && typeof p === "object" && typeof p.pr_number === "number").sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((p) => p.pr_number)
|
|
593
|
+
: [];
|
|
594
|
+
const pickedDoc = prNums.length > 0
|
|
595
|
+
? createInferredStackDoc(String(doc.repo_full_name || `${owner}/${repoName}`), String(picked.created_by || ""), prNums)
|
|
596
|
+
: null;
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const reloadStack = async () => {
|
|
600
|
+
if (pickedDoc) {
|
|
601
|
+
doc = pickedDoc;
|
|
602
|
+
} else {
|
|
603
|
+
const { doc: inferred } = await inferStackDocForRemoteView(`${owner}/${repoName}`, { preselectedChainIndex: 0 });
|
|
604
|
+
doc = inferred;
|
|
605
|
+
}
|
|
606
|
+
const { owner: oPick, repo: rPick } = parseRepoFullName(doc.repo_full_name);
|
|
607
|
+
rows = await fetchStackRowsWithAutoapply(oPick, rPick, doc, mergedReviewFetch, !!opts.reviewAutoapply);
|
|
608
|
+
owner = oPick;
|
|
609
|
+
repoName = rPick;
|
|
610
|
+
};
|
|
611
|
+
if (tuiSession) {
|
|
612
|
+
await withStackLoadInkScreen("Loading stack…", reloadStack);
|
|
613
|
+
} else {
|
|
614
|
+
await reloadStack();
|
|
615
|
+
}
|
|
616
|
+
} catch (e) {
|
|
617
|
+
console.error(`Could not load stack: ${String(e?.message || e)}`);
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
330
622
|
if (next.type === "refresh") {
|
|
331
623
|
const refresh = createSpinner("Refreshing stack");
|
|
332
624
|
refresh.start();
|
|
333
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
625
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
334
626
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
335
627
|
continue;
|
|
336
628
|
}
|
|
337
629
|
|
|
630
|
+
if (next.type === "submit_review") {
|
|
631
|
+
try {
|
|
632
|
+
await githubPostPullReview(owner, repoName, /** @type {number} */ (next.prNumber), {
|
|
633
|
+
event: String(next.event || "COMMENT"),
|
|
634
|
+
body: typeof next.body === "string" ? next.body : ""
|
|
635
|
+
});
|
|
636
|
+
const refresh = createSpinner("Refreshing stack");
|
|
637
|
+
refresh.start();
|
|
638
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
639
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
640
|
+
} catch (e) {
|
|
641
|
+
console.error(`Review submit failed: ${String(e?.message || e)}`);
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (next.type === "submit_review_changes") {
|
|
647
|
+
try {
|
|
648
|
+
const body = await questionLine("Request changes — comment (required, empty=cancel): ");
|
|
649
|
+
if (body.trim()) {
|
|
650
|
+
await githubPostPullReview(owner, repoName, /** @type {number} */ (next.prNumber), {
|
|
651
|
+
event: "REQUEST_CHANGES",
|
|
652
|
+
body: body.trim()
|
|
653
|
+
});
|
|
654
|
+
const refresh = createSpinner("Refreshing stack");
|
|
655
|
+
refresh.start();
|
|
656
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
657
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
658
|
+
}
|
|
659
|
+
} catch (e) {
|
|
660
|
+
console.error(`Review submit failed: ${String(e?.message || e)}`);
|
|
661
|
+
}
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (next.type === "materialize_clone") {
|
|
666
|
+
try {
|
|
667
|
+
const { execFileSync } = await import("child_process");
|
|
668
|
+
const os = await import("os");
|
|
669
|
+
const path = await import("path");
|
|
670
|
+
const tok = resolveGithubToken();
|
|
671
|
+
const prRow = rows.find((r) => r.entry.pr_number === next.prNumber);
|
|
672
|
+
const pull = prRow?.pull;
|
|
673
|
+
const head = pull?.head && typeof pull.head === "object" ? pull.head : {};
|
|
674
|
+
const headRef = typeof head.ref === "string" ? head.ref : "";
|
|
675
|
+
const hr = head.repo && typeof head.repo === "object" ? head.repo : {};
|
|
676
|
+
const cloneUrl = typeof hr.clone_url === "string" ? hr.clone_url : "";
|
|
677
|
+
if (!cloneUrl || !headRef) throw new Error("Missing clone URL or head ref");
|
|
678
|
+
const safeName = `${owner}-${repoName}-${headRef}`.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
679
|
+
const dest = path.join(os.tmpdir(), `nugit-review-${safeName}`);
|
|
680
|
+
const url = tok && cloneUrl.startsWith("https://")
|
|
681
|
+
? cloneUrl.replace("https://", `https://x-access-token:${tok}@`)
|
|
682
|
+
: cloneUrl;
|
|
683
|
+
if (fs.existsSync(dest)) {
|
|
684
|
+
console.error(chalk.yellow(`Directory exists, skipping clone: ${dest}`));
|
|
685
|
+
} else {
|
|
686
|
+
execFileSync("git", ["clone", "--depth", "1", "--branch", headRef, url, dest], { stdio: "inherit" });
|
|
687
|
+
console.error(chalk.green(`Cloned to ${dest}`));
|
|
688
|
+
}
|
|
689
|
+
} catch (e) {
|
|
690
|
+
console.error(`Clone failed: ${String(e?.message || e)}`);
|
|
691
|
+
}
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
338
695
|
if (next.type === "split") {
|
|
339
696
|
try {
|
|
340
697
|
const r = findGitRoot();
|
|
341
|
-
if (!r)
|
|
342
|
-
throw new Error("Not inside a git repository");
|
|
343
|
-
}
|
|
698
|
+
if (!r) throw new Error("Not inside a git repository");
|
|
344
699
|
const { runSplitCommand } = await import("../split-view/run-split.js");
|
|
345
|
-
await runSplitCommand({
|
|
700
|
+
const splitResult = await runSplitCommand({
|
|
346
701
|
root: r,
|
|
347
702
|
owner,
|
|
348
703
|
repo: repoName,
|
|
349
704
|
prNumber: /** @type {number} */ (next.prNumber),
|
|
350
705
|
dryRun: false
|
|
351
706
|
});
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
707
|
+
// Refresh viewer doc with the new inferred stack (post-split)
|
|
708
|
+
if (splitResult?.newPrNumbers?.length) {
|
|
709
|
+
const { authMe } = await import("../api-client.js");
|
|
710
|
+
const me = await authMe();
|
|
711
|
+
const login = me && typeof me.login === "string" ? me.login : "viewer";
|
|
712
|
+
doc = createInferredStackDoc(`${owner}/${repoName}`, login, splitResult.newPrNumbers);
|
|
355
713
|
}
|
|
356
714
|
const reload = createSpinner("Reloading stack");
|
|
357
715
|
reload.start();
|
|
358
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
716
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
359
717
|
reload.stop(`loaded ${rows.length} PR(s)`);
|
|
360
718
|
} catch (e) {
|
|
361
|
-
console.error(`Split failed: ${String(
|
|
719
|
+
console.error(`Split failed: ${String(e?.message || e)}`);
|
|
362
720
|
}
|
|
363
721
|
continue;
|
|
364
722
|
}
|