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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/api-client.js +10 -11
  3. package/src/github-device-flow.js +1 -1
  4. package/src/github-oauth-client-id.js +11 -0
  5. package/src/github-pr-social.js +42 -0
  6. package/src/github-rest.js +114 -6
  7. package/src/nugit-stack.js +20 -0
  8. package/src/nugit-start.js +4 -4
  9. package/src/nugit.js +37 -22
  10. package/src/review-hub/review-autoapprove.js +95 -0
  11. package/src/review-hub/review-hub-back.js +10 -0
  12. package/src/review-hub/review-hub-ink.js +166 -0
  13. package/src/review-hub/run-review-hub.js +188 -0
  14. package/src/split-view/run-split.js +16 -3
  15. package/src/split-view/split-ink.js +2 -2
  16. package/src/stack-discover.js +9 -1
  17. package/src/stack-infer-from-prs.js +71 -0
  18. package/src/stack-view/diff-line-map.js +62 -0
  19. package/src/stack-view/fetch-pr-data.js +104 -4
  20. package/src/stack-view/infer-chains-to-pick-stacks.js +70 -0
  21. package/src/stack-view/ink-app.js +1853 -156
  22. package/src/stack-view/loading-ink.js +44 -0
  23. package/src/stack-view/merge-alternate-pick-stacks.js +223 -0
  24. package/src/stack-view/patch-preview-merge.js +108 -0
  25. package/src/stack-view/remote-infer-doc.js +93 -0
  26. package/src/stack-view/repo-picker-back.js +10 -0
  27. package/src/stack-view/run-stack-view.js +685 -50
  28. package/src/stack-view/run-view-entry.js +119 -0
  29. package/src/stack-view/sgr-mouse.js +56 -0
  30. package/src/stack-view/stack-branch-graph.js +95 -0
  31. package/src/stack-view/stack-pick-graph.js +93 -0
  32. package/src/stack-view/stack-pick-ink.js +270 -0
  33. package/src/stack-view/stack-pick-layout.js +19 -0
  34. package/src/stack-view/stack-pick-sort.js +188 -0
  35. package/src/stack-view/terminal-fullscreen.js +45 -0
  36. package/src/stack-view/tree-ascii.js +73 -0
  37. package/src/stack-view/view-md-plain.js +23 -0
  38. package/src/stack-view/view-repo-picker-ink.js +293 -0
  39. package/src/stack-view/view-tui-sequential.js +126 -0
@@ -1,4 +1,11 @@
1
- import { githubGetPull, githubListPullFiles, githubRestJson } from "../github-rest.js";
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
- githubListPullReviewsFirstPage(owner, repo, n)
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(reviews),
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
+ }