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,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
githubGetPull,
|
|
3
|
+
githubListPullFiles,
|
|
4
|
+
githubRestJson,
|
|
5
|
+
githubCompareRefs
|
|
6
|
+
} from "../github-rest.js";
|
|
7
|
+
import { githubListPullReviewsAll } from "../github-pr-social.js";
|
|
8
|
+
import { compareLooksLikeMainMergesOnly } from "../review-hub/review-autoapprove.js";
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* @param {unknown[]} items
|
|
@@ -80,35 +87,127 @@ function summarizeReviewState(reviews) {
|
|
|
80
87
|
return "none";
|
|
81
88
|
}
|
|
82
89
|
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} owner
|
|
92
|
+
* @param {string} repo
|
|
93
|
+
* @param {unknown[]} reviews
|
|
94
|
+
* @param {string} viewerLogin
|
|
95
|
+
* @param {string} headSha
|
|
96
|
+
* @param {string} defaultBranch
|
|
97
|
+
*/
|
|
98
|
+
async function buildViewerReviewMeta(owner, repo, reviews, viewerLogin, headSha, defaultBranch) {
|
|
99
|
+
/** @type {{ staleApproval: boolean, lastApprovalCommit: string | null, headSha: string, dismissedReview: boolean, riskyChangeAfterApproval: boolean | null, commitsSinceApproval: number }} */
|
|
100
|
+
const meta = {
|
|
101
|
+
staleApproval: false,
|
|
102
|
+
lastApprovalCommit: null,
|
|
103
|
+
headSha,
|
|
104
|
+
dismissedReview: false,
|
|
105
|
+
riskyChangeAfterApproval: null,
|
|
106
|
+
commitsSinceApproval: 0
|
|
107
|
+
};
|
|
108
|
+
if (!viewerLogin || !headSha) {
|
|
109
|
+
return meta;
|
|
110
|
+
}
|
|
111
|
+
for (const rv of reviews) {
|
|
112
|
+
if (!rv || typeof rv !== "object") continue;
|
|
113
|
+
const r = /** @type {Record<string, unknown>} */ (rv);
|
|
114
|
+
const user = r.user && typeof r.user === "object" ? /** @type {Record<string, unknown>} */ (r.user) : {};
|
|
115
|
+
const login = typeof user.login === "string" ? user.login : "";
|
|
116
|
+
if (login !== viewerLogin) continue;
|
|
117
|
+
const state = typeof r.state === "string" ? r.state.toUpperCase() : "";
|
|
118
|
+
if (state === "DISMISSED") {
|
|
119
|
+
meta.dismissedReview = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let lastApprovalCommit = null;
|
|
123
|
+
for (let i = reviews.length - 1; i >= 0; i--) {
|
|
124
|
+
const rv = reviews[i];
|
|
125
|
+
if (!rv || typeof rv !== "object") continue;
|
|
126
|
+
const r = /** @type {Record<string, unknown>} */ (rv);
|
|
127
|
+
const user = r.user && typeof r.user === "object" ? /** @type {Record<string, unknown>} */ (r.user) : {};
|
|
128
|
+
const login = typeof user.login === "string" ? user.login : "";
|
|
129
|
+
if (login !== viewerLogin) continue;
|
|
130
|
+
const state = typeof r.state === "string" ? r.state.toUpperCase() : "";
|
|
131
|
+
if (state === "APPROVED" && typeof r.commit_id === "string") {
|
|
132
|
+
lastApprovalCommit = r.commit_id;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
meta.lastApprovalCommit = lastApprovalCommit;
|
|
137
|
+
if (lastApprovalCommit && lastApprovalCommit !== headSha) {
|
|
138
|
+
meta.staleApproval = true;
|
|
139
|
+
try {
|
|
140
|
+
const cmp = await githubCompareRefs(owner, repo, lastApprovalCommit, headSha);
|
|
141
|
+
const total = typeof cmp.total_commits === "number" ? cmp.total_commits : 0;
|
|
142
|
+
meta.commitsSinceApproval = total;
|
|
143
|
+
const mergeOnly = compareLooksLikeMainMergesOnly(
|
|
144
|
+
/** @type {Record<string, unknown>} */ (cmp),
|
|
145
|
+
defaultBranch
|
|
146
|
+
);
|
|
147
|
+
meta.riskyChangeAfterApproval = !mergeOnly;
|
|
148
|
+
} catch {
|
|
149
|
+
meta.riskyChangeAfterApproval = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return meta;
|
|
153
|
+
}
|
|
154
|
+
|
|
83
155
|
/**
|
|
84
156
|
* @param {string} owner
|
|
85
157
|
* @param {string} repo
|
|
86
158
|
* @param {Array<{ pr_number: number, position: number, head_branch?: string, base_branch?: string }>} prEntries
|
|
87
159
|
* @param {number} [concurrency]
|
|
160
|
+
* @param {{ viewerLogin?: string, defaultBranch?: string, fullReviewFetch?: boolean }} [fetchOpts]
|
|
88
161
|
*/
|
|
89
|
-
export async function fetchStackPrDetails(owner, repo, prEntries, concurrency = 6) {
|
|
162
|
+
export async function fetchStackPrDetails(owner, repo, prEntries, concurrency = 6, fetchOpts = {}) {
|
|
163
|
+
const viewerLogin = fetchOpts.viewerLogin || "";
|
|
164
|
+
const defaultBranch = fetchOpts.defaultBranch || "main";
|
|
165
|
+
const fullFetch = !!fetchOpts.fullReviewFetch || !!viewerLogin;
|
|
166
|
+
|
|
90
167
|
const sorted = [...prEntries].sort((a, b) => a.position - b.position);
|
|
91
168
|
return mapInBatches(sorted, concurrency, async (entry) => {
|
|
92
169
|
const n = entry.pr_number;
|
|
93
170
|
try {
|
|
94
171
|
const pull = await githubGetPull(owner, repo, n);
|
|
172
|
+
const headSha =
|
|
173
|
+
pull && typeof pull === "object" && pull.head && typeof pull.head === "object"
|
|
174
|
+
? String(/** @type {Record<string, unknown>} */ (pull.head).sha || "")
|
|
175
|
+
: "";
|
|
176
|
+
|
|
95
177
|
const [issueRes, reviewRes, filesRes, reviewsRes] = await Promise.allSettled([
|
|
96
178
|
githubListIssueCommentsFirstPage(owner, repo, n),
|
|
97
179
|
githubListPullReviewCommentsFirstPage(owner, repo, n),
|
|
98
180
|
githubListPullFiles(owner, repo, n),
|
|
99
|
-
|
|
181
|
+
fullFetch
|
|
182
|
+
? githubListPullReviewsAll(owner, repo, n)
|
|
183
|
+
: githubListPullReviewsFirstPage(owner, repo, n)
|
|
100
184
|
]);
|
|
101
185
|
const issueComments = issueRes.status === "fulfilled" ? issueRes.value : [];
|
|
102
186
|
const reviewComments = reviewRes.status === "fulfilled" ? reviewRes.value : [];
|
|
103
187
|
const files = filesRes.status === "fulfilled" ? filesRes.value : [];
|
|
104
188
|
const reviews = reviewsRes.status === "fulfilled" ? reviewsRes.value : [];
|
|
189
|
+
const reviewList = Array.isArray(reviews) ? reviews : [];
|
|
190
|
+
|
|
191
|
+
let viewerReviewMeta = null;
|
|
192
|
+
if (viewerLogin && headSha) {
|
|
193
|
+
viewerReviewMeta = await buildViewerReviewMeta(
|
|
194
|
+
owner,
|
|
195
|
+
repo,
|
|
196
|
+
reviewList,
|
|
197
|
+
viewerLogin,
|
|
198
|
+
headSha,
|
|
199
|
+
defaultBranch
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
105
203
|
return {
|
|
106
204
|
entry,
|
|
107
205
|
pull,
|
|
108
206
|
issueComments,
|
|
109
207
|
reviewComments,
|
|
110
208
|
files: Array.isArray(files) ? files : [],
|
|
111
|
-
reviewSummary: summarizeReviewState(
|
|
209
|
+
reviewSummary: summarizeReviewState(reviewList),
|
|
210
|
+
viewerReviewMeta,
|
|
112
211
|
error: null
|
|
113
212
|
};
|
|
114
213
|
} catch (e) {
|
|
@@ -119,6 +218,7 @@ export async function fetchStackPrDetails(owner, repo, prEntries, concurrency =
|
|
|
119
218
|
reviewComments: [],
|
|
120
219
|
files: [],
|
|
121
220
|
reviewSummary: "none",
|
|
221
|
+
viewerReviewMeta: null,
|
|
122
222
|
error: String(/** @type {Error} */ (e).message || e)
|
|
123
223
|
};
|
|
124
224
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build StackPickInk-shaped entries from inferred PR chains (bottom → tip) and GitHub pull payloads.
|
|
3
|
+
*
|
|
4
|
+
* @param {number[][]} chains
|
|
5
|
+
* @param {unknown[]} pulls
|
|
6
|
+
* @returns {object[]}
|
|
7
|
+
*/
|
|
8
|
+
export function inferChainsToPickStacks(chains, pulls) {
|
|
9
|
+
/** @type {Map<number, Record<string, unknown>>} */
|
|
10
|
+
const byNum = new Map();
|
|
11
|
+
for (const raw of pulls) {
|
|
12
|
+
if (!raw || typeof raw !== "object") continue;
|
|
13
|
+
const p = /** @type {Record<string, unknown>} */ (raw);
|
|
14
|
+
const num = p.number;
|
|
15
|
+
if (typeof num !== "number") continue;
|
|
16
|
+
byNum.set(num, p);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return chains.map((chain, inferChainIndex) => {
|
|
20
|
+
/** @type {{ pr_number: number, position: number, head_branch?: string, title?: string, additions?: number, deletions?: number }[]} */
|
|
21
|
+
const prRows = [];
|
|
22
|
+
let sumAdd = 0;
|
|
23
|
+
let sumDel = 0;
|
|
24
|
+
for (let i = 0; i < chain.length; i++) {
|
|
25
|
+
const prn = chain[i];
|
|
26
|
+
const pull = byNum.get(prn);
|
|
27
|
+
const head =
|
|
28
|
+
pull && pull.head && typeof pull.head === "object"
|
|
29
|
+
? /** @type {Record<string, unknown>} */ (pull.head)
|
|
30
|
+
: {};
|
|
31
|
+
const ref = typeof head.ref === "string" ? head.ref : "";
|
|
32
|
+
const add = pull && typeof pull.additions === "number" ? pull.additions : 0;
|
|
33
|
+
const del = pull && typeof pull.deletions === "number" ? pull.deletions : 0;
|
|
34
|
+
sumAdd += add;
|
|
35
|
+
sumDel += del;
|
|
36
|
+
prRows.push({
|
|
37
|
+
pr_number: prn,
|
|
38
|
+
position: i,
|
|
39
|
+
head_branch: ref,
|
|
40
|
+
title: pull && typeof pull.title === "string" ? pull.title : undefined,
|
|
41
|
+
additions: add,
|
|
42
|
+
deletions: del
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const tipPr = chain[chain.length - 1];
|
|
47
|
+
const tipPull = tipPr ? byNum.get(tipPr) : undefined;
|
|
48
|
+
const tipHead = prRows.length ? String(prRows[prRows.length - 1].head_branch || "") : "";
|
|
49
|
+
const tip_updated_at =
|
|
50
|
+
tipPull && typeof tipPull.updated_at === "string" ? tipPull.updated_at : "";
|
|
51
|
+
const user =
|
|
52
|
+
tipPull && tipPull.user && typeof tipPull.user === "object"
|
|
53
|
+
? /** @type {Record<string, unknown>} */ (tipPull.user)
|
|
54
|
+
: {};
|
|
55
|
+
const login = typeof user.login === "string" ? user.login : "";
|
|
56
|
+
|
|
57
|
+
const hasLineStats = sumAdd > 0 || sumDel > 0;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
tip_pr_number: tipPr,
|
|
61
|
+
tip_head_branch: tipHead,
|
|
62
|
+
pr_count: chain.length,
|
|
63
|
+
created_by: login,
|
|
64
|
+
prs: prRows,
|
|
65
|
+
tip_updated_at,
|
|
66
|
+
inferChainIndex,
|
|
67
|
+
...(hasLineStats ? { inferDiffAdd: sumAdd, inferDiffDel: sumDel } : {})
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|