git-stack-cli 2.9.1 → 2.9.3

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.3",
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);
package/src/app/App.tsx CHANGED
@@ -5,6 +5,7 @@ import { CherryPickCheck } from "~/app/CherryPickCheck";
5
5
  import { DependencyCheck } from "~/app/DependencyCheck";
6
6
  import { DetectInitialPR } from "~/app/DetectInitialPR";
7
7
  import { DirtyCheck } from "~/app/DirtyCheck";
8
+ import { FetchPullRequests } from "~/app/FetchPullRequests";
8
9
  import { GatherMetadata } from "~/app/GatherMetadata";
9
10
  import { GithubApiError } from "~/app/GithubApiError";
10
11
  import { HandleCtrlCSigint } from "~/app/HandleCtrlCSigint";
@@ -100,9 +101,11 @@ function MaybeMain() {
100
101
  <DependencyCheck>
101
102
  <DirtyCheck>
102
103
  <GatherMetadata>
103
- <LocalCommitStatus>
104
- <Rebase />
105
- </LocalCommitStatus>
104
+ <FetchPullRequests>
105
+ <LocalCommitStatus>
106
+ <Rebase />
107
+ </LocalCommitStatus>
108
+ </FetchPullRequests>
106
109
  </GatherMetadata>
107
110
  </DirtyCheck>
108
111
  </DependencyCheck>
@@ -117,11 +120,13 @@ function MaybeMain() {
117
120
  <DirtyCheck>
118
121
  <GatherMetadata>
119
122
  <RequireBranch>
120
- <LocalCommitStatus>
121
- <DetectInitialPR>
122
- <Main />
123
- </DetectInitialPR>
124
- </LocalCommitStatus>
123
+ <FetchPullRequests>
124
+ <LocalCommitStatus>
125
+ <DetectInitialPR>
126
+ <Main />
127
+ </DetectInitialPR>
128
+ </LocalCommitStatus>
129
+ </FetchPullRequests>
125
130
  </RequireBranch>
126
131
  </GatherMetadata>
127
132
  </DirtyCheck>
@@ -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;
@@ -0,0 +1,40 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { Await } from "~/app/Await";
6
+ import { Store } from "~/app/Store";
7
+ import { colors } from "~/core/colors";
8
+ import * as github from "~/core/github";
9
+
10
+ type Props = {
11
+ children: React.ReactNode;
12
+ };
13
+
14
+ export function FetchPullRequests(props: Props) {
15
+ const fallback = <Ink.Text color={colors.yellow}>Fetching pull requests…</Ink.Text>;
16
+
17
+ return (
18
+ <Await fallback={fallback} function={run}>
19
+ {props.children}
20
+ </Await>
21
+ );
22
+ }
23
+
24
+ async function run() {
25
+ const actions = Store.getState().actions;
26
+
27
+ try {
28
+ // gather all open prs in repo at once
29
+ // cheaper query to populate cache
30
+ await github.pr_list();
31
+ } catch (err) {
32
+ actions.error("Unable to fetch pull requests.");
33
+
34
+ if (err instanceof Error) {
35
+ actions.error(err.message);
36
+ }
37
+
38
+ actions.exit(24);
39
+ }
40
+ }
@@ -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) {
@@ -177,7 +179,7 @@ const BaseStore = createStore<State>()(
177
179
  json(value) {
178
180
  set((state) => {
179
181
  const node = pretty_json(value);
180
- state.mutate.output(state, { node });
182
+ state.actions.debug(node);
181
183
  });
182
184
  },
183
185
 
@@ -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;
@@ -13,6 +13,7 @@ import { colors } from "~/core/colors";
13
13
  import * as github from "~/core/github";
14
14
  import { invariant } from "~/core/invariant";
15
15
  import { safe_exists } from "~/core/safe_exists";
16
+ import { sleep } from "~/core/sleep";
16
17
 
17
18
  import type * as CommitMetadata from "~/core/CommitMetadata";
18
19
 
@@ -159,6 +160,25 @@ async function run() {
159
160
 
160
161
  actions.unregister_abort_handler();
161
162
 
163
+ // invalidate cache for PRs we pushed
164
+ actions.set((state) => {
165
+ for (const group of push_group_list) {
166
+ if (group.pr) {
167
+ delete state.pr[group.pr.headRefName];
168
+ delete state.cache_pr_diff[group.pr.number];
169
+ }
170
+ }
171
+ });
172
+
173
+ // wait a bit for github to settle after push / edits above
174
+ // we github.pr_list returns outdated information if called too quickly
175
+ await sleep(400);
176
+
177
+ // gather all open prs in repo at once
178
+ // cheaper query to populate cache
179
+ await github.pr_list();
180
+
181
+ // move to next step
162
182
  actions.set((state) => {
163
183
  state.step = "post-rebase-status";
164
184
  });
@@ -185,9 +205,13 @@ async function run() {
185
205
 
186
206
  const group = commit_range.group_list[index];
187
207
 
188
- if (group.id !== commit_range.UNASSIGNED) {
189
- push_group_list.unshift(group);
190
- }
208
+ // skip the unassigned commits group
209
+ if (group.id === commit_range.UNASSIGNED) continue;
210
+
211
+ // if not --force, skip non-dirty master_base groups
212
+ if (group.master_base && !group.dirty && !argv.force) continue;
213
+
214
+ push_group_list.unshift(group);
191
215
  }
192
216
 
193
217
  return push_group_list;
@@ -300,14 +324,14 @@ async function run() {
300
324
  async function push_master_group(group: CommitMetadataGroup) {
301
325
  invariant(repo_root, "repo_root must exist");
302
326
 
303
- const worktree_path = `.git/git-stack-worktrees/push_master_group`;
304
- const worktree_path_absolute = path.join(repo_root, worktree_path);
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);
305
329
 
306
330
  // ensure worktree for pushing master groups
307
- if (!(await safe_exists(worktree_path_absolute))) {
331
+ if (!(await safe_exists(worktree_path))) {
308
332
  actions.output(
309
333
  <Ink.Text color={colors.white}>
310
- Creating <Ink.Text color={colors.yellow}>{worktree_path}</Ink.Text>
334
+ Creating <Ink.Text color={colors.yellow}>{repo_rel_worktree_path}</Ink.Text>
311
335
  </Ink.Text>,
312
336
  );
313
337
  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,11 +28,11 @@ type CommitRangeGroup = {
26
28
  type CommitGroupMap = { [sha: string]: CommitRangeGroup };
27
29
 
28
30
  export async function range(commit_group_map?: CommitGroupMap) {
29
- // gather all open prs in repo first
30
- // cheaper query to populate cache
31
- await github.pr_list();
31
+ const DEBUG = process.env.DEV && false;
32
32
 
33
- const master_branch = Store.getState().master_branch;
33
+ const state = Store.getState();
34
+ const actions = state.actions;
35
+ const master_branch = state.master_branch;
34
36
  const master_branch_name = master_branch.replace(/^origin\//, "");
35
37
  const commit_list = await git.get_commits(`${master_branch}..HEAD`);
36
38
 
@@ -163,16 +165,16 @@ export async function range(commit_group_map?: CommitGroupMap) {
163
165
  // console.debug(" ", "group.base", group.base);
164
166
  }
165
167
 
166
- // console.debug({ group });
168
+ actions.json({ group });
167
169
 
168
170
  if (!group.pr) {
169
171
  group.dirty = true;
170
172
  } else {
171
173
  if (group.pr.baseRefName !== group.base) {
172
- // console.debug("PR_BASEREF_MISMATCH");
174
+ actions.debug("PR_BASEREF_MISMATCH");
173
175
  group.dirty = true;
174
176
  } else if (group.master_base) {
175
- // console.debug("MASTER_BASE_DIFF_COMPARE");
177
+ actions.debug("MASTER_BASE_DIFF_COMPARE");
176
178
 
177
179
  // special case
178
180
  // master_base groups cannot be compared by commit sha
@@ -180,13 +182,15 @@ export async function range(commit_group_map?: CommitGroupMap) {
180
182
  // gh pr diff --color=never 110
181
183
  // git --no-pager diff --color=never 00c8fe0~1..00c8fe0
182
184
  let diff_github = await github.pr_diff(group.pr.number);
183
- diff_github = diff_strip_index_lines(diff_github);
185
+ diff_github = normalize_diff(diff_github);
184
186
 
185
187
  let diff_local = await git.get_diff(group.commits);
186
- diff_local = diff_strip_index_lines(diff_local);
188
+ diff_local = normalize_diff(diff_local);
189
+
190
+ actions.json({ diff_local, diff_github });
187
191
 
188
192
  // find the first differing character index
189
- let compare_length = Math.min(diff_github.length, diff_local.length);
193
+ let compare_length = Math.max(diff_github.length, diff_local.length);
190
194
  let diff_index = -1;
191
195
  for (let c_i = 0; c_i < compare_length; c_i++) {
192
196
  if (diff_github[c_i] !== diff_local[c_i]) {
@@ -197,23 +201,25 @@ export async function range(commit_group_map?: CommitGroupMap) {
197
201
  if (diff_index > -1) {
198
202
  group.dirty = true;
199
203
 
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);
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);
204
209
 
205
- // diff_github = diff_github.substring(start_index, end_index);
206
- // diff_github = JSON.stringify(diff_github).slice(1, -1);
210
+ diff_github = diff_github.substring(start_index, end_index);
211
+ diff_github = JSON.stringify(diff_github).slice(1, -1);
207
212
 
208
- // diff_local = diff_local.substring(start_index, end_index);
209
- // diff_local = JSON.stringify(diff_local).slice(1, -1);
213
+ diff_local = diff_local.substring(start_index, end_index);
214
+ diff_local = JSON.stringify(diff_local).slice(1, -1);
210
215
 
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}⌃`);
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}⌃`);
222
+ }
217
223
  }
218
224
  } else if (!group.master_base && previous_group && previous_group.master_base) {
219
225
  // special case
@@ -233,10 +239,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
233
239
 
234
240
  // compare all commits against pr commits
235
241
  if (group.pr.commits.length !== all_commits.length) {
236
- // console.debug("BOUNDARY_COMMIT_LENGTH_MISMATCH");
242
+ actions.debug("BOUNDARY_COMMIT_LENGTH_MISMATCH");
237
243
  group.dirty = true;
238
244
  } else {
239
- // console.debug("BOUNDARY_COMMIT_SHA_COMPARISON");
245
+ actions.debug("BOUNDARY_COMMIT_SHA_COMPARISON");
240
246
  for (let i = 0; i < group.pr.commits.length; i++) {
241
247
  const pr_commit = group.pr.commits[i];
242
248
  const local_commit = all_commits[i];
@@ -247,10 +253,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
247
253
  }
248
254
  }
249
255
  } else if (group.pr.commits.length !== group.commits.length) {
250
- // console.debug("COMMIT_LENGTH_MISMATCH");
256
+ actions.debug("COMMIT_LENGTH_MISMATCH");
251
257
  group.dirty = true;
252
258
  } else {
253
- // console.debug("COMMIT_SHA_COMPARISON");
259
+ actions.debug("COMMIT_SHA_COMPARISON");
254
260
  // if we still haven't marked this dirty, check each commit
255
261
  // comapre literal commit shas in group
256
262
  for (let i = 0; i < group.pr.commits.length; i++) {
@@ -258,6 +264,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
258
264
  const local_commit = group.commits[i];
259
265
 
260
266
  if (pr_commit.oid !== local_commit.sha) {
267
+ actions.json({ pr_commit, local_commit });
261
268
  group.dirty = true;
262
269
  }
263
270
  }
@@ -280,10 +287,17 @@ export async function range(commit_group_map?: CommitGroupMap) {
280
287
 
281
288
  export const UNASSIGNED = "unassigned";
282
289
 
283
- function diff_strip_index_lines(diff_text: string) {
284
- return diff_text.replace(RE.diff_index_line, "");
290
+ function normalize_diff(diff_text: string) {
291
+ diff_text = diff_text.replace(RE.diff_index_line, "");
292
+ diff_text = diff_text.replace(RE.diff_section_header, "");
293
+ return diff_text;
285
294
  }
286
295
 
287
296
  const RE = {
297
+ // index 8b7c5f7b37688..84124e0a677ca 100644
298
+ // https://regex101.com/r/YBwF6P/1
288
299
  diff_index_line: /^index [0-9a-f]+\.\.[0-9a-f]+.*?\n/gm,
300
+ // @@ -29,6 +29,7 @@ from caas_cli import cli as caas_cli # type: ignore
301
+ // https://regex101.com/r/ohMeDC/1
302
+ diff_section_header: /^@@ .*? @@(?: .*)?\n/gm,
289
303
  };