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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "2.9.3",
3
+ "version": "2.9.4",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+ import { DateTime } from "luxon";
5
+
6
+ type Props = {
7
+ node: React.ReactNode;
8
+ };
9
+
10
+ export function DebugOutput(props: Props) {
11
+ const { stdout } = Ink.useStdout();
12
+ const available_width = stdout.columns;
13
+
14
+ const timestamp = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss.SSS");
15
+ const content_width = available_width - timestamp.length - 2;
16
+
17
+ const content = (function () {
18
+ switch (typeof props.node) {
19
+ case "boolean":
20
+ case "number":
21
+ case "string": {
22
+ return <Ink.Text dimColor>{String(props.node)}</Ink.Text>;
23
+ }
24
+ default:
25
+ return props.node;
26
+ }
27
+ })();
28
+
29
+ return (
30
+ <Ink.Box flexDirection="column">
31
+ <Ink.Box flexDirection="row" gap={1} width={available_width}>
32
+ <Ink.Box width={timestamp.length} flexDirection="column">
33
+ <Ink.Text dimColor>{timestamp}</Ink.Text>
34
+ </Ink.Box>
35
+
36
+ <Ink.Box width={content_width}>{content}</Ink.Box>
37
+ </Ink.Box>
38
+ </Ink.Box>
39
+ );
40
+ }
@@ -4,6 +4,7 @@ import * as Ink from "ink-cjs";
4
4
 
5
5
  import { Await } from "~/app/Await";
6
6
  import { Brackets } from "~/app/Brackets";
7
+ import { FormatText } from "~/app/FormatText";
7
8
  import { Parens } from "~/app/Parens";
8
9
  import { Store } from "~/app/Store";
9
10
  import { cli } from "~/core/cli";
@@ -51,34 +52,36 @@ async function run(props: Props) {
51
52
  }
52
53
 
53
54
  actions.output(
54
- <Ink.Text>
55
- <Ink.Text>{"Github "}</Ink.Text>
56
-
57
- <Brackets>graphql</Brackets>
58
-
59
- <Ink.Text>{" API rate limit "}</Ink.Text>
60
-
61
- <Brackets>
62
- <Ink.Text>{used}</Ink.Text>
63
- <Ink.Text>/</Ink.Text>
64
- <Ink.Text>{limit}</Ink.Text>
65
- </Brackets>
66
-
67
- <Ink.Text>{" will reset at "}</Ink.Text>
68
-
69
- <Ink.Text bold color={colors.yellow}>
70
- {reset_time}
71
- </Ink.Text>
72
-
73
- <Ink.Text> </Ink.Text>
74
-
75
- <Parens>
76
- <Ink.Text>{"in "}</Ink.Text>
77
- <Ink.Text bold color={colors.yellow}>
78
- {time_until}
79
- </Ink.Text>
80
- </Parens>
81
- </Ink.Text>,
55
+ <FormatText
56
+ message="Github {graphql} API rate limit {ratio} will reset at {reset_time} {time_until}"
57
+ values={{
58
+ graphql: <Brackets>graphql</Brackets>,
59
+ ratio: (
60
+ <Brackets>
61
+ <FormatText message="{used}/{limit}" values={{ used, limit }} />
62
+ </Brackets>
63
+ ),
64
+ reset_time: (
65
+ <Ink.Text bold color={colors.yellow}>
66
+ {reset_time}
67
+ </Ink.Text>
68
+ ),
69
+ time_until: (
70
+ <Parens>
71
+ <FormatText
72
+ message="in {time_until}"
73
+ values={{
74
+ time_until: (
75
+ <Ink.Text bold color={colors.yellow}>
76
+ {time_until}
77
+ </Ink.Text>
78
+ ),
79
+ }}
80
+ />
81
+ </Parens>
82
+ ),
83
+ }}
84
+ />,
82
85
  );
83
86
 
