nugit-cli 0.1.0 → 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 +0 -12
- package/src/github-rest.js +35 -0
- package/src/nugit-config.js +84 -0
- package/src/nugit-stack.js +29 -266
- package/src/nugit.js +103 -661
- package/src/review-hub/review-hub-ink.js +6 -3
- package/src/review-hub/run-review-hub.js +34 -91
- package/src/services/repo-branches.js +151 -0
- package/src/services/stack-inference.js +90 -0
- package/src/split-view/run-split.js +14 -89
- package/src/stack-view/infer-chains-to-pick-stacks.js +10 -0
- package/src/stack-view/ink-app.js +3 -2118
- package/src/stack-view/loader.js +19 -93
- package/src/stack-view/loading-ink.js +2 -44
- package/src/stack-view/merge-alternate-pick-stacks.js +23 -1
- package/src/stack-view/remote-infer-doc.js +28 -45
- package/src/stack-view/run-stack-view.js +249 -526
- package/src/stack-view/run-view-entry.js +14 -18
- package/src/stack-view/stack-pick-ink.js +169 -131
- package/src/stack-view/stack-picker-graph-pane.js +118 -0
- package/src/stack-view/terminal-fullscreen.js +7 -45
- 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 -292
- 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
package/src/nugit-start.js
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import {
|
|
5
|
-
buildStartEnv,
|
|
6
|
-
expandUserPath,
|
|
7
|
-
getConfigPath,
|
|
8
|
-
inferMonorepoRootFromCli,
|
|
9
|
-
loadEnvFile,
|
|
10
|
-
mergeNugitPath,
|
|
11
|
-
readUserConfig,
|
|
12
|
-
writeUserConfig
|
|
13
|
-
} from "./user-config.js";
|
|
14
|
-
import { findGitRoot, parseRepoFullName } from "./nugit-stack.js";
|
|
15
|
-
import { getRepoFullNameFromGitRoot } from "./git-info.js";
|
|
16
|
-
import { questionLine } from "./stack-view/prompt-line.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param {object} opts
|
|
20
|
-
* @param {string} [opts.installRoot]
|
|
21
|
-
* @param {string} [opts.envFile]
|
|
22
|
-
* @param {string} [opts.workingDirectory]
|
|
23
|
-
*/
|
|
24
|
-
export function runConfigInit(opts) {
|
|
25
|
-
const root = path.resolve(
|
|
26
|
-
expandUserPath(opts.installRoot || inferMonorepoRootFromCli())
|
|
27
|
-
);
|
|
28
|
-
const defaultEnv = path.join(root, ".env");
|
|
29
|
-
const envFile = expandUserPath(opts.envFile || defaultEnv);
|
|
30
|
-
const cfg = { installRoot: root, envFile };
|
|
31
|
-
if (opts.workingDirectory) {
|
|
32
|
-
cfg.workingDirectory = expandUserPath(opts.workingDirectory);
|
|
33
|
-
}
|
|
34
|
-
writeUserConfig(cfg);
|
|
35
|
-
console.error(`Wrote ${getConfigPath()}`);
|
|
36
|
-
console.error(` installRoot: ${root}`);
|
|
37
|
-
console.error(` envFile: ${envFile}`);
|
|
38
|
-
if (!fs.existsSync(envFile)) {
|
|
39
|
-
console.error(` (env file does not exist yet — create it or run: nugit config set env-file <path>)`);
|
|
40
|
-
}
|
|
41
|
-
if (cfg.workingDirectory) {
|
|
42
|
-
console.error(` workingDirectory: ${cfg.workingDirectory}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function runConfigShow() {
|
|
47
|
-
const c = readUserConfig();
|
|
48
|
-
console.log(JSON.stringify(c, null, 2));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} key
|
|
53
|
-
* @param {string} value
|
|
54
|
-
*/
|
|
55
|
-
export function runConfigSet(key, value) {
|
|
56
|
-
const c = readUserConfig();
|
|
57
|
-
const k = key.toLowerCase().replace(/_/g, "-");
|
|
58
|
-
if (k === "install-root") {
|
|
59
|
-
c.installRoot = path.resolve(expandUserPath(value));
|
|
60
|
-
} else if (k === "env-file") {
|
|
61
|
-
c.envFile = expandUserPath(value);
|
|
62
|
-
} else if (k === "working-directory" || k === "cwd") {
|
|
63
|
-
c.workingDirectory = expandUserPath(value);
|
|
64
|
-
} else if (k === "stack-discovery-mode") {
|
|
65
|
-
c.stackDiscovery = { ...(c.stackDiscovery || {}), mode: value };
|
|
66
|
-
} else if (k === "stack-discovery-max-open-prs") {
|
|
67
|
-
c.stackDiscovery = {
|
|
68
|
-
...(c.stackDiscovery || {}),
|
|
69
|
-
maxOpenPrs: Number.parseInt(value, 10)
|
|
70
|
-
};
|
|
71
|
-
} else if (k === "stack-discovery-fetch-concurrency") {
|
|
72
|
-
c.stackDiscovery = {
|
|
73
|
-
...(c.stackDiscovery || {}),
|
|
74
|
-
fetchConcurrency: Number.parseInt(value, 10)
|
|
75
|
-
};
|
|
76
|
-
} else if (k === "stack-discovery-background") {
|
|
77
|
-
c.stackDiscovery = {
|
|
78
|
-
...(c.stackDiscovery || {}),
|
|
79
|
-
background: value === "true" || value === "1"
|
|
80
|
-
};
|
|
81
|
-
} else if (k === "stack-discovery-lazy-first-pass-max-prs") {
|
|
82
|
-
c.stackDiscovery = {
|
|
83
|
-
...(c.stackDiscovery || {}),
|
|
84
|
-
lazyFirstPassMaxPrs: Number.parseInt(value, 10)
|
|
85
|
-
};
|
|
86
|
-
} else {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`Unknown key "${key}". Use: install-root | env-file | working-directory | stack-discovery-mode | stack-discovery-max-open-prs | stack-discovery-fetch-concurrency | stack-discovery-background | stack-discovery-lazy-first-pass-max-prs`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
writeUserConfig(c);
|
|
92
|
-
console.error(`Updated ${getConfigPath()}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Shell-escape for single-quoted POSIX strings.
|
|
97
|
-
* @param {string} s
|
|
98
|
-
*/
|
|
99
|
-
function shellQuoteExport(s) {
|
|
100
|
-
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Print eval-able export lines (bash/zsh).
|
|
105
|
-
* @param {'bash' | 'fish'} style
|
|
106
|
-
*/
|
|
107
|
-
export function runEnvExport(style = "bash") {
|
|
108
|
-
const cfg = readUserConfig();
|
|
109
|
-
if (!cfg.installRoot || !cfg.envFile) {
|
|
110
|
-
throw new Error("Run `nugit config init` first (or set install-root and env-file).");
|
|
111
|
-
}
|
|
112
|
-
const root = path.resolve(expandUserPath(cfg.installRoot));
|
|
113
|
-
const { vars, pathUsed } = loadEnvFile(cfg.envFile);
|
|
114
|
-
const merged = mergeNugitPath(
|
|
115
|
-
{
|
|
116
|
-
...process.env,
|
|
117
|
-
...vars,
|
|
118
|
-
NUGIT_MONOREPO_ROOT: root,
|
|
119
|
-
NUGIT_ENV_FILE: pathUsed
|
|
120
|
-
},
|
|
121
|
-
root
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (style === "fish") {
|
|
125
|
-
for (const [k, v] of Object.entries(vars)) {
|
|
126
|
-
console.log(`set -gx ${k} ${JSON.stringify(v)}`);
|
|
127
|
-
}
|
|
128
|
-
console.log(`set -gx NUGIT_MONOREPO_ROOT ${JSON.stringify(root)}`);
|
|
129
|
-
console.log(`set -gx NUGIT_ENV_FILE ${JSON.stringify(pathUsed)}`);
|
|
130
|
-
if (merged.PATH && merged.PATH !== process.env.PATH) {
|
|
131
|
-
console.log(`set -gx PATH ${JSON.stringify(merged.PATH)}`);
|
|
132
|
-
}
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
for (const [k, v] of Object.entries(vars)) {
|
|
137
|
-
console.log(`export ${k}=${shellQuoteExport(v)}`);
|
|
138
|
-
}
|
|
139
|
-
console.log(`export NUGIT_MONOREPO_ROOT=${shellQuoteExport(root)}`);
|
|
140
|
-
console.log(`export NUGIT_ENV_FILE=${shellQuoteExport(pathUsed)}`);
|
|
141
|
-
if (merged.PATH && merged.PATH !== process.env.PATH) {
|
|
142
|
-
console.log(`export PATH=${shellQuoteExport(merged.PATH)}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* @param {object} opts
|
|
148
|
-
* @param {string} [opts.command] shell command string for -lc
|
|
149
|
-
*/
|
|
150
|
-
export function runStart(opts) {
|
|
151
|
-
const cfg = readUserConfig();
|
|
152
|
-
if (!cfg.installRoot || !cfg.envFile) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
"No saved config. Run:\n nugit config init\n # then: nugit start"
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
const env = buildStartEnv(cfg);
|
|
158
|
-
const cwd = cfg.workingDirectory
|
|
159
|
-
? expandUserPath(cfg.workingDirectory)
|
|
160
|
-
: process.cwd();
|
|
161
|
-
const shell = process.env.SHELL || "/bin/bash";
|
|
162
|
-
const cmd = opts.command;
|
|
163
|
-
const args = cmd ? ["-lc", cmd] : ["-i"];
|
|
164
|
-
const child = spawn(shell, args, {
|
|
165
|
-
env,
|
|
166
|
-
cwd,
|
|
167
|
-
stdio: "inherit"
|
|
168
|
-
});
|
|
169
|
-
child.on("exit", (code, signal) => {
|
|
170
|
-
if (signal) {
|
|
171
|
-
process.exit(1);
|
|
172
|
-
}
|
|
173
|
-
process.exit(code ?? 0);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Interactive menu when stdin/stdout are TTY. Options 1–2 run then exit; 3 spawns the shell (same as plain `nugit start` without a menu).
|
|
179
|
-
*/
|
|
180
|
-
export async function runStartHub() {
|
|
181
|
-
console.error("nugit start — choose:");
|
|
182
|
-
console.error(" 1) View stacks (`nugit view` — search GitHub or this directory’s remote)");
|
|
183
|
-
console.error(" 2) Split a PR");
|
|
184
|
-
console.error(" 3) Open shell (needs `nugit config init`)");
|
|
185
|
-
const ans = (await questionLine("Enter 1–3 [3]: ")).trim();
|
|
186
|
-
const choice = ans || "3";
|
|
187
|
-
if (choice === "1") {
|
|
188
|
-
const { runNugitViewEntry } = await import("./stack-view/run-view-entry.js");
|
|
189
|
-
await runNugitViewEntry(undefined, undefined, {});
|
|
190
|
-
process.exit(0);
|
|
191
|
-
}
|
|
192
|
-
if (choice === "2") {
|
|
193
|
-
const raw = (await questionLine("PR number to split: ")).trim();
|
|
194
|
-
const n = Number.parseInt(raw, 10);
|
|
195
|
-
if (!Number.isFinite(n) || n < 1) {
|
|
196
|
-
console.error("Invalid PR number.");
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
const root = findGitRoot();
|
|
200
|
-
if (!root) {
|
|
201
|
-
console.error("Not inside a git repository.");
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
const repoFull = getRepoFullNameFromGitRoot(root);
|
|
205
|
-
const { owner, repo: repoName } = parseRepoFullName(repoFull);
|
|
206
|
-
const { runSplitCommand } = await import("./split-view/run-split.js");
|
|
207
|
-
await runSplitCommand({ root, owner, repo: repoName, prNumber: n });
|
|
208
|
-
process.exit(0);
|
|
209
|
-
}
|
|
210
|
-
runStart({});
|
|
211
|
-
}
|
package/src/stack-discover.js
DELETED
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { githubListOpenPulls } from "./github-rest.js";
|
|
2
|
-
import { getGithubContents, decodeGithubFileContent, getPull } from "./api-client.js";
|
|
3
|
-
import { validateStackDoc } from "./nugit-stack.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Stack tip PR # from layer.tip, else top entry by position in doc.prs.
|
|
7
|
-
* @param {Record<string, unknown>} doc
|
|
8
|
-
* @returns {number | null}
|
|
9
|
-
*/
|
|
10
|
-
export function stackTipPrNumber(doc) {
|
|
11
|
-
const layer = doc.layer;
|
|
12
|
-
if (layer && typeof layer === "object") {
|
|
13
|
-
const tip = /** @type {{ tip?: { pr_number?: number } }} */ (layer).tip;
|
|
14
|
-
if (tip && typeof tip === "object" && typeof tip.pr_number === "number" && tip.pr_number >= 1) {
|
|
15
|
-
return tip.pr_number;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const prs = Array.isArray(doc.prs) ? doc.prs : [];
|
|
19
|
-
if (!prs.length) {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
const sorted = [...prs].sort(
|
|
23
|
-
(a, b) =>
|
|
24
|
-
(/** @type {{ position?: number }} */ (a).position ?? 0) -
|
|
25
|
-
(/** @type {{ position?: number }} */ (b).position ?? 0)
|
|
26
|
-
);
|
|
27
|
-
const top = sorted[sorted.length - 1];
|
|
28
|
-
const n = top && typeof top === "object" ? /** @type {{ pr_number?: number }} */ (top).pr_number : undefined;
|
|
29
|
-
return typeof n === "number" && n >= 1 ? n : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @param {Record<string, unknown>} doc
|
|
34
|
-
* @param {string} owner
|
|
35
|
-
* @param {string} repo
|
|
36
|
-
*/
|
|
37
|
-
export function docRepoMatches(doc, owner, repo) {
|
|
38
|
-
const full = String(doc.repo_full_name || "").toLowerCase();
|
|
39
|
-
return full === `${owner}/${repo}`.toLowerCase();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @template T
|
|
44
|
-
* @param {T[]} items
|
|
45
|
-
* @param {number} concurrency
|
|
46
|
-
* @param {(item: T, index: number) => Promise<void>} fn
|
|
47
|
-
*/
|
|
48
|
-
async function forEachPool(items, concurrency, fn) {
|
|
49
|
-
let idx = 0;
|
|
50
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
51
|
-
for (;;) {
|
|
52
|
-
const i = idx++;
|
|
53
|
-
if (i >= items.length) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
await fn(items[i], i);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
await Promise.all(workers);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @param {string} owner
|
|
64
|
-
* @param {string} repo
|
|
65
|
-
* @param {string} ref
|
|
66
|
-
* @returns {Promise<Record<string, unknown> | null>}
|
|
67
|
-
*/
|
|
68
|
-
async function tryLoadStackDocAtRef(owner, repo, ref) {
|
|
69
|
-
try {
|
|
70
|
-
const item = await getGithubContents(owner, repo, ".nugit/stack.json", ref);
|
|
71
|
-
const text = decodeGithubFileContent(item);
|
|
72
|
-
if (!text) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
const doc = JSON.parse(text);
|
|
76
|
-
validateStackDoc(doc);
|
|
77
|
-
if (!docRepoMatches(doc, owner, repo)) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
return /** @type {Record<string, unknown>} */ (doc);
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* If this is a propagated prefix, try to load the full tip stack doc.
|
|
88
|
-
* @param {string} owner
|
|
89
|
-
* @param {string} repo
|
|
90
|
-
* @param {Record<string, unknown>} doc
|
|
91
|
-
*/
|
|
92
|
-
async function maybeExpandPrefixDoc(owner, repo, doc) {
|
|
93
|
-
const layer = doc.layer && typeof doc.layer === "object" ? /** @type {Record<string, unknown>} */ (doc.layer) : null;
|
|
94
|
-
if (!layer) {
|
|
95
|
-
return doc;
|
|
96
|
-
}
|
|
97
|
-
const stackSize = layer.stack_size;
|
|
98
|
-
const prs = Array.isArray(doc.prs) ? doc.prs : [];
|
|
99
|
-
const tip = layer.tip && typeof layer.tip === "object" ? /** @type {Record<string, unknown>} */ (layer.tip) : null;
|
|
100
|
-
const tipHead = tip && typeof tip.head_branch === "string" ? tip.head_branch.trim() : "";
|
|
101
|
-
if (
|
|
102
|
-
typeof stackSize !== "number" ||
|
|
103
|
-
!Number.isFinite(stackSize) ||
|
|
104
|
-
stackSize <= prs.length ||
|
|
105
|
-
!tipHead
|
|
106
|
-
) {
|
|
107
|
-
return doc;
|
|
108
|
-
}
|
|
109
|
-
const full = await tryLoadStackDocAtRef(owner, repo, tipHead);
|
|
110
|
-
if (!full) {
|
|
111
|
-
return doc;
|
|
112
|
-
}
|
|
113
|
-
const fullPrs = Array.isArray(full.prs) ? full.prs : [];
|
|
114
|
-
return fullPrs.length > prs.length ? full : doc;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Scan open PRs; any head with committed `.nugit/stack.json` counts. Deduplicate by stack tip PR # (layer.tip or top of prs).
|
|
119
|
-
*
|
|
120
|
-
* @param {string} owner
|
|
121
|
-
* @param {string} repo
|
|
122
|
-
* @param {{
|
|
123
|
-
* maxOpenPrs?: number,
|
|
124
|
-
* listPerPage?: number,
|
|
125
|
-
* enrich?: boolean,
|
|
126
|
-
* fetchConcurrency?: number,
|
|
127
|
-
* onProgress?: (msg: string) => void
|
|
128
|
-
* }} [opts]
|
|
129
|
-
*/
|
|
130
|
-
export async function discoverStacksInRepo(owner, repo, opts = {}) {
|
|
131
|
-
const maxOpenPrs = opts.maxOpenPrs ?? 500;
|
|
132
|
-
const listPerPage = Math.min(100, Math.max(1, opts.listPerPage ?? 100));
|
|
133
|
-
const fetchConc = Math.max(1, Math.min(32, opts.fetchConcurrency ?? 8));
|
|
134
|
-
const enrich = opts.enrich !== false;
|
|
135
|
-
const onProgress = typeof opts.onProgress === "function" ? opts.onProgress : null;
|
|
136
|
-
|
|
137
|
-
/** @type {unknown[]} */
|
|
138
|
-
const allPulls = [];
|
|
139
|
-
let page = 1;
|
|
140
|
-
let truncated = false;
|
|
141
|
-
for (;;) {
|
|
142
|
-
if (maxOpenPrs > 0 && allPulls.length >= maxOpenPrs) {
|
|
143
|
-
truncated = true;
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
const pulls = await githubListOpenPulls(owner, repo, page, listPerPage);
|
|
147
|
-
if (!Array.isArray(pulls) || pulls.length === 0) {
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
for (const p of pulls) {
|
|
151
|
-
if (maxOpenPrs > 0 && allPulls.length >= maxOpenPrs) {
|
|
152
|
-
truncated = true;
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
allPulls.push(p);
|
|
156
|
-
}
|
|
157
|
-
if (truncated) {
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
if (onProgress) {
|
|
161
|
-
onProgress(`listed open PRs: ${allPulls.length}`);
|
|
162
|
-
}
|
|
163
|
-
if (pulls.length < listPerPage) {
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
page += 1;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** @type {({ doc: Record<string, unknown>, discoveredFromPr: number, headRef: string } | null)[]} */
|
|
170
|
-
const rowSlots = Array(allPulls.length).fill(null);
|
|
171
|
-
|
|
172
|
-
let checkedHeads = 0;
|
|
173
|
-
await forEachPool(allPulls, fetchConc, async (pull, i) => {
|
|
174
|
-
const p = pull && typeof pull === "object" ? /** @type {Record<string, unknown>} */ (pull) : {};
|
|
175
|
-
const head = p.head && typeof p.head === "object" ? /** @type {Record<string, unknown>} */ (p.head) : {};
|
|
176
|
-
const ref = typeof head.ref === "string" ? head.ref : "";
|
|
177
|
-
const num = p.number;
|
|
178
|
-
if (!ref || typeof num !== "number") {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const doc = await tryLoadStackDocAtRef(owner, repo, ref);
|
|
182
|
-
checkedHeads += 1;
|
|
183
|
-
if (onProgress && (checkedHeads % 10 === 0 || checkedHeads === allPulls.length)) {
|
|
184
|
-
onProgress(`checked stack.json on PR heads: ${checkedHeads}/${allPulls.length}`);
|
|
185
|
-
}
|
|
186
|
-
if (!doc) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
rowSlots[i] = { doc, discoveredFromPr: num, headRef: ref };
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const found = rowSlots.filter(Boolean);
|
|
193
|
-
/** @type {Map<number, { doc: Record<string, unknown>, discoveredFromPr: number, headRef: string }>} */
|
|
194
|
-
const byTip = new Map();
|
|
195
|
-
|
|
196
|
-
for (const row of found) {
|
|
197
|
-
if (!row) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const expandedDoc = await maybeExpandPrefixDoc(owner, repo, row.doc);
|
|
201
|
-
const tip = stackTipPrNumber(expandedDoc);
|
|
202
|
-
if (tip == null) {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const prev = byTip.get(tip);
|
|
206
|
-
const score = Array.isArray(expandedDoc.prs) ? expandedDoc.prs.length : 0;
|
|
207
|
-
const prevScore = prev ? (Array.isArray(prev.doc.prs) ? prev.doc.prs.length : 0) : -1;
|
|
208
|
-
if (!prev || score > prevScore || (score === prevScore && row.discoveredFromPr === tip)) {
|
|
209
|
-
byTip.set(tip, {
|
|
210
|
-
doc: expandedDoc,
|
|
211
|
-
discoveredFromPr: row.discoveredFromPr,
|
|
212
|
-
headRef: row.headRef
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const repoFull = `${owner}/${repo}`;
|
|
218
|
-
/** @type {import("./stack-discover.js").DiscoveredStack[]} */
|
|
219
|
-
const stacks = [];
|
|
220
|
-
|
|
221
|
-
for (const [tipPr, meta] of [...byTip.entries()].sort((a, b) => a[0] - b[0])) {
|
|
222
|
-
const doc = meta.doc;
|
|
223
|
-
const prs = Array.isArray(doc.prs) ? doc.prs : [];
|
|
224
|
-
const sorted = [...prs].sort(
|
|
225
|
-
(a, b) =>
|
|
226
|
-
(/** @type {{ position?: number }} */ (a).position ?? 0) -
|
|
227
|
-
(/** @type {{ position?: number }} */ (b).position ?? 0)
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
/** @type {{ pr_number: number, position: number, title?: string, html_url?: string, head_branch?: string }[]} */
|
|
231
|
-
let prRows = sorted.map((entry) => {
|
|
232
|
-
const e = entry && typeof entry === "object" ? /** @type {Record<string, unknown>} */ (entry) : {};
|
|
233
|
-
return {
|
|
234
|
-
pr_number: /** @type {number} */ (e.pr_number),
|
|
235
|
-
position: /** @type {number} */ (e.position),
|
|
236
|
-
head_branch: typeof e.head_branch === "string" ? e.head_branch : undefined
|
|
237
|
-
};
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
if (enrich) {
|
|
241
|
-
if (onProgress) {
|
|
242
|
-
onProgress(`loading PR titles for stack tip #${tipPr}`);
|
|
243
|
-
}
|
|
244
|
-
await forEachPool(prRows, fetchConc, async (row) => {
|
|
245
|
-
try {
|
|
246
|
-
const g = await getPull(owner, repo, row.pr_number);
|
|
247
|
-
row.title = typeof g.title === "string" ? g.title : undefined;
|
|
248
|
-
row.html_url = typeof g.html_url === "string" ? g.html_url : undefined;
|
|
249
|
-
} catch {
|
|
250
|
-
/* keep without title */
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const tipEntry = sorted.find((e) => {
|
|
256
|
-
const o = e && typeof e === "object" ? /** @type {{ pr_number?: number }} */ (e) : {};
|
|
257
|
-
return o.pr_number === tipPr;
|
|
258
|
-
});
|
|
259
|
-
const tipObj = tipEntry && typeof tipEntry === "object" ? /** @type {Record<string, unknown>} */ (tipEntry) : {};
|
|
260
|
-
const tipHeadBranch =
|
|
261
|
-
typeof tipObj.head_branch === "string"
|
|
262
|
-
? tipObj.head_branch
|
|
263
|
-
: meta.headRef;
|
|
264
|
-
|
|
265
|
-
const tipPull = allPulls.find((pull) => {
|
|
266
|
-
const p = pull && typeof pull === "object" ? /** @type {Record<string, unknown>} */ (pull) : {};
|
|
267
|
-
return p.number === tipPr;
|
|
268
|
-
});
|
|
269
|
-
const tipPu = tipPull && typeof tipPull === "object" ? /** @type {Record<string, unknown>} */ (tipPull) : {};
|
|
270
|
-
const tip_updated_at = typeof tipPu.updated_at === "string" ? tipPu.updated_at : "";
|
|
271
|
-
|
|
272
|
-
stacks.push({
|
|
273
|
-
tip_pr_number: tipPr,
|
|
274
|
-
created_by: String(doc.created_by || ""),
|
|
275
|
-
discovered_from_pr: meta.discoveredFromPr,
|
|
276
|
-
pr_count: prRows.length,
|
|
277
|
-
prs: prRows,
|
|
278
|
-
tip_head_branch: tipHeadBranch,
|
|
279
|
-
tip_updated_at,
|
|
280
|
-
fetch_command: `nugit stack fetch --repo ${repoFull} --ref ${tipHeadBranch}`,
|
|
281
|
-
view_command: `nugit view --repo ${repoFull} --ref ${tipHeadBranch}`
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return {
|
|
286
|
-
repo_full_name: repoFull,
|
|
287
|
-
scanned_open_prs: allPulls.length,
|
|
288
|
-
open_prs_truncated: truncated,
|
|
289
|
-
stacks_found: stacks.length,
|
|
290
|
-
stacks
|
|
291
|
-
};
|
|
292
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { readUserConfig } from "./user-config.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {{
|
|
5
|
-
* mode: 'eager' | 'lazy' | 'manual',
|
|
6
|
-
* maxOpenPrs: number,
|
|
7
|
-
* fetchConcurrency: number,
|
|
8
|
-
* background: boolean,
|
|
9
|
-
* lazyFirstPassMaxPrs: number
|
|
10
|
-
* }} StackDiscoveryResolved
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Resolve stack discovery options from ~/.config/nugit/config.json and env.
|
|
15
|
-
* Env overrides: NUGIT_STACK_DISCOVERY_MODE, NUGIT_STACK_DISCOVERY_MAX_OPEN_PRS, NUGIT_STACK_DISCOVERY_CONCURRENCY, NUGIT_STACK_DISCOVERY_BACKGROUND
|
|
16
|
-
* @param {Partial<StackDiscoveryResolved>} [cliOverrides] CLI flags override file/env for that invocation
|
|
17
|
-
* @returns {StackDiscoveryResolved}
|
|
18
|
-
*/
|
|
19
|
-
export function getStackDiscoveryOpts(cliOverrides = {}) {
|
|
20
|
-
const cfg = readUserConfig();
|
|
21
|
-
const sd =
|
|
22
|
-
cfg.stackDiscovery && typeof cfg.stackDiscovery === "object"
|
|
23
|
-
? /** @type {Record<string, unknown>} */ (cfg.stackDiscovery)
|
|
24
|
-
: {};
|
|
25
|
-
|
|
26
|
-
const envMode = process.env.NUGIT_STACK_DISCOVERY_MODE;
|
|
27
|
-
const modeRaw =
|
|
28
|
-
cliOverrides.mode ??
|
|
29
|
-
envMode ??
|
|
30
|
-
(typeof sd.mode === "string" ? sd.mode : "eager");
|
|
31
|
-
const mode =
|
|
32
|
-
modeRaw === "lazy" || modeRaw === "manual" || modeRaw === "eager" ? modeRaw : "eager";
|
|
33
|
-
|
|
34
|
-
const maxFromEnv = process.env.NUGIT_STACK_DISCOVERY_MAX_OPEN_PRS;
|
|
35
|
-
const maxOpenPrs =
|
|
36
|
-
cliOverrides.maxOpenPrs ??
|
|
37
|
-
(maxFromEnv != null && maxFromEnv !== ""
|
|
38
|
-
? Number.parseInt(maxFromEnv, 10)
|
|
39
|
-
: typeof sd.maxOpenPrs === "number"
|
|
40
|
-
? sd.maxOpenPrs
|
|
41
|
-
: mode === "lazy"
|
|
42
|
-
? 100
|
|
43
|
-
: 500);
|
|
44
|
-
|
|
45
|
-
const concFromEnv = process.env.NUGIT_STACK_DISCOVERY_CONCURRENCY;
|
|
46
|
-
const fetchConcurrency =
|
|
47
|
-
cliOverrides.fetchConcurrency ??
|
|
48
|
-
(concFromEnv != null && concFromEnv !== ""
|
|
49
|
-
? Number.parseInt(concFromEnv, 10)
|
|
50
|
-
: typeof sd.fetchConcurrency === "number"
|
|
51
|
-
? sd.fetchConcurrency
|
|
52
|
-
: 8);
|
|
53
|
-
|
|
54
|
-
const bgEnv = process.env.NUGIT_STACK_DISCOVERY_BACKGROUND;
|
|
55
|
-
const background =
|
|
56
|
-
cliOverrides.background ??
|
|
57
|
-
(bgEnv === "1" || bgEnv === "true"
|
|
58
|
-
? true
|
|
59
|
-
: bgEnv === "0" || bgEnv === "false"
|
|
60
|
-
? false
|
|
61
|
-
: typeof sd.background === "boolean"
|
|
62
|
-
? sd.background
|
|
63
|
-
: false);
|
|
64
|
-
|
|
65
|
-
const lazyFirst =
|
|
66
|
-
typeof sd.lazyFirstPassMaxPrs === "number" ? sd.lazyFirstPassMaxPrs : Math.min(50, maxOpenPrs || 50);
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
mode,
|
|
70
|
-
maxOpenPrs: Number.isFinite(maxOpenPrs) && maxOpenPrs >= 0 ? maxOpenPrs : 500,
|
|
71
|
-
fetchConcurrency: Math.max(1, Math.min(32, Number.isFinite(fetchConcurrency) ? fetchConcurrency : 8)),
|
|
72
|
-
background,
|
|
73
|
-
lazyFirstPassMaxPrs: Math.max(1, lazyFirst)
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* For lazy mode: first pass cap (smaller scan before optional full refresh).
|
|
79
|
-
* @param {StackDiscoveryResolved} opts
|
|
80
|
-
* @param {boolean} full If true, use maxOpenPrs
|
|
81
|
-
* @returns {number}
|
|
82
|
-
*/
|
|
83
|
-
export function effectiveMaxOpenPrs(opts, full) {
|
|
84
|
-
if (full || opts.mode === "eager") {
|
|
85
|
-
return opts.maxOpenPrs;
|
|
86
|
-
}
|
|
87
|
-
if (opts.mode === "manual") {
|
|
88
|
-
return opts.maxOpenPrs;
|
|
89
|
-
}
|
|
90
|
-
return Math.min(opts.lazyFirstPassMaxPrs, opts.maxOpenPrs || 100);
|
|
91
|
-
}
|