nugit-cli 0.0.1 → 0.1.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/package.json +1 -1
- package/src/api-client.js +10 -11
- 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 +114 -6
- package/src/nugit-stack.js +20 -0
- package/src/nugit-start.js +4 -4
- package/src/nugit.js +37 -22
- 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 +166 -0
- package/src/review-hub/run-review-hub.js +188 -0
- package/src/split-view/run-split.js +16 -3
- package/src/split-view/split-ink.js +2 -2
- package/src/stack-discover.js +9 -1
- 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 +70 -0
- package/src/stack-view/ink-app.js +1853 -156
- package/src/stack-view/loading-ink.js +44 -0
- package/src/stack-view/merge-alternate-pick-stacks.js +223 -0
- package/src/stack-view/patch-preview-merge.js +108 -0
- package/src/stack-view/remote-infer-doc.js +93 -0
- package/src/stack-view/repo-picker-back.js +10 -0
- package/src/stack-view/run-stack-view.js +685 -50
- package/src/stack-view/run-view-entry.js +119 -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 +270 -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/terminal-fullscreen.js +45 -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
|
@@ -1,23 +1,98 @@
|
|
|
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
|
+
readStackFile,
|
|
18
|
+
validateStackDoc,
|
|
19
|
+
createInferredStackDoc
|
|
20
|
+
} from "../nugit-stack.js";
|
|
11
21
|
import { getRepoFullNameFromGitRoot } from "../git-info.js";
|
|
12
|
-
import { discoverStacksInRepo } from "../stack-discover.js";
|
|
22
|
+
import { discoverStacksInRepo, stackTipPrNumber } from "../stack-discover.js";
|
|
13
23
|
import { getStackDiscoveryOpts, effectiveMaxOpenPrs } from "../stack-discovery-config.js";
|
|
14
24
|
import { tryLoadStackIndex, writeStackIndex } from "../stack-graph.js";
|
|
15
|
-
import { formatStacksListHuman } from "../cli-output.js";
|
|
16
25
|
import { fetchStackPrDetails } from "./fetch-pr-data.js";
|
|
17
26
|
import { loadStackDocForView } from "./loader.js";
|
|
27
|
+
import { inferStackDocForRemoteView, isGithubNotFoundError } from "./remote-infer-doc.js";
|
|
28
|
+
import { getRepoMetadata } from "../api-client.js";
|
|
18
29
|
import { StackInkApp, createExitPayload } from "./ink-app.js";
|
|
19
30
|
import { renderStaticStackView } from "./static-render.js";
|
|
20
31
|
import { questionLine } from "./prompt-line.js";
|
|
32
|
+
import {
|
|
33
|
+
loadReviewAutoapproveConfig,
|
|
34
|
+
isRepoHeadAutoapproveEligible
|
|
35
|
+
} from "../review-hub/review-autoapprove.js";
|
|
36
|
+
import {
|
|
37
|
+
pickStackIndexWithInk,
|
|
38
|
+
pickInferChainIndexWithInk,
|
|
39
|
+
STACK_PICK_BACK_TO_REPO
|
|
40
|
+
} from "./view-tui-sequential.js";
|
|
41
|
+
import { clearInkScreen } from "./terminal-fullscreen.js";
|
|
42
|
+
import { RepoPickerBackError } from "./repo-picker-back.js";
|
|
43
|
+
import { ReviewHubBackError } from "../review-hub/review-hub-back.js";
|
|
44
|
+
import {
|
|
45
|
+
augmentAlternatePickStacksWithInfer,
|
|
46
|
+
ensureDocRepresentedInPickStacks
|
|
47
|
+
} from "./merge-alternate-pick-stacks.js";
|
|
48
|
+
import { withStackLoadInkScreen } from "./loading-ink.js";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} owner
|
|
52
|
+
* @param {string} repoName
|
|
53
|
+
* @param {Record<string, unknown>} doc
|
|
54
|
+
* @param {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} mergedReviewFetch
|
|
55
|
+
* @param {boolean} [reviewAutoapply]
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchStackRowsWithAutoapply(
|
|
58
|
+
owner,
|
|
59
|
+
repoName,
|
|
60
|
+
doc,
|
|
61
|
+
mergedReviewFetch,
|
|
62
|
+
reviewAutoapply
|
|
63
|
+
) {
|
|
64
|
+
let rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
65
|
+
|
|
66
|
+
if (reviewAutoapply && resolveGithubToken() && mergedReviewFetch.viewerLogin) {
|
|
67
|
+
const cfg = loadReviewAutoapproveConfig();
|
|
68
|
+
if (cfg) {
|
|
69
|
+
let approvedAny = false;
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
if (row.error || !row.pull) continue;
|
|
72
|
+
const head = row.pull.head && typeof row.pull.head === "object" ? row.pull.head : {};
|
|
73
|
+
const headRef = typeof head.ref === "string" ? head.ref : "";
|
|
74
|
+
if (!isRepoHeadAutoapproveEligible(cfg, owner, repoName, headRef)) continue;
|
|
75
|
+
const m = row.viewerReviewMeta;
|
|
76
|
+
if (!m?.staleApproval || m.riskyChangeAfterApproval) continue;
|
|
77
|
+
const ms = row.pull.mergeable_state;
|
|
78
|
+
if (ms && ms !== "clean" && ms !== "unstable") continue;
|
|
79
|
+
try {
|
|
80
|
+
await githubPostPullReview(owner, repoName, row.entry.pr_number, {
|
|
81
|
+
event: "APPROVE",
|
|
82
|
+
body: "nugit auto-approve: merge-only delta (review-autoapprove.json)"
|
|
83
|
+
});
|
|
84
|
+
approvedAny = true;
|
|
85
|
+
} catch {
|
|
86
|
+
/* ignore per-PR */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (approvedAny) {
|
|
90
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
21
96
|
|
|
22
97
|
function createSpinner(prefix) {
|
|
23
98
|
const frames = [chalk.cyan("⠋"), chalk.cyan("⠙"), chalk.cyan("⠹"), chalk.cyan("⠸"), chalk.cyan("⠼"), chalk.cyan("⠴")];
|
|
@@ -145,18 +220,125 @@ async function promptReviewers(owner, repo, prNumber) {
|
|
|
145
220
|
return [...new Set(chosen)];
|
|
146
221
|
}
|
|
147
222
|
|
|
223
|
+
/**
|
|
224
|
+
* When the viewer opens with an explicit ref (repo picker, `nugit view owner/repo`, etc.),
|
|
225
|
+
* `discoverableStacks` is never set. Populate from `.nugit/stack-index.json` or a scan so
|
|
226
|
+
* Backspace can reopen the multi-stack Ink picker.
|
|
227
|
+
*
|
|
228
|
+
* @param {string | null} root
|
|
229
|
+
* @param {string} repoFull
|
|
230
|
+
* @param {string} owner
|
|
231
|
+
* @param {string} repoName
|
|
232
|
+
* @param {unknown[] | null} current
|
|
233
|
+
* @param {boolean} tuiSession
|
|
234
|
+
* @returns {Promise<{ stacks: unknown[] | null, openPullNumbers: Set<number> | null }>}
|
|
235
|
+
*/
|
|
236
|
+
async function hydrateDiscoverableStacksForAlternatePicker(
|
|
237
|
+
root,
|
|
238
|
+
repoFull,
|
|
239
|
+
owner,
|
|
240
|
+
repoName,
|
|
241
|
+
current,
|
|
242
|
+
tuiSession
|
|
243
|
+
) {
|
|
244
|
+
/** @type {{ stacks: unknown[] | null, openPullNumbers: Set<number> | null }} */
|
|
245
|
+
const wrap = (stacks, openPullNumbers = null) => ({ stacks, openPullNumbers });
|
|
246
|
+
|
|
247
|
+
if (!repoFull || !resolveGithubToken()) {
|
|
248
|
+
return wrap(current);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (current && Array.isArray(current) && current.length > 1) {
|
|
252
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, current);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (root) {
|
|
256
|
+
const cached = tryLoadStackIndex(root, repoFull);
|
|
257
|
+
const stacks = cached && Array.isArray(cached.stacks) ? cached.stacks : null;
|
|
258
|
+
if (stacks && stacks.length > 1) {
|
|
259
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, stacks);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const discovery = getStackDiscoveryOpts();
|
|
264
|
+
if (discovery.mode === "manual") {
|
|
265
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, Array.isArray(current) ? current : []);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const full =
|
|
269
|
+
process.env.NUGIT_STACK_DISCOVERY_FULL === "1" || process.env.NUGIT_STACK_DISCOVERY_FULL === "true";
|
|
270
|
+
const spinner = createSpinner("Scanning stacks");
|
|
271
|
+
if (!tuiSession) {
|
|
272
|
+
spinner.start();
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const discovered = await discoverStacksInRepo(owner, repoName, {
|
|
276
|
+
maxOpenPrs: effectiveMaxOpenPrs(discovery, full),
|
|
277
|
+
enrich: false,
|
|
278
|
+
fetchConcurrency: discovery.fetchConcurrency,
|
|
279
|
+
onProgress: (m) => (!tuiSession ? spinner.update(m) : undefined)
|
|
280
|
+
});
|
|
281
|
+
if (!tuiSession) {
|
|
282
|
+
spinner.stop(
|
|
283
|
+
`found ${discovered.stacks_found} stack(s) across ${discovered.scanned_open_prs} open PR(s)`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
if (root) {
|
|
287
|
+
try {
|
|
288
|
+
if (discovery.mode === "eager" || discovery.mode === "lazy") {
|
|
289
|
+
writeStackIndex(root, discovered);
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
/* ignore index write */
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (discovered.stacks_found > 1) {
|
|
296
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, discovered.stacks);
|
|
297
|
+
}
|
|
298
|
+
if (discovered.stacks_found === 1) {
|
|
299
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, discovered.stacks);
|
|
300
|
+
}
|
|
301
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, []);
|
|
302
|
+
} catch {
|
|
303
|
+
if (!tuiSession) {
|
|
304
|
+
spinner.stop("scan failed");
|
|
305
|
+
}
|
|
306
|
+
return augmentAlternatePickStacksWithInfer(owner, repoName, Array.isArray(current) ? current : []);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
148
310
|
/**
|
|
149
311
|
* @param {object} opts
|
|
150
312
|
* @param {boolean} [opts.noTui]
|
|
151
313
|
* @param {string} [opts.repo]
|
|
152
314
|
* @param {string} [opts.ref]
|
|
153
315
|
* @param {string} [opts.file]
|
|
316
|
+
* @param {Record<string, unknown>} [opts.explicitDoc] skip discovery / stack.json load
|
|
317
|
+
* @param {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} [opts.reviewFetchOpts]
|
|
318
|
+
* @param {string} [opts.viewTitle] Ink header
|
|
319
|
+
* @param {boolean} [opts.reviewAutoapply] apply review-autoapprove.json rules once after load
|
|
320
|
+
* @param {boolean} [opts.shellMode] multi-page nugit view layout (default true for TUI)
|
|
321
|
+
* @param {boolean} [opts.allowBackToRepoPicker] inferred PR-group Ink picker: Backspace throws RepoPickerBackError (repo-picker loop only)
|
|
322
|
+
* @param {boolean} [opts.allowBackToReviewHub] stack picker Backspace throws ReviewHubBackError (review hub loop only)
|
|
154
323
|
*/
|
|
155
324
|
export async function runStackViewCommand(opts) {
|
|
325
|
+
const tuiSession = !opts.noTui && process.stdin.isTTY && process.stdout.isTTY;
|
|
326
|
+
/** @type {unknown[] | null} Re-open stack picker (Backspace) when length > 1 */
|
|
327
|
+
let discoverableStacks = null;
|
|
328
|
+
|
|
329
|
+
if (!resolveGithubToken()) {
|
|
330
|
+
console.error(
|
|
331
|
+
chalk.dim(
|
|
332
|
+
"No NUGIT_USER_TOKEN: using unauthenticated GitHub reads (low rate limit; public repos only for API data). Set a PAT for private repos, higher limits, or posting comments."
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
}
|
|
156
336
|
const root = findGitRoot();
|
|
157
337
|
let repo = opts.repo;
|
|
158
338
|
let ref = opts.ref;
|
|
159
|
-
|
|
339
|
+
let explicitDoc = opts.explicitDoc || null;
|
|
340
|
+
|
|
341
|
+
if (!explicitDoc && !opts.file && !ref) {
|
|
160
342
|
let repoFull = repo || null;
|
|
161
343
|
if (!repoFull && root) {
|
|
162
344
|
try {
|
|
@@ -187,64 +369,279 @@ export async function runStackViewCommand(opts) {
|
|
|
187
369
|
usedCache = true;
|
|
188
370
|
}
|
|
189
371
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
372
|
+
const discoverOpts = {
|
|
373
|
+
maxOpenPrs: effectiveMaxOpenPrs(discovery, full),
|
|
374
|
+
enrich: false,
|
|
375
|
+
fetchConcurrency: discovery.fetchConcurrency
|
|
376
|
+
};
|
|
377
|
+
/** @type {object[]} */
|
|
378
|
+
let mergedForPick = [];
|
|
379
|
+
|
|
380
|
+
if (tuiSession) {
|
|
381
|
+
await withStackLoadInkScreen("Loading stacks…", async () => {
|
|
382
|
+
if (!discovered) {
|
|
383
|
+
discovered = await discoverStacksInRepo(owner, repoName, {
|
|
384
|
+
...discoverOpts,
|
|
385
|
+
onProgress: undefined
|
|
386
|
+
});
|
|
387
|
+
if (root) {
|
|
388
|
+
try {
|
|
389
|
+
if (discovery.mode === "eager" || discovery.mode === "lazy") {
|
|
390
|
+
writeStackIndex(root, discovered);
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
/* ignore index write */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const rawStacks = discovered && Array.isArray(discovered.stacks) ? discovered.stacks : [];
|
|
398
|
+
const aug = await augmentAlternatePickStacksWithInfer(owner, repoName, rawStacks);
|
|
399
|
+
mergedForPick = aug.stacks;
|
|
198
400
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
401
|
+
} else {
|
|
402
|
+
if (!discovered) {
|
|
403
|
+
const spinner = createSpinner("Scanning stacks");
|
|
404
|
+
spinner.start();
|
|
405
|
+
discovered = await discoverStacksInRepo(owner, repoName, {
|
|
406
|
+
...discoverOpts,
|
|
407
|
+
onProgress: (m) => spinner.update(m)
|
|
408
|
+
});
|
|
409
|
+
spinner.stop(
|
|
410
|
+
`found ${discovered.stacks_found} stack(s) across ${discovered.scanned_open_prs} open PR(s)`
|
|
411
|
+
);
|
|
412
|
+
if (root) {
|
|
413
|
+
try {
|
|
414
|
+
if (discovery.mode === "eager" || discovery.mode === "lazy") {
|
|
415
|
+
writeStackIndex(root, discovered);
|
|
416
|
+
}
|
|
417
|
+
} catch {
|
|
418
|
+
/* ignore index write */
|
|
206
419
|
}
|
|
207
|
-
} catch {
|
|
208
|
-
/* ignore index write */
|
|
209
420
|
}
|
|
421
|
+
} else if (usedCache) {
|
|
422
|
+
console.error(chalk.dim("Using .nugit/stack-index.json — set NUGIT_STACK_DISCOVERY_FULL=1 to rescan GitHub."));
|
|
210
423
|
}
|
|
211
|
-
|
|
212
|
-
|
|
424
|
+
const rawStacks = discovered && Array.isArray(discovered.stacks) ? discovered.stacks : [];
|
|
425
|
+
const aug = await augmentAlternatePickStacksWithInfer(owner, repoName, rawStacks);
|
|
426
|
+
mergedForPick = aug.stacks;
|
|
213
427
|
}
|
|
214
|
-
if (
|
|
428
|
+
if (mergedForPick.length > 1) {
|
|
215
429
|
if (opts.noTui) {
|
|
216
|
-
console.log(
|
|
430
|
+
console.log(
|
|
431
|
+
chalk.bold.cyan(`Stacks in ${repoFull}`) +
|
|
432
|
+
chalk.dim(
|
|
433
|
+
` · ${mergedForPick.length} candidate(s) (PR heads with .nugit/stack.json + inferred same-repo chains)`
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
console.log("");
|
|
437
|
+
for (const s of mergedForPick) {
|
|
438
|
+
const tag =
|
|
439
|
+
s && typeof s === "object" && /** @type {{ inferredOnly?: boolean }} */ (s).inferredOnly
|
|
440
|
+
? chalk.dim(" (inferred)")
|
|
441
|
+
: "";
|
|
442
|
+
const row = /** @type {{ tip_pr_number: number, pr_count: number, tip_head_branch: string }} */ (s);
|
|
443
|
+
console.log(
|
|
444
|
+
` ${chalk.yellow("tip #" + row.tip_pr_number)} · ${row.pr_count} PR(s) · ${chalk.magenta(row.tip_head_branch)}${tag}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
console.log("");
|
|
448
|
+
console.log(chalk.dim("Re-run with a TTY to pick interactively, or pass --ref to a stack tip branch."));
|
|
217
449
|
return;
|
|
218
450
|
}
|
|
219
|
-
const
|
|
220
|
-
|
|
451
|
+
const useInkPick =
|
|
452
|
+
!opts.noTui && process.stdin.isTTY && process.stdout.isTTY;
|
|
453
|
+
discoverableStacks = mergedForPick;
|
|
454
|
+
/** @type {{ tip_head_branch: string } | null} */
|
|
455
|
+
let pickedStack = null;
|
|
456
|
+
if (useInkPick) {
|
|
457
|
+
const hasBack = !!(opts.allowBackToRepoPicker || opts.allowBackToReviewHub);
|
|
458
|
+
pickedStack = await pickStackIndexWithInk(mergedForPick, {
|
|
459
|
+
allowBackToRepo: hasBack,
|
|
460
|
+
escapeToRepo: hasBack
|
|
461
|
+
});
|
|
462
|
+
if (pickedStack === STACK_PICK_BACK_TO_REPO) {
|
|
463
|
+
if (opts.allowBackToReviewHub) {
|
|
464
|
+
throw new ReviewHubBackError();
|
|
465
|
+
}
|
|
466
|
+
if (opts.allowBackToRepoPicker) {
|
|
467
|
+
throw new RepoPickerBackError();
|
|
468
|
+
}
|
|
469
|
+
console.error(
|
|
470
|
+
chalk.dim("Backspace: repo list is only available when you start with `nugit view` (no args) from a TTY.")
|
|
471
|
+
);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
const idx = await pickDiscoveredStackIndex(
|
|
476
|
+
/** @type {{ tip_head_branch: string, tip_pr_number: number, pr_count: number, created_by: string, prs: unknown[] }[]} */ (
|
|
477
|
+
mergedForPick
|
|
478
|
+
)
|
|
479
|
+
);
|
|
480
|
+
if (idx < 0) {
|
|
481
|
+
console.error("Cancelled.");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
pickedStack = mergedForPick[idx];
|
|
485
|
+
}
|
|
486
|
+
if (!pickedStack) {
|
|
487
|
+
if (opts.allowBackToReviewHub) {
|
|
488
|
+
throw new ReviewHubBackError();
|
|
489
|
+
}
|
|
490
|
+
if (opts.allowBackToRepoPicker) {
|
|
491
|
+
throw new RepoPickerBackError();
|
|
492
|
+
}
|
|
221
493
|
console.error("Cancelled.");
|
|
222
494
|
return;
|
|
223
495
|
}
|
|
224
496
|
repo = repoFull;
|
|
225
|
-
ref =
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
497
|
+
ref = pickedStack.tip_head_branch;
|
|
498
|
+
if (pickedStack.inferredOnly || pickedStack.inferredFromViewerDoc) {
|
|
499
|
+
const prNums = Array.isArray(pickedStack.prs)
|
|
500
|
+
? pickedStack.prs
|
|
501
|
+
.filter((p) => p && typeof p === "object" && typeof p.pr_number === "number")
|
|
502
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
503
|
+
.map((p) => p.pr_number)
|
|
504
|
+
: [];
|
|
505
|
+
if (prNums.length > 0) {
|
|
506
|
+
explicitDoc = createInferredStackDoc(repoFull, String(pickedStack.created_by || ""), prNums);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (!tuiSession) {
|
|
510
|
+
console.error(`Viewing selected stack tip: ${repo}@${ref}`);
|
|
511
|
+
}
|
|
512
|
+
} else if (mergedForPick.length === 1) {
|
|
229
513
|
repo = repoFull;
|
|
230
|
-
|
|
231
|
-
|
|
514
|
+
const only = /** @type {{ tip_head_branch: string, inferredOnly?: boolean, inferredFromViewerDoc?: boolean, created_by?: string, prs?: { pr_number: number, position?: number }[] }} */ (mergedForPick[0]);
|
|
515
|
+
ref = only.tip_head_branch;
|
|
516
|
+
discoverableStacks = mergedForPick;
|
|
517
|
+
if (only.inferredOnly || only.inferredFromViewerDoc) {
|
|
518
|
+
const prNums = Array.isArray(only.prs)
|
|
519
|
+
? only.prs
|
|
520
|
+
.filter((p) => p && typeof p === "object" && typeof p.pr_number === "number")
|
|
521
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
522
|
+
.map((p) => p.pr_number)
|
|
523
|
+
: [];
|
|
524
|
+
if (prNums.length > 0) {
|
|
525
|
+
explicitDoc = createInferredStackDoc(repoFull, String(only.created_by || ""), prNums);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!tuiSession) {
|
|
529
|
+
console.error(`Viewing stack tip: ${repo}@${ref}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!ref && repo && !explicitDoc) {
|
|
536
|
+
const { owner: dbO, repo: dbR } = parseRepoFullName(repo);
|
|
537
|
+
const dbMeta = await getRepoMetadata(dbO, dbR);
|
|
538
|
+
ref = typeof dbMeta.default_branch === "string" ? dbMeta.default_branch : "main";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (explicitDoc && repo && !opts.reviewFetchOpts?.defaultBranch) {
|
|
542
|
+
try {
|
|
543
|
+
const { owner: dbO, repo: dbR } = parseRepoFullName(repo);
|
|
544
|
+
const dbMeta = await getRepoMetadata(dbO, dbR);
|
|
545
|
+
const db = typeof dbMeta.default_branch === "string" ? dbMeta.default_branch : "main";
|
|
546
|
+
opts = { ...opts, reviewFetchOpts: { ...(opts.reviewFetchOpts || {}), defaultBranch: db } };
|
|
547
|
+
} catch { /* non-critical */ }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let doc;
|
|
551
|
+
/** @type {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} */
|
|
552
|
+
let mergedReviewFetch = { ...(opts.reviewFetchOpts || {}) };
|
|
553
|
+
|
|
554
|
+
if (explicitDoc) {
|
|
555
|
+
doc = explicitDoc;
|
|
556
|
+
validateStackDoc(doc);
|
|
557
|
+
} else {
|
|
558
|
+
try {
|
|
559
|
+
const loaded = await loadStackDocForView({
|
|
560
|
+
root,
|
|
561
|
+
repo,
|
|
562
|
+
ref,
|
|
563
|
+
file: opts.file
|
|
564
|
+
});
|
|
565
|
+
doc = loaded.doc;
|
|
566
|
+
} catch (e) {
|
|
567
|
+
const canInfer =
|
|
568
|
+
repo &&
|
|
569
|
+
ref &&
|
|
570
|
+
!opts.file &&
|
|
571
|
+
resolveGithubToken() &&
|
|
572
|
+
isGithubNotFoundError(e);
|
|
573
|
+
if (!canInfer) {
|
|
574
|
+
throw e;
|
|
232
575
|
}
|
|
576
|
+
if (!tuiSession) {
|
|
577
|
+
console.error(
|
|
578
|
+
chalk.dim(
|
|
579
|
+
"No .nugit/stack.json on that ref — inferring stack from open PRs (same-repo). " +
|
|
580
|
+
"Use an explicit --ref to a branch that has stack.json if this is wrong."
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
const useInkInfer =
|
|
585
|
+
!opts.noTui && process.stdin.isTTY && process.stdout.isTTY;
|
|
586
|
+
const { doc: inferred, viewerLogin } = await inferStackDocForRemoteView(repo, {
|
|
587
|
+
interactivePick: !opts.noTui,
|
|
588
|
+
tuiChainPick: useInkInfer
|
|
589
|
+
? (ch, pl) =>
|
|
590
|
+
pickInferChainIndexWithInk(ch, pl, { allowBackToRepo: !!(opts.allowBackToRepoPicker || opts.allowBackToReviewHub) })
|
|
591
|
+
: undefined
|
|
592
|
+
});
|
|
593
|
+
doc = inferred;
|
|
594
|
+
const { owner: oInf, repo: rInf } = parseRepoFullName(doc.repo_full_name);
|
|
595
|
+
const meta = await getRepoMetadata(oInf, rInf);
|
|
596
|
+
const defaultBranch = typeof meta.default_branch === "string" ? meta.default_branch : "main";
|
|
597
|
+
mergedReviewFetch = {
|
|
598
|
+
...mergedReviewFetch,
|
|
599
|
+
viewerLogin: mergedReviewFetch?.viewerLogin || viewerLogin,
|
|
600
|
+
defaultBranch: mergedReviewFetch?.defaultBranch || defaultBranch,
|
|
601
|
+
fullReviewFetch: mergedReviewFetch?.fullReviewFetch ?? true
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let { owner, repo: repoName } = parseRepoFullName(doc.repo_full_name);
|
|
607
|
+
let rows;
|
|
608
|
+
const loadRowsOnce = async () => {
|
|
609
|
+
rows = await fetchStackRowsWithAutoapply(
|
|
610
|
+
owner,
|
|
611
|
+
repoName,
|
|
612
|
+
doc,
|
|
613
|
+
mergedReviewFetch,
|
|
614
|
+
!!opts.reviewAutoapply
|
|
615
|
+
);
|
|
616
|
+
};
|
|
617
|
+
if (tuiSession) {
|
|
618
|
+
await withStackLoadInkScreen("Loading stack…", loadRowsOnce);
|
|
619
|
+
} else {
|
|
620
|
+
const loadSpinner = createSpinner("Loading stack");
|
|
621
|
+
if (!tuiSession) {
|
|
622
|
+
loadSpinner.start();
|
|
623
|
+
}
|
|
624
|
+
await loadRowsOnce();
|
|
625
|
+
if (!tuiSession) {
|
|
626
|
+
loadSpinner.stop(`loaded ${rows.length} PR(s)`);
|
|
233
627
|
}
|
|
234
628
|
}
|
|
235
629
|
|
|
236
|
-
|
|
630
|
+
const repoFullStr = String(doc.repo_full_name || "");
|
|
631
|
+
const hydrated = await hydrateDiscoverableStacksForAlternatePicker(
|
|
237
632
|
root,
|
|
238
|
-
|
|
633
|
+
repoFullStr,
|
|
634
|
+
owner,
|
|
635
|
+
repoName,
|
|
636
|
+
discoverableStacks,
|
|
637
|
+
tuiSession
|
|
638
|
+
);
|
|
639
|
+
discoverableStacks = ensureDocRepresentedInPickStacks(
|
|
640
|
+
hydrated.stacks,
|
|
641
|
+
/** @type {Record<string, unknown>} */ (doc),
|
|
239
642
|
ref,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const { owner, repo: repoName } = parseRepoFullName(doc.repo_full_name);
|
|
244
|
-
const loadSpinner = createSpinner("Loading stack");
|
|
245
|
-
loadSpinner.start();
|
|
246
|
-
let rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
247
|
-
loadSpinner.stop(`loaded ${rows.length} PR(s)`);
|
|
643
|
+
hydrated.openPullNumbers
|
|
644
|
+
);
|
|
248
645
|
|
|
249
646
|
if (opts.noTui) {
|
|
250
647
|
renderStaticStackView(rows);
|
|
@@ -253,9 +650,21 @@ export async function runStackViewCommand(opts) {
|
|
|
253
650
|
|
|
254
651
|
let running = true;
|
|
255
652
|
while (running) {
|
|
653
|
+
if (tuiSession) {
|
|
654
|
+
clearInkScreen();
|
|
655
|
+
}
|
|
256
656
|
const exitPayload = createExitPayload();
|
|
257
657
|
const { waitUntilExit } = render(
|
|
258
|
-
React.createElement(StackInkApp, {
|
|
658
|
+
React.createElement(StackInkApp, {
|
|
659
|
+
rows,
|
|
660
|
+
exitPayload,
|
|
661
|
+
viewTitle: opts.viewTitle || "nugit view",
|
|
662
|
+
browseOwner: owner,
|
|
663
|
+
browseRepo: repoName,
|
|
664
|
+
repoFullName: String(doc.repo_full_name || ""),
|
|
665
|
+
shellMode: opts.shellMode !== false,
|
|
666
|
+
alternateStacks: discoverableStacks
|
|
667
|
+
})
|
|
259
668
|
);
|
|
260
669
|
await waitUntilExit();
|
|
261
670
|
// Give terminal mode a short moment to settle before readline prompts.
|
|
@@ -280,7 +689,7 @@ export async function runStackViewCommand(opts) {
|
|
|
280
689
|
}
|
|
281
690
|
const refresh = createSpinner("Refreshing stack");
|
|
282
691
|
refresh.start();
|
|
283
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
692
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
284
693
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
285
694
|
} catch (e) {
|
|
286
695
|
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
@@ -301,7 +710,7 @@ export async function runStackViewCommand(opts) {
|
|
|
301
710
|
}
|
|
302
711
|
const refresh = createSpinner("Refreshing stack");
|
|
303
712
|
refresh.start();
|
|
304
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
713
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
305
714
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
306
715
|
} catch (e) {
|
|
307
716
|
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
@@ -319,7 +728,7 @@ export async function runStackViewCommand(opts) {
|
|
|
319
728
|
}
|
|
320
729
|
const refresh = createSpinner("Refreshing stack");
|
|
321
730
|
refresh.start();
|
|
322
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
731
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
323
732
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
324
733
|
} catch (e) {
|
|
325
734
|
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
@@ -327,14 +736,240 @@ export async function runStackViewCommand(opts) {
|
|
|
327
736
|
continue;
|
|
328
737
|
}
|
|
329
738
|
|
|
739
|
+
if (next.type === "pull_review_line_comment") {
|
|
740
|
+
try {
|
|
741
|
+
const { diffLineToGitHub } = await import("./diff-line-map.js");
|
|
742
|
+
const prNum = /** @type {number} */ (next.prNumber);
|
|
743
|
+
const filePath = String(next.path || "");
|
|
744
|
+
const commitId = String(next.commitId || "");
|
|
745
|
+
const patchIdx = typeof next.patchLineIndex === "number" ? next.patchLineIndex : -1;
|
|
746
|
+
|
|
747
|
+
const prRow = rows.find((r) => r.entry.pr_number === prNum);
|
|
748
|
+
const fileObj = Array.isArray(prRow?.files) ? prRow.files.find((f) => f && String(f.filename) === filePath) : null;
|
|
749
|
+
const pLines = fileObj && typeof fileObj.patch === "string" ? String(fileObj.patch).split("\n") : [];
|
|
750
|
+
const mapped = pLines.length > 0 && patchIdx >= 0 ? diffLineToGitHub(pLines, patchIdx) : null;
|
|
751
|
+
|
|
752
|
+
if (!mapped) {
|
|
753
|
+
console.error(chalk.yellow("Cannot comment on this line (header or unmappable)."));
|
|
754
|
+
} else {
|
|
755
|
+
const body = await questionLine(`Comment on ${filePath}:${mapped.line} (${mapped.side}) (empty=cancel): `);
|
|
756
|
+
if (body.trim()) {
|
|
757
|
+
await githubPostPullReviewLineComment(owner, repoName, prNum, {
|
|
758
|
+
body: body.trim(),
|
|
759
|
+
commit_id: commitId,
|
|
760
|
+
path: filePath,
|
|
761
|
+
line: mapped.line,
|
|
762
|
+
side: mapped.side
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const refresh = createSpinner("Refreshing stack");
|
|
766
|
+
refresh.start();
|
|
767
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
768
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
769
|
+
}
|
|
770
|
+
} catch (e) {
|
|
771
|
+
console.error(`Action failed: ${String(e?.message || e)}`);
|
|
772
|
+
}
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (next.type === "pick_stack") {
|
|
777
|
+
const hydratedPick = await hydrateDiscoverableStacksForAlternatePicker(
|
|
778
|
+
root,
|
|
779
|
+
String(doc.repo_full_name || ""),
|
|
780
|
+
owner,
|
|
781
|
+
repoName,
|
|
782
|
+
discoverableStacks,
|
|
783
|
+
tuiSession
|
|
784
|
+
);
|
|
785
|
+
discoverableStacks = ensureDocRepresentedInPickStacks(
|
|
786
|
+
hydratedPick.stacks,
|
|
787
|
+
/** @type {Record<string, unknown>} */ (doc),
|
|
788
|
+
ref,
|
|
789
|
+
hydratedPick.openPullNumbers
|
|
790
|
+
);
|
|
791
|
+
if (!discoverableStacks || discoverableStacks.length < 2) {
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const viewingTip = stackTipPrNumber(doc);
|
|
795
|
+
const viewingHeadRef = typeof ref === "string" && ref.trim() ? ref.trim() : undefined;
|
|
796
|
+
const hasBackFromViewer = !!(opts.allowBackToRepoPicker || opts.allowBackToReviewHub);
|
|
797
|
+
const picked = await pickStackIndexWithInk(
|
|
798
|
+
/** @type {{ tip_head_branch: string, tip_pr_number: number }[]} */ (discoverableStacks),
|
|
799
|
+
{
|
|
800
|
+
allowBackToRepo: hasBackFromViewer,
|
|
801
|
+
escapeToRepo: hasBackFromViewer,
|
|
802
|
+
viewingTipPrNumber: viewingTip != null ? viewingTip : undefined,
|
|
803
|
+
viewingHeadRef
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
if (picked === STACK_PICK_BACK_TO_REPO) {
|
|
807
|
+
if (opts.allowBackToReviewHub) {
|
|
808
|
+
throw new ReviewHubBackError();
|
|
809
|
+
}
|
|
810
|
+
if (opts.allowBackToRepoPicker) {
|
|
811
|
+
throw new RepoPickerBackError();
|
|
812
|
+
}
|
|
813
|
+
console.error(
|
|
814
|
+
chalk.dim("Backspace: repo list is only available when you start with `nugit view` (no args) from a TTY.")
|
|
815
|
+
);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (!picked) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
ref = picked.tip_head_branch;
|
|
822
|
+
const pickedIsInferred = !!(picked.inferredOnly || picked.inferredFromViewerDoc);
|
|
823
|
+
let pickedDoc = null;
|
|
824
|
+
if (pickedIsInferred && Array.isArray(picked.prs)) {
|
|
825
|
+
const prNums = picked.prs
|
|
826
|
+
.filter((p) => p && typeof p === "object" && typeof p.pr_number === "number")
|
|
827
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0))
|
|
828
|
+
.map((p) => p.pr_number);
|
|
829
|
+
if (prNums.length > 0) {
|
|
830
|
+
pickedDoc = createInferredStackDoc(
|
|
831
|
+
String(doc.repo_full_name || `${owner}/${repoName}`),
|
|
832
|
+
String(picked.created_by || ""),
|
|
833
|
+
prNums
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
if (tuiSession) {
|
|
839
|
+
await withStackLoadInkScreen("Loading stack…", async () => {
|
|
840
|
+
if (pickedDoc) {
|
|
841
|
+
doc = pickedDoc;
|
|
842
|
+
} else {
|
|
843
|
+
const loaded = await loadStackDocForView({
|
|
844
|
+
root,
|
|
845
|
+
repo,
|
|
846
|
+
ref,
|
|
847
|
+
file: opts.file
|
|
848
|
+
});
|
|
849
|
+
doc = loaded.doc;
|
|
850
|
+
}
|
|
851
|
+
const { owner: oPick, repo: rPick } = parseRepoFullName(doc.repo_full_name);
|
|
852
|
+
rows = await fetchStackRowsWithAutoapply(
|
|
853
|
+
oPick,
|
|
854
|
+
rPick,
|
|
855
|
+
doc,
|
|
856
|
+
mergedReviewFetch,
|
|
857
|
+
!!opts.reviewAutoapply
|
|
858
|
+
);
|
|
859
|
+
owner = oPick;
|
|
860
|
+
repoName = rPick;
|
|
861
|
+
});
|
|
862
|
+
} else {
|
|
863
|
+
if (pickedDoc) {
|
|
864
|
+
doc = pickedDoc;
|
|
865
|
+
} else {
|
|
866
|
+
const loaded = await loadStackDocForView({
|
|
867
|
+
root,
|
|
868
|
+
repo,
|
|
869
|
+
ref,
|
|
870
|
+
file: opts.file
|
|
871
|
+
});
|
|
872
|
+
doc = loaded.doc;
|
|
873
|
+
}
|
|
874
|
+
const { owner: oPick, repo: rPick } = parseRepoFullName(doc.repo_full_name);
|
|
875
|
+
const reloadPick = createSpinner("Loading stack");
|
|
876
|
+
reloadPick.start();
|
|
877
|
+
rows = await fetchStackRowsWithAutoapply(
|
|
878
|
+
oPick,
|
|
879
|
+
rPick,
|
|
880
|
+
doc,
|
|
881
|
+
mergedReviewFetch,
|
|
882
|
+
!!opts.reviewAutoapply
|
|
883
|
+
);
|
|
884
|
+
reloadPick.stop(`loaded ${rows.length} PR(s)`);
|
|
885
|
+
owner = oPick;
|
|
886
|
+
repoName = rPick;
|
|
887
|
+
}
|
|
888
|
+
} catch (e) {
|
|
889
|
+
console.error(`Could not load stack: ${String(/** @type {{ message?: string }} */ (e)?.message || e)}`);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
330
895
|
if (next.type === "refresh") {
|
|
331
896
|
const refresh = createSpinner("Refreshing stack");
|
|
332
897
|
refresh.start();
|
|
333
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
898
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
334
899
|
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
335
900
|
continue;
|
|
336
901
|
}
|
|
337
902
|
|
|
903
|
+
if (next.type === "submit_review") {
|
|
904
|
+
try {
|
|
905
|
+
await githubPostPullReview(owner, repoName, /** @type {number} */ (next.prNumber), {
|
|
906
|
+
event: String(next.event || "COMMENT"),
|
|
907
|
+
body: typeof next.body === "string" ? next.body : ""
|
|
908
|
+
});
|
|
909
|
+
const refresh = createSpinner("Refreshing stack");
|
|
910
|
+
refresh.start();
|
|
911
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
912
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
913
|
+
} catch (e) {
|
|
914
|
+
console.error(`Review submit failed: ${String(/** @type {{ message?: string }} */ (e)?.message || e)}`);
|
|
915
|
+
}
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (next.type === "submit_review_changes") {
|
|
920
|
+
try {
|
|
921
|
+
const body = await questionLine(`Request changes — comment (required, empty=cancel): `);
|
|
922
|
+
if (body.trim()) {
|
|
923
|
+
await githubPostPullReview(owner, repoName, /** @type {number} */ (next.prNumber), {
|
|
924
|
+
event: "REQUEST_CHANGES",
|
|
925
|
+
body: body.trim()
|
|
926
|
+
});
|
|
927
|
+
const refresh = createSpinner("Refreshing stack");
|
|
928
|
+
refresh.start();
|
|
929
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
930
|
+
refresh.stop(`loaded ${rows.length} PR(s)`);
|
|
931
|
+
}
|
|
932
|
+
} catch (e) {
|
|
933
|
+
console.error(`Review submit failed: ${String(/** @type {{ message?: string }} */ (e)?.message || e)}`);
|
|
934
|
+
}
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (next.type === "materialize_clone") {
|
|
939
|
+
try {
|
|
940
|
+
const { execFileSync } = await import("child_process");
|
|
941
|
+
const os = await import("os");
|
|
942
|
+
const path = await import("path");
|
|
943
|
+
const tok = resolveGithubToken();
|
|
944
|
+
const prRow = rows.find((r) => r.entry.pr_number === next.prNumber);
|
|
945
|
+
const pull = prRow?.pull;
|
|
946
|
+
const head = pull?.head && typeof pull.head === "object" ? pull.head : {};
|
|
947
|
+
const headRef = typeof head.ref === "string" ? head.ref : "";
|
|
948
|
+
const hr = head.repo && typeof head.repo === "object" ? head.repo : {};
|
|
949
|
+
const cloneUrl = typeof hr.clone_url === "string" ? hr.clone_url : "";
|
|
950
|
+
if (!cloneUrl || !headRef) {
|
|
951
|
+
throw new Error("Missing clone URL or head ref");
|
|
952
|
+
}
|
|
953
|
+
const safeName = `${owner}-${repoName}-${headRef}`.replace(/[^a-zA-Z0-9._-]+/g, "_");
|
|
954
|
+
const dest = path.join(os.tmpdir(), `nugit-review-${safeName}`);
|
|
955
|
+
const url =
|
|
956
|
+
tok && cloneUrl.startsWith("https://")
|
|
957
|
+
? cloneUrl.replace("https://", `https://x-access-token:${tok}@`)
|
|
958
|
+
: cloneUrl;
|
|
959
|
+
if (fs.existsSync(dest)) {
|
|
960
|
+
console.error(chalk.yellow(`Directory exists, skipping clone: ${dest}`));
|
|
961
|
+
} else {
|
|
962
|
+
execFileSync("git", ["clone", "--depth", "1", "--branch", headRef, url, dest], {
|
|
963
|
+
stdio: "inherit"
|
|
964
|
+
});
|
|
965
|
+
console.error(chalk.green(`Cloned to ${dest}`));
|
|
966
|
+
}
|
|
967
|
+
} catch (e) {
|
|
968
|
+
console.error(`Clone failed: ${String(/** @type {{ message?: string }} */ (e)?.message || e)}`);
|
|
969
|
+
}
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
|
|
338
973
|
if (next.type === "split") {
|
|
339
974
|
try {
|
|
340
975
|
const r = findGitRoot();
|
|
@@ -355,7 +990,7 @@ export async function runStackViewCommand(opts) {
|
|
|
355
990
|
}
|
|
356
991
|
const reload = createSpinner("Reloading stack");
|
|
357
992
|
reload.start();
|
|
358
|
-
rows = await fetchStackPrDetails(owner, repoName, doc.prs);
|
|
993
|
+
rows = await fetchStackPrDetails(owner, repoName, doc.prs, 6, mergedReviewFetch);
|
|
359
994
|
reload.stop(`loaded ${rows.length} PR(s)`);
|
|
360
995
|
} catch (e) {
|
|
361
996
|
console.error(`Split failed: ${String(/** @type {{ message?: string }} */ (e)?.message || e)}`);
|