84
87
  if (props.exit) {
@@ -47,6 +47,11 @@ async function run() {
47
47
  // get latest merge_base relative to local master
48
48
  const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
49
49
 
50
+ // ensure merge_base is updated
51
+ actions.set((state) => {
52
+ state.merge_base = merge_base;
53
+ });
54
+
50
55
  // immediately paint all commit to preserve selected commit ranges
51
56
  let commit_range = await CommitMetadata.range(commit_map);
52
57
 
@@ -116,7 +121,7 @@ async function run() {
116
121
  if (argv.sync) {
117
122
  actions.set((state) => {
118
123
  state.step = "sync-github";
119
- state.sync_github = { commit_range, rebase_group_index };
124
+ state.sync_github = { commit_range };
120
125
  });
121
126
  } else {
122
127
  actions.set((state) => {
@@ -2,35 +2,26 @@ import * as React from "react";
2
2
 
3
3
  import * as Ink from "ink-cjs";
4
4
 
5
+ import { DebugOutput } from "~/app/DebugOutput";
5
6
  import { Store } from "~/app/Store";
6
7
 
7
8
  export function Output() {
8
9
  const output = Store.useState((state) => state.output);
9
10
  const pending_output = Store.useState((state) => state.pending_output);
10
- const pending_output_items = Object.values(pending_output);
11
11
 
12
12
  return (
13
13
  <React.Fragment>
14
14
  <Ink.Static items={output}>
15
- {(node, i) => {
16
- return <Ink.Box key={i}>{node}</Ink.Box>;
15
+ {(entry) => {
16
+ const [id, node] = entry;
17
+ return <Ink.Box key={id}>{node}</Ink.Box>;
17
18
  }}
18
19
  </Ink.Static>
19
20
 
20
- {pending_output_items.map((node_list, i) => {
21
- return (
22
- <Ink.Box key={i}>
23
- <Ink.Text>
24
- {node_list.map((text, j) => {
25
- return (
26
- <React.Fragment key={j}>
27
- <Ink.Text>{text}</Ink.Text>
28
- </React.Fragment>
29
- );
30
- })}
31
- </Ink.Text>
32
- </Ink.Box>
33
- );
21
+ {Object.entries(pending_output).map((entry) => {
22
+ const [id, content_list] = entry;
23
+ const content = content_list.join("");
24
+ return <DebugOutput key={id} node={content} />;
34
25
  })}
35
26
  </React.Fragment>
36
27
  );
package/src/app/Store.tsx CHANGED
@@ -1,11 +1,13 @@
1
1
  import * as React from "react";
2
2
 
3
+ import crypto from "node:crypto";
4
+
3
5
  import * as Ink from "ink-cjs";
4
6
  import { createStore, useStore } from "zustand";
5
7
  import { immer } from "zustand/middleware/immer";
6
8
 
9
+ import { DebugOutput } from "~/app/DebugOutput";
7
10
  import { Exit } from "~/app/Exit";
8
- import { LogTimestamp } from "~/app/LogTimestamp";
9
11
  import { colors } from "~/core/colors";
10
12
  import { pretty_json } from "~/core/pretty_json";
11
13
 
@@ -21,13 +23,10 @@ type CommitMap = Parameters<typeof CommitMetadata.range>[0];
21
23
  type MutateOutputArgs = {
22
24
  node: React.ReactNode;
23
25
  id?: string;
24
- debug?: boolean;
25
- withoutTimestamp?: boolean;
26
26
  };
27
27
 
28
28
  type SyncGithubState = {
29
29
  commit_range: CommitMetadata.CommitRange;
30
- rebase_group_index: number;
31
30
  };
32
31
 
33
32
  // async function that returns exit code
@@ -73,12 +72,12 @@ export type State = {
73
72
  | "sync-github"
74
73
  | "post-rebase-status";
75
74
 
76
- output: Array<React.ReactNode>;
77
- pending_output: Record<string, Array<React.ReactNode>>;
75
+ output: Array<[string, React.ReactNode]>;
76
+ pending_output: Record<string, Array<string>>;
78
77
 
79
78
  // cache
80
79
  pr: { [branch: string]: PullRequest };
81
- cache_pr_diff: { [id: number]: string };
80
+ cache_gh_cli_by_branch: { [branch: string]: { [command: string]: string } };
82
81
 
83
82
  actions: {
84
83
  exit(code: number, args?: ExitArgs): void;
@@ -88,7 +87,9 @@ export type State = {
88
87
  json(value: pretty_json.JSONValue): void;
89
88
  error(error: unknown): void;
90
89
  output(node: React.ReactNode): void;
91
- debug(node: React.ReactNode, id?: string): void;
90
+ debug(node: React.ReactNode): void;
91
+ debug_pending(id: string, content: string): void;
92
+ debug_pending_end(id: string): void;
92
93
 
93
94
  isDebug(): boolean;
94
95
 
@@ -100,8 +101,6 @@ export type State = {
100
101
 
101
102
  mutate: {
102
103
  output(state: State, args: MutateOutputArgs): void;
103
- pending_output(state: State, args: MutateOutputArgs): void;
104
- end_pending_output(state: State, id: string): void;
105
104
  };
106
105
 
107
106
  select: {
@@ -139,7 +138,7 @@ const BaseStore = createStore<State>()(
139
138
  pending_output: {},
140
139
 
141
140
  pr: {},
142
- cache_pr_diff: {},
141
+ cache_gh_cli_by_branch: {},
143
142
 
144
143
  actions: {
145
144
  exit(code, args) {
@@ -211,20 +210,32 @@ const BaseStore = createStore<State>()(
211
210
  });
212
211
  },
213
212
 
214
- debug(node, id) {
213
+ debug(node) {
215
214
  if (get().actions.isDebug()) {
216
- const debug = true;
215
+ set((state) => {
216
+ state.mutate.output(state, { node: <DebugOutput node={node} /> });
217
+ });
218
+ }
219
+ },
217
220
 
221
+ debug_pending(id, content) {
222
+ if (get().actions.isDebug()) {
218
223
  set((state) => {
219
- if (id) {
220
- state.mutate.pending_output(state, { id, node, debug });
221
- } else {
222
- state.mutate.output(state, { node, debug });
224
+ if (!state.pending_output[id]) {
225
+ state.pending_output[id] = [];
223
226
  }
227
+
228
+ state.pending_output[id].push(content);
224
229
  });
225
230
  }
226
231
  },
227
232
 
233
+ debug_pending_end(id) {
234
+ set((state) => {
235
+ delete state.pending_output[id];
236
+ });
237
+ },
238
+
228
239
  isDebug() {
229
240
  const state = get();
230
241
  return state.select.debug(state);
@@ -251,38 +262,8 @@ const BaseStore = createStore<State>()(
251
262
 
252
263
  mutate: {
253
264
  output(state, args) {
254
- const renderOutput = renderOutputArgs(args);
255
- state.output.push(renderOutput);
256
- },
257
-
258
- pending_output(state, args) {
259
- const { id } = args;
260
-
261
- if (!id) {
262
- return;
263
- }
264
-
265
- // set `withoutTimestamp` to skip <LogTimestamp> for all subsequent pending outputs
266
- // we only want to timestamp for the first part (when we initialize the [])
267
- // if we have many incremental outputs on the same line we do not want multiple timestamps
268
- //
269
- // await Promise.all([
270
- // cli(`for i in $(seq 1 5); do echo $i; sleep 1; done`),
271
- // cli(`for i in $(seq 5 1); do printf "$i "; sleep 1; done; echo`),
272
- // ]);
273
- //
274
- let withoutTimestamp = true;
275
- if (!state.pending_output[id]) {
276
- withoutTimestamp = false;
277
- state.pending_output[id] = [];
278
- }
279
-
280
- const renderOutput = renderOutputArgs({ ...args, withoutTimestamp });
281
- state.pending_output[id].push(renderOutput);
282
- },
283
-
284
- end_pending_output(state, id) {
285
- delete state.pending_output[id];
265
+ const id = crypto.randomUUID();
266
+ state.output.push([id, args.node]);
286
267
  },
287
268
  },
288
269
 
@@ -294,28 +275,6 @@ const BaseStore = createStore<State>()(
294
275
  })),
295
276
  );
296
277
 
297
- function renderOutputArgs(args: MutateOutputArgs) {
298
- let output = args.node;
299
-
300
- switch (typeof args.node) {
301
- case "boolean":
302
- case "number":
303
- case "string":
304
- output = <Ink.Text dimColor={args.debug}>{String(args.node)}</Ink.Text>;
305
- }
306
-
307
- if (args.debug) {
308
- return (
309
- <React.Fragment>
310
- {args.withoutTimestamp ? null : <LogTimestamp />}
311
- {output}
312
- </React.Fragment>
313
- );
314
- }
315
-
316
- return output;
317
- }
318
-
319
278
  function useState<R>(selector: (state: State) => R): R {
320
279
  return useStore(BaseStore, selector);
321
280
  }
@@ -26,18 +26,18 @@ async function run() {
26
26
  const actions = state.actions;
27
27
  const argv = state.argv;
28
28
  const branch_name = state.branch_name;
29
+ const merge_base = state.merge_base;
29
30
  const commit_map = state.commit_map;
30
31
  const master_branch = state.master_branch;
31
- const repo_root = state.repo_root;
32
+ const repo_path = state.repo_path;
32
33
  const sync_github = state.sync_github;
33
34
 
34
35
  invariant(branch_name, "branch_name must exist");
35
36
  invariant(commit_map, "commit_map must exist");
36
- invariant(repo_root, "repo_root must exist");
37
+ invariant(repo_path, "repo_path must exist");
37
38
  invariant(sync_github, "sync_github must exist");
38
39
 
39
40
  const commit_range = sync_github.commit_range;
40
- const rebase_group_index = sync_github.rebase_group_index;
41
41
 
42
42
  let DEFAULT_PR_BODY = "";
43
43
  if (state.pr_template_body) {
@@ -46,9 +46,6 @@ async function run() {
46
46
 
47
47
  const push_group_list = get_push_group_list();
48
48
 
49
- // console.debug({ push_group_list });
50
- // throw new Error("STOP");
51
-
52
49
  // for all push targets in push_group_list
53
50
  // things that can be done in parallel are grouped by numbers
54
51
  //
@@ -165,7 +162,7 @@ async function run() {
165
162
  for (const group of push_group_list) {
166
163
  if (group.pr) {
167
164
  delete state.pr[group.pr.headRefName];
168
- delete state.cache_pr_diff[group.pr.number];
165
+ delete state.cache_gh_cli_by_branch[group.pr.headRefName];
169
166
  }
170
167
  }
171
168
  });
@@ -192,24 +189,14 @@ async function run() {
192
189
  }
193
190
 
194
191
  function get_push_group_list() {
195
- // start from HEAD and work backward to rebase_group_index
196
192
  const push_group_list = [];
197
193
 
198
- for (let i = 0; i < commit_range.group_list.length; i++) {
199
- const index = commit_range.group_list.length - 1 - i;
200
-
201
- // do not go past rebase_group_index
202
- if (index < rebase_group_index) {
203
- break;
204
- }
205
-
206
- const group = commit_range.group_list[index];
207
-
194
+ for (let group of commit_range.group_list) {
208
195
  // skip the unassigned commits group
209
196
  if (group.id === commit_range.UNASSIGNED) continue;
210
197
 
211
- // if not --force, skip non-dirty master_base groups
212
- if (group.master_base && !group.dirty && !argv.force) continue;
198
+ // if not --force, skip non-dirty groups
199
+ if (!group.dirty && !argv.force) continue;
213
200
 
214
201
  push_group_list.unshift(group);
215
202
  }
@@ -240,7 +227,7 @@ async function run() {
240
227
  // Unable to sync.
241
228
  // ```
242
229
  //
243
- if (!is_master_base(group)) {
230
+ if (!is_pr_master_base(group)) {
244
231
  await github.pr_edit({
245
232
  branch: group.id,
246
233
  base: master_branch,
@@ -258,7 +245,15 @@ async function run() {
258
245
  invariant(group.base, "group.base must exist");
259
246
 
260
247
  if (group.pr) {
261
- if (!is_master_base(group)) {
248
+ // there are two scenarios where we should restore the base after push
249
+ // 1. if we aren't master base and pr is master base we should fix it
250
+ const base_mismatch = !group.master_base && is_pr_master_base(group);
251
+ // 2. if group pr was not master before the push we set it to master before pushing
252
+ // now we need to restore it back to how it was before the before_push
253
+ const was_modified_before_push = !is_pr_master_base(group);
254
+
255
+ const needs_base_fix = base_mismatch || was_modified_before_push;
256
+ if (needs_base_fix) {
262
257
  // ensure base matches pr in github
263
258
  await github.pr_edit({ branch: group.id, base: group.base });
264
259
  }
@@ -313,25 +308,31 @@ async function run() {
313
308
  }
314
309
  }
315
310
 
316
- function is_master_base(group: CommitMetadataGroup) {
311
+ function is_pr_master_base(group: CommitMetadataGroup) {
317
312
  if (!group.pr) {
318
313
  return false;
319
314
  }
320
315
 
321
- return group.master_base || `origin/${group.pr.baseRefName}` === master_branch;
316
+ return `origin/${group.pr.baseRefName}` === master_branch;
322
317
  }
323
318
 
324
319
  async function push_master_group(group: CommitMetadataGroup) {
325
- invariant(repo_root, "repo_root must exist");
320
+ invariant(repo_path, "repo_path must exist");
326
321
 
327
- const repo_rel_worktree_path = `.git/git-stack-worktrees/push_master_group`;
328
- const worktree_path = path.join(repo_root, repo_rel_worktree_path);
322
+ const worktree_path = path.join(
323
+ process.env.HOME,
324
+ ".cache",
325
+ "git-stack",
326
+ "worktrees",
327
+ repo_path,
328
+ "push_master_group",
329
+ );
329
330
 
330
331
  // ensure worktree for pushing master groups
331
332
  if (!(await safe_exists(worktree_path))) {
332
333
  actions.output(
333
334
  <Ink.Text color={colors.white}>
334
- Creating <Ink.Text color={colors.yellow}>{repo_rel_worktree_path}</Ink.Text>
335
+ Creating <Ink.Text color={colors.yellow}>{worktree_path}</Ink.Text>
335
336
  </Ink.Text>,
336
337
  );
337
338
  actions.output(
@@ -342,13 +343,13 @@ async function run() {
342
343
 
343
344
  // ensure worktree is clean + on the right base before applying commits
344
345
  // - abort any in-progress cherry-pick/rebase
345
- // - drop local changes/untracked files (including ignored) for a truly fresh state
346
+ // - drop local changes/untracked files to fresh state
346
347
  // - reset to the desired base
347
348
  await cli(`git -C ${worktree_path} cherry-pick --abort`, { ignoreExitCode: true });
348
349
  await cli(`git -C ${worktree_path} rebase --abort`, { ignoreExitCode: true });
349
350
  await cli(`git -C ${worktree_path} merge --abort`, { ignoreExitCode: true });
350
351
  await cli(`git -C ${worktree_path} checkout -f ${master_branch}`);
351
- await cli(`git -C ${worktree_path} reset --hard ${master_branch}`);
352
+ await cli(`git -C ${worktree_path} reset --hard ${merge_base}`);
352
353
  await cli(`git -C ${worktree_path} clean -fd`);
353
354
 
354
355
  // cherry-pick the group commits onto that base
@@ -1,11 +1,14 @@
1
1
  import * as React from "react";
2
2
 
3
+ import path from "node:path";
4
+
3
5
  import * as Ink from "ink-cjs";
4
6
 
5
7
  import { Await } from "~/app/Await";
6
8
  import { Store } from "~/app/Store";
7
9
  import { cli } from "~/core/cli";
8
10
  import { colors } from "~/core/colors";
11
+ import { pretty_json } from "~/core/pretty_json";
9
12
 
10
13
  type Props = {
11
14
  children: React.ReactNode;
@@ -29,6 +32,11 @@ async function run() {
29
32
  await cli(`echo USER=$USER`);
30
33
  await cli(`echo GIT_AUTHOR_NAME=$GIT_AUTHOR_NAME`);
31
34
  await cli(`echo GIT_AUTHOR_EMAIL=$GIT_AUTHOR_EMAIL`);
35
+
36
+ const PATH = process.env["PATH"];
37
+ const PATH_LIST = pretty_json(PATH.split(path.delimiter));
38
+ actions.debug(`process.env.PATH ${PATH_LIST}`);
39
+
32
40
  await cli(`git config --list --show-origin`);
33
41
  } catch (err) {
34
42
  actions.error("Unable to log verbose debug information.");