git-stack-cli 2.9.3 → 2.9.5

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.5",
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,13 @@ 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 } };
81
+ cache_diff: { [key: string]: string };
82
82
 
83
83
  actions: {
84
84
  exit(code: number, args?: ExitArgs): void;
@@ -88,7 +88,9 @@ export type State = {
88
88
  json(value: pretty_json.JSONValue): void;
89
89
  error(error: unknown): void;
90
90
  output(node: React.ReactNode): void;
91
- debug(node: React.ReactNode, id?: string): void;
91
+ debug(node: React.ReactNode): void;
92
+ debug_pending(id: string, content: string): void;
93
+ debug_pending_end(id: string): void;
92
94
 
93
95
  isDebug(): boolean;
94
96
 
@@ -100,8 +102,6 @@ export type State = {
100
102
 
101
103
  mutate: {
102
104
  output(state: State, args: MutateOutputArgs): void;
103
- pending_output(state: State, args: MutateOutputArgs): void;
104
- end_pending_output(state: State, id: string): void;
105
105
  };
106
106
 
107
107
  select: {
@@ -139,7 +139,8 @@ const BaseStore = createStore<State>()(
139
139
  pending_output: {},
140
140
 
141
141
  pr: {},
142
- cache_pr_diff: {},
142
+ cache_gh_cli_by_branch: {},
143
+ cache_diff: {},
143
144
 
144
145
  actions: {
145
146
  exit(code, args) {
@@ -207,24 +208,39 @@ const BaseStore = createStore<State>()(
207
208
 
208
209
  output(node) {
209
210
  set((state) => {
211
+ if (typeof node === "string") {
212
+ node = <Ink.Text>{node}</Ink.Text>;
213
+ }
210
214
  state.mutate.output(state, { node });
211
215
  });
212
216
  },
213
217
 
214
- debug(node, id) {
218
+ debug(node) {
215
219
  if (get().actions.isDebug()) {
216
- const debug = true;
220
+ set((state) => {
221
+ state.mutate.output(state, { node: <DebugOutput node={node} /> });
222
+ });
223
+ }
224
+ },
217
225
 
226
+ debug_pending(id, content) {
227
+ if (get().actions.isDebug()) {
218
228
  set((state) => {
219
- if (id) {
220
- state.mutate.pending_output(state, { id, node, debug });
221
- } else {
222
- state.mutate.output(state, { node, debug });
229
+ if (!state.pending_output[id]) {
230
+ state.pending_output[id] = [];
223
231
  }
232
+
233
+ state.pending_output[id].push(content);
224
234
  });
225
235
  }
226
236
  },
227
237
 
238
+ debug_pending_end(id) {
239
+ set((state) => {
240
+ delete state.pending_output[id];
241
+ });
242
+ },
243
+
228
244
  isDebug() {
229
245
  const state = get();
230
246
  return state.select.debug(state);
@@ -251,38 +267,8 @@ const BaseStore = createStore<State>()(
251
267
 
252
268
  mutate: {
253
269
  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];
270
+ const id = crypto.randomUUID();
271
+ state.output.push([id, args.node]);
286
272
  },
287
273
  },
288
274
 
@@ -294,28 +280,6 @@ const BaseStore = createStore<State>()(
294
280
  })),
295
281
  );
296
282
 
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
283
  function useState<R>(selector: (state: State) => R): R {
320
284
  return useStore(BaseStore, selector);
321
285
  }
@@ -1,7 +1,5 @@
1
1
  import * as React from "react";
2
2
 
3
- import path from "node:path";
4
-
5
3
  import * as Ink from "ink-cjs";
6
4
  import last from "lodash/last";
7
5
 
@@ -10,9 +8,9 @@ import { Store } from "~/app/Store";
10
8
  import * as StackSummaryTable from "~/core/StackSummaryTable";
11
9
  import { cli } from "~/core/cli";
12
10
  import { colors } from "~/core/colors";
11
+ import * as git from "~/core/git";
13
12
  import * as github from "~/core/github";
14
13
  import { invariant } from "~/core/invariant";
15
- import { safe_exists } from "~/core/safe_exists";
16
14
  import { sleep } from "~/core/sleep";
17
15
 
18
16
  import type * as CommitMetadata from "~/core/CommitMetadata";
@@ -28,16 +26,15 @@ async function run() {
28
26
  const branch_name = state.branch_name;
29
27
  const commit_map = state.commit_map;
30
28
  const master_branch = state.master_branch;
31
- const repo_root = state.repo_root;
29
+ const repo_path = state.repo_path;
32
30
  const sync_github = state.sync_github;
33
31
 
34
32
  invariant(branch_name, "branch_name must exist");
35
33
  invariant(commit_map, "commit_map must exist");
36
- invariant(repo_root, "repo_root must exist");
34
+ invariant(repo_path, "repo_path must exist");
37
35
  invariant(sync_github, "sync_github must exist");
38
36
 
39
37
  const commit_range = sync_github.commit_range;
40
- const rebase_group_index = sync_github.rebase_group_index;
41
38
 
42
39
  let DEFAULT_PR_BODY = "";
43
40
  if (state.pr_template_body) {
@@ -46,9 +43,6 @@ async function run() {
46
43
 
47
44
  const push_group_list = get_push_group_list();
48
45
 
49
- // console.debug({ push_group_list });
50
- // throw new Error("STOP");
51
-
52
46
  // for all push targets in push_group_list
53
47
  // things that can be done in parallel are grouped by numbers
54
48
  //
@@ -165,7 +159,7 @@ async function run() {
165
159
  for (const group of push_group_list) {
166
160
  if (group.pr) {
167
161
  delete state.pr[group.pr.headRefName];
168
- delete state.cache_pr_diff[group.pr.number];
162
+ delete state.cache_gh_cli_by_branch[group.pr.headRefName];
169
163
  }
170
164
  }
171
165
  });
@@ -192,24 +186,14 @@ async function run() {
192
186
  }
193
187
 
194
188
  function get_push_group_list() {
195
- // start from HEAD and work backward to rebase_group_index
196
189
  const push_group_list = [];
197
190
 
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
-
191
+ for (let group of commit_range.group_list) {
208
192
  // skip the unassigned commits group
209
193
  if (group.id === commit_range.UNASSIGNED) continue;
210
194
 
211
- // if not --force, skip non-dirty master_base groups
212
- if (group.master_base && !group.dirty && !argv.force) continue;
195
+ // if not --force, skip non-dirty groups
196
+ if (!group.dirty && !argv.force) continue;
213
197
 
214
198
  push_group_list.unshift(group);
215
199
  }
@@ -240,7 +224,7 @@ async function run() {
240
224
  // Unable to sync.
241
225
  // ```
242
226
  //
243
- if (!is_master_base(group)) {
227
+ if (!is_pr_master_base(group)) {
244
228
  await github.pr_edit({
245
229
  branch: group.id,
246
230
  base: master_branch,
@@ -258,7 +242,15 @@ async function run() {
258
242
  invariant(group.base, "group.base must exist");
259
243
 
260
244
  if (group.pr) {
261
- if (!is_master_base(group)) {
245
+ // there are two scenarios where we should restore the base after push
246
+ // 1. if we aren't master base and pr is master base we should fix it
247
+ const base_mismatch = !group.master_base && is_pr_master_base(group);
248
+ // 2. if group pr was not master before the push we set it to master before pushing
249
+ // now we need to restore it back to how it was before the before_push
250
+ const was_modified_before_push = !is_pr_master_base(group);
251
+
252
+ const needs_base_fix = base_mismatch || was_modified_before_push;
253
+ if (needs_base_fix) {
262
254
  // ensure base matches pr in github
263
255
  await github.pr_edit({ branch: group.id, base: group.base });
264
256
  }
@@ -313,47 +305,19 @@ async function run() {
313
305
  }
314
306
  }
315
307
 
316
- function is_master_base(group: CommitMetadataGroup) {
308
+ function is_pr_master_base(group: CommitMetadataGroup) {
317
309
  if (!group.pr) {
318
310
  return false;
319
311
  }
320
312
 
321
- return group.master_base || `origin/${group.pr.baseRefName}` === master_branch;
313
+ return `origin/${group.pr.baseRefName}` === master_branch;
322
314
  }
323
315
 
324
316
  async function push_master_group(group: CommitMetadataGroup) {
325
- invariant(repo_root, "repo_root must exist");
326
-
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);
329
-
330
- // ensure worktree for pushing master groups
331
- if (!(await safe_exists(worktree_path))) {
332
- actions.output(
333
- <Ink.Text color={colors.white}>
334
- Creating <Ink.Text color={colors.yellow}>{repo_rel_worktree_path}</Ink.Text>
335
- </Ink.Text>,
336
- );
337
- actions.output(
338
- <Ink.Text color={colors.gray}>(this may take a moment the first time…)</Ink.Text>,
339
- );
340
- await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
341
- }
317
+ invariant(repo_path, "repo_path must exist");
342
318
 
343
- // ensure worktree is clean + on the right base before applying commits
344
- // - abort any in-progress cherry-pick/rebase
345
- // - drop local changes/untracked files (including ignored) for a truly fresh state
346
- // - reset to the desired base
347
- await cli(`git -C ${worktree_path} cherry-pick --abort`, { ignoreExitCode: true });
348
- await cli(`git -C ${worktree_path} rebase --abort`, { ignoreExitCode: true });
349
- await cli(`git -C ${worktree_path} merge --abort`, { ignoreExitCode: true });
350
- 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} clean -fd`);
353
-
354
- // cherry-pick the group commits onto that base
355
- const cp_commit_list = group.commits.map((c) => c.sha).join(" ");
356
- await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
319
+ const commit_list = group.commits;
320
+ const { worktree_path } = await git.worktree_add({ commit_list });
357
321
 
358
322
  const push_target = `HEAD:refs/heads/${group.id}`;
359
323
  const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
@@ -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.");