git-stack-cli 2.9.3 → 2.9.4

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.
@@ -3,6 +3,7 @@
3
3
  import { Store } from "~/app/Store";
4
4
  import * as git from "~/core/git";
5
5
  import * as github from "~/core/github";
6
+ import { invariant } from "~/core/invariant";
6
7
 
7
8
  export type CommitRange = Awaited<ReturnType<typeof range>>;
8
9
 
@@ -28,14 +29,16 @@ type CommitRangeGroup = {
28
29
  type CommitGroupMap = { [sha: string]: CommitRangeGroup };
29
30
 
30
31
  export async function range(commit_group_map?: CommitGroupMap) {
31
- const DEBUG = process.env.DEV && false;
32
-
33
32
  const state = Store.getState();
34
33
  const actions = state.actions;
34
+ const argv = state.argv;
35
+ const merge_base = state.merge_base;
35
36
  const master_branch = state.master_branch;
36
37
  const master_branch_name = master_branch.replace(/^origin\//, "");
37
38
  const commit_list = await git.get_commits(`${master_branch}..HEAD`);
38
39
 
40
+ invariant(merge_base, "merge_base must exist");
41
+
39
42
  const pr_lookup: Record<string, void | PullRequest> = {};
40
43
 
41
44
  let invalid = false;
@@ -125,6 +128,15 @@ export async function range(commit_group_map?: CommitGroupMap) {
125
128
  const group = group_value_list[i];
126
129
  const previous_group: undefined | CommitGroup = group_value_list[i - 1];
127
130
 
131
+ // actions.json({ group });
132
+ actions.debug(`title=${group.title}`);
133
+ actions.debug(` id=${group.id}`);
134
+ actions.debug(` master_base=${group.master_base}`);
135
+
136
+ // special case
137
+ // boundary between normal commits and master commits
138
+ const MASTER_BASE_BOUNDARY = !group.master_base && previous_group && previous_group.master_base;
139
+
128
140
  if (group.id !== UNASSIGNED) {
129
141
  let pr_result = pr_lookup[group.id];
130
142
 
@@ -145,9 +157,8 @@ export async function range(commit_group_map?: CommitGroupMap) {
145
157
  if (i === 0) {
146
158
  group.base = master_branch_name;
147
159
  } else {
148
- const last_group = group_value_list[i - 1];
149
- // console.debug(" ", "last_group", last_group.pr?.title.substring(0, 40));
150
- // console.debug(" ", "last_group.id", last_group.id);
160
+ // console.debug(" ", "previous_group", previous_group.pr?.title.substring(0, 40));
161
+ // console.debug(" ", "previous_group.id", previous_group.id);
151
162
 
152
163
  if (group.master_base) {
153
164
  // explicitly set base to master when master_base is true
@@ -155,73 +166,93 @@ export async function range(commit_group_map?: CommitGroupMap) {
155
166
  } else if (group.id === UNASSIGNED) {
156
167
  // null out base when unassigned and after unassigned
157
168
  group.base = null;
158
- } else if (last_group.base === null) {
169
+ } else if (MASTER_BASE_BOUNDARY) {
170
+ // ensure we set its base to `master`
171
+ actions.debug(` MASTER_BASE_BOUNDARY set group.base = ${master_branch_name}`);
172
+ group.base = master_branch_name;
173
+ } else if (previous_group.base === null) {
159
174
  // null out base when last group base is null
160
175
  group.base = null;
161
176
  } else {
162
- group.base = last_group.id;
177
+ group.base = previous_group.id;
163
178
  }
164
-
165
- // console.debug(" ", "group.base", group.base);
166
179
  }
167
180
 
168
- actions.json({ group });
181
+ actions.debug(` base=${group.base}`);
169
182
 
170
183
  if (!group.pr) {
184
+ actions.debug(` group.pr=${group.pr}`);
171
185
  group.dirty = true;
172
186
  } else {
173
- if (group.pr.baseRefName !== group.base) {
174
- actions.debug("PR_BASEREF_MISMATCH");
187
+ // actions.json(group.pr);
188
+ actions.debug(` group.pr.state=${group.pr.state}`);
189
+ actions.debug(` group.pr.baseRefName=${group.pr.baseRefName}`);
190
+
191
+ if (group.pr.state === "MERGED" || group.pr.state === "CLOSED") {
192
+ group.dirty = true;
193
+ } else if (group.pr.baseRefName !== group.base) {
194
+ actions.debug(" PR_BASEREF_MISMATCH");
175
195
  group.dirty = true;
176
196
  } else if (group.master_base) {
177
- actions.debug("MASTER_BASE_DIFF_COMPARE");
178
-
179
- // special case
180
- // master_base groups cannot be compared by commit sha
181
- // instead compare the literal diff local against origin
182
- // gh pr diff --color=never 110
183
- // git --no-pager diff --color=never 00c8fe0~1..00c8fe0
184
- let diff_github = await github.pr_diff(group.pr.number);
185
- diff_github = normalize_diff(diff_github);
186
-
187
- let diff_local = await git.get_diff(group.commits);
188
- diff_local = normalize_diff(diff_local);
189
-
190
- actions.json({ diff_local, diff_github });
191
-
192
- // find the first differing character index
193
- let compare_length = Math.max(diff_github.length, diff_local.length);
194
- let diff_index = -1;
195
- for (let c_i = 0; c_i < compare_length; c_i++) {
196
- if (diff_github[c_i] !== diff_local[c_i]) {
197
- diff_index = c_i;
198
- break;
197
+ // first check if merge base has changed
198
+ let branch_compare = await github.pr_compare(group.pr.headRefName);
199
+ if (!(branch_compare instanceof Error)) {
200
+ if (branch_compare.merge_base_commit.sha !== merge_base) {
201
+ actions.debug(" MASTER_BASE_MERGE_BASE_MISMATCH");
202
+ group.dirty = true;
199
203
  }
200
204
  }
201
- if (diff_index > -1) {
202
- group.dirty = true;
203
205
 
204
- if (DEBUG) {
205
- // print preview at diff_index for both strings
206
- const preview_radius = 30;
207
- const start_index = Math.max(0, diff_index - preview_radius);
208
- const end_index = Math.min(compare_length, diff_index + preview_radius);
206
+ // if still not dirty, check diffs
207
+ if (!group.dirty) {
208
+ actions.debug(" MASTER_BASE_DIFF_COMPARE");
209
+
210
+ // special case
211
+ // master_base groups cannot be compared by commit sha
212
+ // instead compare the literal diff local against origin
213
+ // gh pr diff --color=never 110
214
+ // git --no-pager diff --color=never 00c8fe0~1..00c8fe0
215
+ let diff_github = await github.pr_diff(group.pr.headRefName);
216
+ diff_github = normalize_diff(diff_github);
217
+
218
+ let diff_local = await git.get_diff(group.commits);
219
+ diff_local = normalize_diff(diff_local);
220
+
221
+ // find the first differing character index
222
+ let compare_length = Math.max(diff_github.length, diff_local.length);
223
+ let diff_index = -1;
224
+ for (let c_i = 0; c_i < compare_length; c_i++) {
225
+ if (diff_github[c_i] !== diff_local[c_i]) {
226
+ diff_index = c_i;
227
+ break;
228
+ }
229
+ }
230
+ if (diff_index > -1) {
231
+ actions.debug(" MASTER_BASE_DIFF_MISMATCH");
232
+ group.dirty = true;
233
+
234
+ if (argv.verbose) {
235
+ // print preview at diff_index for both strings
236
+ const preview_radius = 30;
237
+ const start_index = Math.max(0, diff_index - preview_radius);
238
+ const end_index = Math.min(compare_length, diff_index + preview_radius);
209
239
 
210
- diff_github = diff_github.substring(start_index, end_index);
211
- diff_github = JSON.stringify(diff_github).slice(1, -1);
240
+ diff_github = diff_github.substring(start_index, end_index);
241
+ diff_github = JSON.stringify(diff_github).slice(1, -1);
212
242
 
213
- diff_local = diff_local.substring(start_index, end_index);
214
- diff_local = JSON.stringify(diff_local).slice(1, -1);
243
+ diff_local = diff_local.substring(start_index, end_index);
244
+ diff_local = JSON.stringify(diff_local).slice(1, -1);
215
245
 
216
- let pointer_indent = " ".repeat(diff_index - start_index + 1);
217
- actions.debug(`⚠️ git diff mismatch`);
218
- actions.debug(` ${pointer_indent}⌄`);
219
- actions.debug(`diff_github …${diff_github}…`);
220
- actions.debug(`diff_local …${diff_local}…`);
221
- actions.debug(` ${pointer_indent}⌃`);
246
+ let pointer_indent = " ".repeat(diff_index - start_index + 1);
247
+ actions.debug(` ⚠️ git diff mismatch`);
248
+ actions.debug(` ${pointer_indent}⌄`);
249
+ actions.debug(` diff_github …${diff_github}…`);
250
+ actions.debug(` diff_local …${diff_local}…`);
251
+ actions.debug(` ${pointer_indent}⌃`);
252
+ }
222
253
  }
223
254
  }
224
- } else if (!group.master_base && previous_group && previous_group.master_base) {
255
+ } else if (MASTER_BASE_BOUNDARY) {
225
256
  // special case
226
257
  // boundary between normal commits and master commits
227
258
 
@@ -239,24 +270,23 @@ export async function range(commit_group_map?: CommitGroupMap) {
239
270
 
240
271
  // compare all commits against pr commits
241
272
  if (group.pr.commits.length !== all_commits.length) {
242
- actions.debug("BOUNDARY_COMMIT_LENGTH_MISMATCH");
273
+ actions.debug(" BOUNDARY_COMMIT_LENGTH_MISMATCH");
243
274
  group.dirty = true;
244
275
  } else {
245
- actions.debug("BOUNDARY_COMMIT_SHA_COMPARISON");
246
276
  for (let i = 0; i < group.pr.commits.length; i++) {
247
277
  const pr_commit = group.pr.commits[i];
248
278
  const local_commit = all_commits[i];
249
279
 
250
280
  if (pr_commit.oid !== local_commit.sha) {
281
+ actions.debug(" BOUNDARY_COMMIT_SHA_MISMATCH");
251
282
  group.dirty = true;
252
283
  }
253
284
  }
254
285
  }
255
286
  } else if (group.pr.commits.length !== group.commits.length) {
256
- actions.debug("COMMIT_LENGTH_MISMATCH");
287
+ actions.debug(" COMMIT_LENGTH_MISMATCH");
257
288
  group.dirty = true;
258
289
  } else {
259
- actions.debug("COMMIT_SHA_COMPARISON");
260
290
  // if we still haven't marked this dirty, check each commit
261
291
  // comapre literal commit shas in group
262
292
  for (let i = 0; i < group.pr.commits.length; i++) {
@@ -264,14 +294,14 @@ export async function range(commit_group_map?: CommitGroupMap) {
264
294
  const local_commit = group.commits[i];
265
295
 
266
296
  if (pr_commit.oid !== local_commit.sha) {
267
- actions.json({ pr_commit, local_commit });
297
+ actions.debug(" COMMIT_SHA_MISMATCH");
268
298
  group.dirty = true;
269
299
  }
270
300
  }
271
301
  }
272
302
  }
273
303
 
274
- // console.debug(" ", "group.dirty", group.dirty);
304
+ actions.debug(` group.dirty=${group.dirty}`);
275
305
  }
276
306
 
277
307
  // reverse group_list to match git log
package/src/core/cli.ts CHANGED
@@ -8,6 +8,7 @@ type SpawnOptions = Parameters<typeof child.spawn>[2];
8
8
  type Options = SpawnOptions & {
9
9
  ignoreExitCode?: boolean;
10
10
  onOutput?: (data: string) => void;
11
+ quiet?: boolean;
11
12
  };
12
13
 
13
14
  type Return = {
@@ -45,13 +46,15 @@ export async function cli(
45
46
 
46
47
  const id = `${++i}-${command}`;
47
48
  state.actions.debug(log.start(command));
48
- state.actions.debug(log.pending(command), id);
49
+ state.actions.debug_pending(id, log.pending(command));
49
50
 
50
51
  const timer = Timer();
51
52
 
52
53
  function write_output(value: string) {
53
54
  output += value;
54
- state.actions.debug(value, id);
55
+ if (!options.quiet) {
56
+ state.actions.debug_pending(id, value);
57
+ }
55
58
  options.onOutput?.(value);
56
59
  }
57
60
 
@@ -79,9 +82,11 @@ export async function cli(
79
82
  duration,
80
83
  };
81
84
 
82
- state.actions.set((state) => state.mutate.end_pending_output(state, id));
85
+ state.actions.debug_pending_end(id);
83
86
  state.actions.debug(log.end(result));
84
- state.actions.debug(log.output(result));
87
+ if (!options.quiet) {
88
+ state.actions.debug(log.output(result));
89
+ }
85
90
 
86
91
  if (!options.ignoreExitCode && result.code !== 0) {
87
92
  state.actions.debug(log.non_zero_exit(result));
@@ -133,7 +138,9 @@ cli.sync = function cli_sync(
133
138
  };
134
139
 
135
140
  state.actions.debug(log.end(result));
136
- state.actions.debug(log.output(result));
141
+ if (!options.quiet) {
142
+ state.actions.debug(log.output(result));
143
+ }
137
144
 
138
145
  if (!options.ignoreExitCode && result.code !== 0) {
139
146
  state.actions.debug(log.non_zero_exit(result));
package/src/core/git.ts CHANGED
@@ -61,7 +61,7 @@ export async function get_diff(commit_list: CommitList) {
61
61
  const first_commit = commit_list[0];
62
62
  const last_commit = commit_list[commit_list.length - 1];
63
63
  const sha_range = `${first_commit.sha}~1..${last_commit.sha}`;
64
- const diff_result = await cli(`git --no-pager diff --color=never ${sha_range}`);
64
+ const diff_result = await cli(`git --no-pager diff --color=never ${sha_range}`, { quiet: true });
65
65
  return diff_result.stdout;
66
66
  }
67
67
 
@@ -40,7 +40,6 @@ export async function pr_list(): Promise<Array<PullRequest>> {
40
40
  if (actions.isDebug()) {
41
41
  actions.output(
42
42
  <FormatText
43
- wrapper={<Ink.Text dimColor />}
44
43
  message="Github cache {count} open PRs from {repo_path} authored by {username}"
45
44
  values={{
46
45
  count: (
@@ -101,7 +100,8 @@ export async function pr_status(branch: string): Promise<null | PullRequest> {
101
100
  );
102
101
  }
103
102
 
104
- const pr = await gh_json<PullRequest>(`pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`);
103
+ const commmand = `pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`;
104
+ const pr = await gh_json<PullRequest>(commmand, { branch });
105
105
 
106
106
  if (pr instanceof Error) {
107
107
  return null;
@@ -241,48 +241,37 @@ export async function pr_draft(args: DraftPullRequestArgs) {
241
241
  }
242
242
  }
243
243
 
244
- export async function pr_diff(number: number) {
245
- const state = Store.getState();
246
- const actions = state.actions;
244
+ export async function pr_diff(branch: string) {
245
+ // https://cli.github.com/manual/gh_pr_diff
246
+ const result = await gh(`pr diff --color=never ${branch}`, { branch });
247
247
 
248
- const maybe_diff = state.cache_pr_diff[number];
248
+ if (result instanceof Error) {
249
+ handle_error(result.message);
250
+ }
249
251
 
250
- if (maybe_diff) {
251
- if (actions.isDebug()) {
252
- actions.debug(
253
- cache_message({
254
- hit: true,
255
- message: "Github pr_diff cache",
256
- extra: number,
257
- }),
258
- );
259
- }
252
+ return result;
253
+ }
260
254
 
261
- return maybe_diff;
262
- }
255
+ export async function pr_compare(branch: string) {
256
+ const state = Store.getState();
257
+ const master_branch = state.master_branch;
258
+ const repo_path = state.repo_path;
259
+ invariant(master_branch, "master_branch must exist");
260
+ invariant(repo_path, "repo_path must exist");
263
261
 
264
- if (actions.isDebug()) {
265
- actions.debug(
266
- cache_message({
267
- hit: false,
268
- message: "Github pr_diff cache",
269
- extra: number,
270
- }),
271
- );
272
- }
262
+ const master_branch_name = master_branch.replace(/^origin\//, "");
273
263
 
274
- // https://cli.github.com/manual/gh_pr_diff
275
- const cli_result = await cli(`gh pr diff --color=never ${number}`);
264
+ // gh api repos/openai/openai/compare/master...chrome/publish/vine-1211---4h2xmw0o3ndvnt
265
+ const result = await gh_json<BranchCompare>(
266
+ `api repos/${repo_path}/compare/${master_branch_name}...${branch}`,
267
+ { branch },
268
+ );
276
269
 
277
- if (cli_result.code !== 0) {
278
- handle_error(cli_result.output);
270
+ if (result instanceof Error) {
271
+ handle_error(result.message);
279
272
  }
280
273
 
281
- actions.set((state) => {
282
- state.cache_pr_diff[number] = cli_result.output;
283
- });
284
-
285
- return cli_result.stdout;
274
+ return result;
286
275
  }
287
276
 
288
277
  // pull request JSON fields
@@ -290,29 +279,99 @@ export async function pr_diff(number: number) {
290
279
  // prettier-ignore
291
280
  const JSON_FIELDS = "--json id,number,state,baseRefName,headRefName,commits,title,body,url,isDraft";
292
281
 
282
+ type GhCmdOptions = {
283
+ branch?: string;
284
+ };
285
+
293
286
  // consistent handle gh cli commands returning json
294
287
  // redirect to tmp file to avoid scrollback overflow causing scrollback to be cleared
295
- async function gh_json<T>(command: string): Promise<T | Error> {
288
+ async function gh_json<T>(command: string, gh_options?: GhCmdOptions): Promise<T | Error> {
289
+ const gh_result = await gh(command, gh_options);
290
+
291
+ if (gh_result instanceof Error) {
292
+ return gh_result;
293
+ }
294
+
295
+ try {
296
+ const json = JSON.parse(gh_result);
297
+ return json as T;
298
+ } catch (error) {
299
+ return new Error(`gh_json JSON.parse: ${error}`);
300
+ }
301
+ }
302
+
303
+ // consistent handle gh cli commands
304
+ // redirect to tmp file to avoid scrollback overflow causing scrollback to be cleared
305
+ async function gh(command: string, gh_options?: GhCmdOptions): Promise<string | Error> {
306
+ const state = Store.getState();
307
+ const actions = state.actions;
308
+
309
+ if (gh_options?.branch) {
310
+ const branch = gh_options.branch;
311
+
312
+ type CacheEntryByHeadRefName = (typeof state.cache_gh_cli_by_branch)[string][string];
313
+
314
+ let cache: undefined | CacheEntryByHeadRefName = undefined;
315
+
316
+ if (branch) {
317
+ if (state.cache_gh_cli_by_branch[branch]) {
318
+ cache = state.cache_gh_cli_by_branch[branch][command];
319
+ }
320
+ }
321
+
322
+ if (cache) {
323
+ if (actions.isDebug()) {
324
+ actions.debug(
325
+ cache_message({
326
+ hit: true,
327
+ message: "gh cache",
328
+ extra: command,
329
+ }),
330
+ );
331
+ }
332
+
333
+ return cache;
334
+ }
335
+
336
+ if (actions.isDebug()) {
337
+ actions.debug(
338
+ cache_message({
339
+ hit: false,
340
+ message: "gh cache",
341
+ extra: command,
342
+ }),
343
+ );
344
+ }
345
+ }
346
+
296
347
  // hash command for unique short string
297
348
  let hash = crypto.createHash("md5").update(command).digest("hex");
298
- let tmp_filename = safe_filename(`gh_json-${hash}`);
299
- const tmp_pr_json = path.join(await get_tmp_dir(), `${tmp_filename}.json`);
349
+ let tmp_filename = safe_filename(`gh-${hash}`);
350
+ const tmp_filepath = path.join(await get_tmp_dir(), `${tmp_filename}`);
300
351
 
301
352
  const options = { ignoreExitCode: true };
302
- const cli_result = await cli(`gh ${command} > ${tmp_pr_json}`, options);
353
+ const cli_result = await cli(`gh ${command} > ${tmp_filepath}`, options);
303
354
 
304
355
  if (cli_result.code !== 0) {
305
356
  return new Error(cli_result.output);
306
357
  }
307
358
 
308
359
  // read from file
309
- const json_str = String(await fs.readFile(tmp_pr_json));
310
- try {
311
- const json = JSON.parse(json_str);
312
- return json;
313
- } catch (error) {
314
- return new Error(`gh_json JSON.parse: ${error}`);
360
+ let content = String(await fs.readFile(tmp_filepath));
361
+ content = content.trim();
362
+
363
+ if (gh_options?.branch) {
364
+ const branch = gh_options.branch;
365
+
366
+ actions.set((state) => {
367
+ if (!state.cache_gh_cli_by_branch[branch]) {
368
+ state.cache_gh_cli_by_branch[branch] = {};
369
+ }
370
+ state.cache_gh_cli_by_branch[branch][command] = content;
371
+ });
315
372
  }
373
+
374
+ return content;
316
375
  }
317
376
 
318
377
  function handle_error(output: string): never {
@@ -408,6 +467,36 @@ export type PullRequest = {
408
467
  isDraft: boolean;
409
468
  };
410
469
 
470
+ type MergeBaseCommit = {
471
+ author: unknown;
472
+ comments_url: string;
473
+ commit: unknown;
474
+ committer: unknown;
475
+ html_url: string;
476
+ node_id: string;
477
+ parents: unknown;
478
+ sha: string;
479
+ url: string;
480
+ };
481
+
482
+ export type BranchCompare = {
483
+ ahead_by: number;
484
+ base_commit: unknown;
485
+ behind_by: number;
486
+ commits: unknown;
487
+ diff_url: string;
488
+
489
+ files: unknown;
490
+ html_url: string;
491
+
492
+ merge_base_commit: MergeBaseCommit;
493
+ patch_url: string;
494
+ permalink_url: string;
495
+ status: unknown;
496
+ total_commits: number;
497
+ url: string;
498
+ };
499
+
411
500
  const RE = {
412
501
  non_alphanumeric_dash: /[^a-zA-Z0-9_-]+/g,
413
502
  };
package/src/index.tsx CHANGED
@@ -5,7 +5,6 @@
5
5
  import * as React from "react";
6
6
 
7
7
  import fs from "node:fs/promises";
8
- import path from "node:path";
9
8
 
10
9
  import * as Ink from "ink-cjs";
11
10
 
@@ -61,10 +60,6 @@ import { pretty_json } from "~/core/pretty_json";
61
60
 
62
61
  actions.debug(pretty_json(argv as any));
63
62
 
64
- const PATH = process.env["PATH"];
65
- const PATH_LIST = pretty_json(PATH.split(path.delimiter));
66
- actions.debug(`process.env.PATH ${PATH_LIST}`);
67
-
68
63
  await ink.waitUntilExit();
69
64
 
70
65
  function maybe_verbose_help() {
@@ -1,6 +1,7 @@
1
1
  declare namespace NodeJS {
2
2
  interface ProcessEnv {
3
3
  PATH: string;
4
+ HOME: string;
4
5
  DEV?: "true" | "false";
5
6
  CLI_VERSION?: string;
6
7
  GIT_SEQUENCE_EDITOR_SCRIPT?: string;
@@ -1,8 +0,0 @@
1
- import * as React from "react";
2
-
3
- import * as Ink from "ink-cjs";
4
- import { DateTime } from "luxon";
5
-
6
- export function LogTimestamp() {
7
- return <Ink.Text dimColor>{DateTime.now().toFormat("[yyyy-MM-dd HH:mm:ss.SSS] ")}</Ink.Text>;
8
- }