git-stack-cli 2.9.1 → 2.9.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -48,6 +48,10 @@ const REPO_ROOT = (await spawn.sync("git rev-parse --show-toplevel")).stdout;
48
48
 
49
49
  const define = await get_define();
50
50
 
51
+ if (DEV) {
52
+ define["process.env.DEV"] = JSON.stringify("true");
53
+ }
54
+
51
55
  const BUILD_CONFIG = {
52
56
  entrypoints: ["./src/index.tsx"],
53
57
  outdir: "./dist/js",
@@ -5,7 +5,7 @@ import { spawn } from "~/core/spawn";
5
5
 
6
6
  const REPO_ROOT = (await spawn.sync("git rev-parse --show-toplevel")).stdout;
7
7
 
8
- export async function get_define() {
8
+ export async function get_define(): Promise<Record<string, string>> {
9
9
  const PACKAGE_JSON = await file.read_json(path.join(REPO_ROOT, "package.json"));
10
10
  const GIT_SEQUENCE_EDITOR_SCRIPT_PATH = path.join(REPO_ROOT, "scripts", "git-sequence-editor.sh");
11
11
  const UNSAFE_GIT_SEQUENCE_EDITOR_SCRIPT = await file.read_text(GIT_SEQUENCE_EDITOR_SCRIPT_PATH);
@@ -152,6 +152,12 @@ export function AutoUpdate(props: Props) {
152
152
  init_state().catch(abort);
153
153
 
154
154
  async function init_state() {
155
+ if (process.env.DEV === "true") {
156
+ info(<Ink.Text color={colors.yellow}>Skip AutoUpdate</Ink.Text>);
157
+ patch({ status: "done" });
158
+ return;
159
+ }
160
+
155
161
  if (state.latest_version !== null) return;
156
162
 
157
163
  const local_version = process.env.CLI_VERSION;
@@ -15,7 +15,12 @@ type Props = {
15
15
  };
16
16
 
17
17
  export function GithubApiError(props: Props) {
18
- return <Await fallback={null} function={() => run(props)} />;
18
+ return (
19
+ <Await
20
+ fallback={<Ink.Text color={colors.yellow}>Fetching Github API usage…</Ink.Text>}
21
+ function={() => run(props)}
22
+ />
23
+ );
19
24
  }
20
25
 
21
26
  async function run(props: Props) {
@@ -6,17 +6,20 @@ import { Await } from "~/app/Await";
6
6
  import { StatusTable } from "~/app/StatusTable";
7
7
  import { Store } from "~/app/Store";
8
8
  import * as CommitMetadata from "~/core/CommitMetadata";
9
+ import { colors } from "~/core/colors";
9
10
 
10
11
  export function PostRebaseStatus() {
11
- return <Await fallback={null} function={run} />;
12
+ return (
13
+ <Await
14
+ fallback={<Ink.Text color={colors.yellow}>Fetching latest status…</Ink.Text>}
15
+ function={run}
16
+ />
17
+ );
12
18
  }
13
19
 
14
20
  async function run() {
15
21
  const actions = Store.getState().actions;
16
22
 
17
- // reset github pr cache before refreshing via commit range below
18
- actions.reset_pr();
19
-
20
23
  const commit_range = await CommitMetadata.range();
21
24
 
22
25
  actions.set((state) => {
@@ -14,7 +14,12 @@ import { invariant } from "~/core/invariant";
14
14
  import { safe_exists } from "~/core/safe_exists";
15
15
 
16
16
  export function PreManualRebase() {
17
- return <Await fallback={null} function={run} />;
17
+ return (
18
+ <Await
19
+ fallback={<Ink.Text color={colors.yellow}>Check PR templates…</Ink.Text>}
20
+ function={run}
21
+ />
22
+ );
18
23
  }
19
24
 
20
25
  async function run() {
@@ -9,7 +9,12 @@ import { colors } from "~/core/colors";
9
9
  import { invariant } from "~/core/invariant";
10
10
 
11
11
  export function Status() {
12
- return <Await fallback={null} function={run} />;
12
+ return (
13
+ <Await
14
+ fallback={<Ink.Text color={colors.yellow}>Fetching latest status…</Ink.Text>}
15
+ function={run}
16
+ />
17
+ );
13
18
  }
14
19
 
15
20
  async function run() {
package/src/app/Store.tsx CHANGED
@@ -76,7 +76,9 @@ export type State = {
76
76
  output: Array<React.ReactNode>;
77
77
  pending_output: Record<string, Array<React.ReactNode>>;
78
78
 
79
+ // cache
79
80
  pr: { [branch: string]: PullRequest };
81
+ cache_pr_diff: { [id: number]: string };
80
82
 
81
83
  actions: {
82
84
  exit(code: number, args?: ExitArgs): void;
@@ -90,7 +92,6 @@ export type State = {
90
92
 
91
93
  isDebug(): boolean;
92
94
 
93
- reset_pr(): void;
94
95
  register_abort_handler(abort_handler: AbortHandler): void;
95
96
  unregister_abort_handler(): void;
96
97
 
@@ -138,6 +139,7 @@ const BaseStore = createStore<State>()(
138
139
  pending_output: {},
139
140
 
140
141
  pr: {},
142
+ cache_pr_diff: {},
141
143
 
142
144
  actions: {
143
145
  exit(code, args) {
@@ -228,12 +230,6 @@ const BaseStore = createStore<State>()(
228
230
  return state.select.debug(state);
229
231
  },
230
232
 
231
- reset_pr() {
232
- set((state) => {
233
- state.pr = {};
234
- });
235
- },
236
-
237
233
  register_abort_handler(abort_handler) {
238
234
  set((state) => {
239
235
  state.abort_handler = abort_handler;
@@ -160,6 +160,15 @@ async function run() {
160
160
  actions.unregister_abort_handler();
161
161
 
162
162
  actions.set((state) => {
163
+ // invalidate cache for PRs we pushed
164
+ for (const group of push_group_list) {
165
+ if (group.pr) {
166
+ delete state.pr[group.pr.headRefName];
167
+ delete state.cache_pr_diff[group.pr.number];
168
+ }
169
+ }
170
+
171
+ // move to next step
163
172
  state.step = "post-rebase-status";
164
173
  });
165
174
  } catch (err) {
@@ -185,9 +194,13 @@ async function run() {
185
194
 
186
195
  const group = commit_range.group_list[index];
187
196
 
188
- if (group.id !== commit_range.UNASSIGNED) {
189
- push_group_list.unshift(group);
190
- }
197
+ // skip the unassigned commits group
198
+ if (group.id === commit_range.UNASSIGNED) continue;
199
+
200
+ // if not --force, skip non-dirty master_base groups
201
+ if (group.master_base && !group.dirty && !argv.force) continue;
202
+
203
+ push_group_list.unshift(group);
191
204
  }
192
205
 
193
206
  return push_group_list;
@@ -300,14 +313,14 @@ async function run() {
300
313
  async function push_master_group(group: CommitMetadataGroup) {
301
314
  invariant(repo_root, "repo_root must exist");
302
315
 
303
- const worktree_path = `.git/git-stack-worktrees/push_master_group`;
304
- const worktree_path_absolute = path.join(repo_root, worktree_path);
316
+ const repo_rel_worktree_path = `.git/git-stack-worktrees/push_master_group`;
317
+ const worktree_path = path.join(repo_root, repo_rel_worktree_path);
305
318
 
306
319
  // ensure worktree for pushing master groups
307
- if (!(await safe_exists(worktree_path_absolute))) {
320
+ if (!(await safe_exists(worktree_path))) {
308
321
  actions.output(
309
322
  <Ink.Text color={colors.white}>
310
- Creating <Ink.Text color={colors.yellow}>{worktree_path}</Ink.Text>
323
+ Creating <Ink.Text color={colors.yellow}>{repo_rel_worktree_path}</Ink.Text>
311
324
  </Ink.Text>,
312
325
  );
313
326
  actions.output(
package/src/command.ts CHANGED
@@ -60,7 +60,11 @@ export async function command(argv: string[], options: CommandOptions = {}) {
60
60
  .command(
61
61
  "config",
62
62
  "Generate a one-time configuration json based on the passed arguments",
63
- (yargs) => yargs,
63
+ (yargs) => {
64
+ // match options for default command (since we are generating a config for its options)
65
+ let builder = yargs.options(DefaultOptions);
66
+ return builder;
67
+ },
64
68
  )
65
69
 
66
70
  .command(
@@ -10,7 +10,12 @@ import { colors } from "~/core/colors";
10
10
  import { invariant } from "~/core/invariant";
11
11
 
12
12
  export function Config() {
13
- return <Await fallback={null} function={run} />;
13
+ return (
14
+ <Await
15
+ fallback={<Ink.Text color={colors.yellow}>Generating config…</Ink.Text>}
16
+ function={run}
17
+ />
18
+ );
14
19
 
15
20
  async function run() {
16
21
  const state = Store.getState();
@@ -10,7 +10,12 @@ import { cli } from "~/core/cli";
10
10
  import { colors } from "~/core/colors";
11
11
 
12
12
  export function Fixup() {
13
- return <Await fallback={null} function={run} />;
13
+ return (
14
+ <Await
15
+ fallback={<Ink.Text color={colors.yellow}>Fixing up commits…</Ink.Text>}
16
+ function={run}
17
+ />
18
+ );
14
19
  }
15
20
 
16
21
  async function run() {
@@ -5,13 +5,19 @@ import * as Ink from "ink-cjs";
5
5
  import { Await } from "~/app/Await";
6
6
  import { Store } from "~/app/Store";
7
7
  import { cli } from "~/core/cli";
8
+ import { colors } from "~/core/colors";
8
9
  import { invariant } from "~/core/invariant";
9
10
 
10
11
  export function Log() {
11
12
  const { stdout } = Ink.useStdout();
12
13
  const available_width = stdout.columns || 80;
13
14
 
14
- return <Await fallback={null} function={() => run({ available_width })} />;
15
+ return (
16
+ <Await
17
+ fallback={<Ink.Text color={colors.yellow}>Generating log…</Ink.Text>}
18
+ function={() => run({ available_width })}
19
+ />
20
+ );
15
21
  }
16
22
 
17
23
  type Args = {
@@ -1,3 +1,5 @@
1
+ /* eslint-disable no-console */
2
+
1
3
  import { Store } from "~/app/Store";
2
4
  import * as git from "~/core/git";
3
5
  import * as github from "~/core/github";
@@ -26,6 +28,8 @@ type CommitRangeGroup = {
26
28
  type CommitGroupMap = { [sha: string]: CommitRangeGroup };
27
29
 
28
30
  export async function range(commit_group_map?: CommitGroupMap) {
31
+ const DEBUG = process.env.DEV && false;
32
+
29
33
  // gather all open prs in repo first
30
34
  // cheaper query to populate cache
31
35
  await github.pr_list();
@@ -163,7 +167,9 @@ export async function range(commit_group_map?: CommitGroupMap) {
163
167
  // console.debug(" ", "group.base", group.base);
164
168
  }
165
169
 
166
- // console.debug({ group });
170
+ if (DEBUG) {
171
+ console.debug({ group });
172
+ }
167
173
 
168
174
  if (!group.pr) {
169
175
  group.dirty = true;
@@ -180,13 +186,17 @@ export async function range(commit_group_map?: CommitGroupMap) {
180
186
  // gh pr diff --color=never 110
181
187
  // git --no-pager diff --color=never 00c8fe0~1..00c8fe0
182
188
  let diff_github = await github.pr_diff(group.pr.number);
183
- diff_github = diff_strip_index_lines(diff_github);
189
+ diff_github = normalize_diff(diff_github);
184
190
 
185
191
  let diff_local = await git.get_diff(group.commits);
186
- diff_local = diff_strip_index_lines(diff_local);
192
+ diff_local = normalize_diff(diff_local);
193
+
194
+ if (DEBUG) {
195
+ console.debug({ diff_local, diff_github });
196
+ }
187
197
 
188
198
  // find the first differing character index
189
- let compare_length = Math.min(diff_github.length, diff_local.length);
199
+ let compare_length = Math.max(diff_github.length, diff_local.length);
190
200
  let diff_index = -1;
191
201
  for (let c_i = 0; c_i < compare_length; c_i++) {
192
202
  if (diff_github[c_i] !== diff_local[c_i]) {
@@ -197,23 +207,25 @@ export async function range(commit_group_map?: CommitGroupMap) {
197
207
  if (diff_index > -1) {
198
208
  group.dirty = true;
199
209
 
200
- // // print preview at diff_index for both strings
201
- // const preview_radius = 30;
202
- // const start_index = Math.max(0, diff_index - preview_radius);
203
- // const end_index = Math.min(compare_length, diff_index + preview_radius);
210
+ if (DEBUG) {
211
+ // print preview at diff_index for both strings
212
+ const preview_radius = 30;
213
+ const start_index = Math.max(0, diff_index - preview_radius);
214
+ const end_index = Math.min(compare_length, diff_index + preview_radius);
204
215
 
205
- // diff_github = diff_github.substring(start_index, end_index);
206
- // diff_github = JSON.stringify(diff_github).slice(1, -1);
216
+ diff_github = diff_github.substring(start_index, end_index);
217
+ diff_github = JSON.stringify(diff_github).slice(1, -1);
207
218
 
208
- // diff_local = diff_local.substring(start_index, end_index);
209
- // diff_local = JSON.stringify(diff_local).slice(1, -1);
219
+ diff_local = diff_local.substring(start_index, end_index);
220
+ diff_local = JSON.stringify(diff_local).slice(1, -1);
210
221
 
211
- // let pointer_indent = " ".repeat(diff_index - start_index + 1);
212
- // console.warn(`⚠️ git diff mismatch`);
213
- // console.warn(` ${pointer_indent}⌄`);
214
- // console.warn(`diff_github …${diff_github}…`);
215
- // console.warn(`diff_local …${diff_local}…`);
216
- // console.warn(` ${pointer_indent}⌃`);
222
+ let pointer_indent = " ".repeat(diff_index - start_index + 1);
223
+ console.warn(`⚠️ git diff mismatch`);
224
+ console.warn(` ${pointer_indent}⌄`);
225
+ console.warn(`diff_github …${diff_github}…`);
226
+ console.warn(`diff_local …${diff_local}…`);
227
+ console.warn(` ${pointer_indent}⌃`);
228
+ }
217
229
  }
218
230
  } else if (!group.master_base && previous_group && previous_group.master_base) {
219
231
  // special case
@@ -280,10 +292,17 @@ export async function range(commit_group_map?: CommitGroupMap) {
280
292
 
281
293
  export const UNASSIGNED = "unassigned";
282
294
 
283
- function diff_strip_index_lines(diff_text: string) {
284
- return diff_text.replace(RE.diff_index_line, "");
295
+ function normalize_diff(diff_text: string) {
296
+ diff_text = diff_text.replace(RE.diff_index_line, "");
297
+ diff_text = diff_text.replace(RE.diff_section_header, "");
298
+ return diff_text;
285
299
  }
286
300
 
287
301
  const RE = {
302
+ // index 8b7c5f7b37688..84124e0a677ca 100644
303
+ // https://regex101.com/r/YBwF6P/1
288
304
  diff_index_line: /^index [0-9a-f]+\.\.[0-9a-f]+.*?\n/gm,
305
+ // @@ -29,6 +29,7 @@ from caas_cli import cli as caas_cli # type: ignore
306
+ // https://regex101.com/r/ohMeDC/1
307
+ diff_section_header: /^@@ .*? @@(?: .*)?\n/gm,
289
308
  };
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import * as Ink from "ink-cjs";
8
8
 
9
9
  import { Brackets } from "~/app/Brackets";
10
+ import { FormatText } from "~/app/FormatText";
10
11
  import { Store } from "~/app/Store";
11
12
  import { Timer } from "~/core/Timer";
12
13
  import { cli } from "~/core/cli";
@@ -38,16 +39,19 @@ export async function pr_list(): Promise<Array<PullRequest>> {
38
39
 
39
40
  if (actions.isDebug()) {
40
41
  actions.output(
41
- <Ink.Text dimColor>
42
- <Ink.Text>{"Github cache "}</Ink.Text>
43
- <Ink.Text bold color={colors.yellow}>
44
- {result_pr_list.length}
45
- </Ink.Text>
46
- <Ink.Text>{" open PRs from "}</Ink.Text>
47
- <Brackets>{repo_path}</Brackets>
48
- <Ink.Text>{" authored by "}</Ink.Text>
49
- <Brackets>{username}</Brackets>
50
- </Ink.Text>,
42
+ <FormatText
43
+ wrapper={<Ink.Text dimColor />}
44
+ message="Github cache {count} open PRs from {repo_path} authored by {username}"
45
+ values={{
46
+ count: (
47
+ <Ink.Text bold color={colors.yellow}>
48
+ {result_pr_list.length}
49
+ </Ink.Text>
50
+ ),
51
+ repo_path: <Brackets>{repo_path}</Brackets>,
52
+ username: <Brackets>{username}</Brackets>,
53
+ }}
54
+ />,
51
55
  );
52
56
  }
53
57
 
@@ -76,15 +80,11 @@ export async function pr_status(branch: string): Promise<null | PullRequest> {
76
80
  if (cache) {
77
81
  if (actions.isDebug()) {
78
82
  actions.debug(
79
- <Ink.Text>
80
- <Ink.Text dimColor>Github pr_status cache</Ink.Text>
81
- <Ink.Text> </Ink.Text>
82
- <Ink.Text bold color={colors.green}>
83
- {"HIT "}
84
- </Ink.Text>
85
- <Ink.Text> </Ink.Text>
86
- <Ink.Text dimColor>{branch}</Ink.Text>
87
- </Ink.Text>,
83
+ cache_message({
84
+ hit: true,
85
+ message: "Github pr_status cache",
86
+ extra: branch,
87
+ }),
88
88
  );
89
89
  }
90
90
 
@@ -93,15 +93,11 @@ export async function pr_status(branch: string): Promise<null | PullRequest> {
93
93
 
94
94
  if (actions.isDebug()) {
95
95
  actions.debug(
96
- <Ink.Text>
97
- <Ink.Text dimColor>Github pr_status cache</Ink.Text>
98
- <Ink.Text> </Ink.Text>
99
- <Ink.Text bold color={colors.red}>
100
- MISS
101
- </Ink.Text>
102
- <Ink.Text> </Ink.Text>
103
- <Ink.Text dimColor>{branch}</Ink.Text>
104
- </Ink.Text>,
96
+ cache_message({
97
+ hit: false,
98
+ message: "Github pr_status cache",
99
+ extra: branch,
100
+ }),
105
101
  );
106
102
  }
107
103
 
@@ -246,14 +242,46 @@ export async function pr_draft(args: DraftPullRequestArgs) {
246
242
  }
247
243
 
248
244
  export async function pr_diff(number: number) {
249
- // https://cli.github.com/manual/gh_pr_diff
245
+ const state = Store.getState();
246
+ const actions = state.actions;
247
+
248
+ const maybe_diff = state.cache_pr_diff[number];
249
+
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
+ }
260
+
261
+ return maybe_diff;
262
+ }
250
263
 
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
+ }
273
+
274
+ // https://cli.github.com/manual/gh_pr_diff
251
275
  const cli_result = await cli(`gh pr diff --color=never ${number}`);
252
276
 
253
277
  if (cli_result.code !== 0) {
254
278
  handle_error(cli_result.output);
255
279
  }
256
280
 
281
+ actions.set((state) => {
282
+ state.cache_pr_diff[number] = cli_result.output;
283
+ });
284
+
257
285
  return cli_result.stdout;
258
286
  }
259
287
 
@@ -321,6 +349,36 @@ function safe_filename(value: string): string {
321
349
  return value.replace(RE.non_alphanumeric_dash, "-");
322
350
  }
323
351
 
352
+ type CacheMessageArgs = {
353
+ hit: boolean;
354
+ message: React.ReactNode;
355
+ extra: React.ReactNode;
356
+ };
357
+
358
+ function cache_message(args: CacheMessageArgs) {
359
+ const status = args.hit ? (
360
+ <Ink.Text bold color={colors.green}>
361
+ HIT
362
+ </Ink.Text>
363
+ ) : (
364
+ <Ink.Text bold color={colors.red}>
365
+ MISS
366
+ </Ink.Text>
367
+ );
368
+
369
+ return (
370
+ <FormatText
371
+ wrapper={<Ink.Text dimColor />}
372
+ message="{message} {status} {extra}"
373
+ values={{
374
+ message: args.message,
375
+ status,
376
+ extra: args.extra,
377
+ }}
378
+ />
379
+ );
380
+ }
381
+
324
382
  type Commit = {
325
383
  authoredDate: string; // "2023-10-22T23:13:35Z"
326
384
  authors: [