git-stack-cli 2.9.0 → 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.0",
3
+ "version": "2.9.2",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -48,11 +48,15 @@ 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",
54
58
  target: "node",
55
- env: "inline",
59
+ env: "disable",
56
60
  format: "esm",
57
61
  sourcemap: "inline",
58
62
  define,
@@ -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
@@ -2,7 +2,6 @@ import * as React from "react";
2
2
 
3
3
  import { AutoUpdate } from "~/app/AutoUpdate";
4
4
  import { CherryPickCheck } from "~/app/CherryPickCheck";
5
- import { Debug } from "~/app/Debug";
6
5
  import { DependencyCheck } from "~/app/DependencyCheck";
7
6
  import { DetectInitialPR } from "~/app/DetectInitialPR";
8
7
  import { DirtyCheck } from "~/app/DirtyCheck";
@@ -51,7 +50,6 @@ export function App() {
51
50
  return (
52
51
  <Providers>
53
52
  <ErrorBoundary>
54
- <Debug />
55
53
  <Output />
56
54
 
57
55
  <ExitingGate>
@@ -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;
package/src/app/Await.tsx CHANGED
@@ -5,20 +5,13 @@ import { invariant } from "~/core/invariant";
5
5
 
6
6
  type Cache = ReturnType<typeof cache>;
7
7
 
8
- type BaseProps = {
8
+ type Props = {
9
9
  function: Parameters<typeof cache>[0];
10
- };
11
-
12
- type WithChildrenProps = BaseProps & {
13
- fallback: React.SuspenseProps["fallback"];
14
- children: React.ReactNode;
10
+ fallback?: React.SuspenseProps["fallback"];
11
+ children?: React.ReactNode;
15
12
  delayFallbackMs?: number;
16
13
  };
17
14
 
18
- type WithoutChildrenProps = BaseProps;
19
-
20
- type Props = WithChildrenProps | WithoutChildrenProps;
21
-
22
15
  export function Await(props: Props) {
23
16
  const [display_fallback, set_display_fallback] = React.useState(false);
24
17
 
@@ -68,7 +61,7 @@ export function Await(props: Props) {
68
61
  );
69
62
  }
70
63
 
71
- return <ReadCache cache={cacheRef.current} />;
64
+ return <ReadCache cache={cacheRef.current}>{props.children}</ReadCache>;
72
65
  }
73
66
 
74
67
  type ReadCacheProps = {
package/src/app/Exit.tsx CHANGED
@@ -1,11 +1,17 @@
1
1
  import * as React from "react";
2
2
 
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+
3
6
  import * as Ink from "ink-cjs";
4
7
 
5
8
  import { FormatText } from "~/app/FormatText";
6
9
  import { Store } from "~/app/Store";
7
10
  import { cli } from "~/core/cli";
8
11
  import { colors } from "~/core/colors";
12
+ import { get_tmp_dir } from "~/core/get_tmp_dir";
13
+ import * as json from "~/core/json";
14
+ import { pretty_json } from "~/core/pretty_json";
9
15
  import { sleep } from "~/core/sleep";
10
16
 
11
17
  type Props = {
@@ -29,6 +35,14 @@ Exit.handle_exit = async function handle_exit(props: Props) {
29
35
  const state = Store.getState();
30
36
  const actions = state.actions;
31
37
 
38
+ // write state to file for debugging
39
+ if (state.select.debug(state)) {
40
+ const tmp_state_path = path.join(await get_tmp_dir(), "git-stack-state.json");
41
+ await fs.writeFile(tmp_state_path, pretty_json(json.serialize(state)));
42
+ const output = <Ink.Text color={colors.gray}>Wrote state to {tmp_state_path}</Ink.Text>;
43
+ actions.output(output);
44
+ }
45
+
32
46
  actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
33
47
 
34
48
  let exit_code = props.code;
@@ -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,43 +6,22 @@ import { Await } from "~/app/Await";
6
6
  import { Store } from "~/app/Store";
7
7
  import * as CommitMetadata from "~/core/CommitMetadata";
8
8
  import { colors } from "~/core/colors";
9
- import * as json from "~/core/json";
10
9
 
11
10
  type Props = {
12
11
  children: React.ReactNode;
13
12
  };
14
13
 
15
14
  export function LocalCommitStatus(props: Props) {
16
- const argv = Store.useState((state) => state.argv);
17
-
18
- const fallback = <Ink.Text color={colors.yellow}>Fetching PR status from Github…</Ink.Text>;
19
-
20
- if (argv["mock-metadata"]) {
21
- return (
22
- <Await fallback={fallback} function={mock_metadata}>
23
- {props.children}
24
- </Await>
25
- );
26
- }
27
-
28
15
  return (
29
- <Await fallback={fallback} function={run}>
16
+ <Await
17
+ fallback={<Ink.Text color={colors.yellow}>Fetching PR status from Github…</Ink.Text>}
18
+ function={run}
19
+ >
30
20
  {props.children}
31
21
  </Await>
32
22
  );
33
23
  }
34
24
 
35
- async function mock_metadata() {
36
- const module = await import("../__fixtures__/metadata");
37
-
38
- const deserialized = json.deserialize(module.METADATA);
39
-
40
- Store.setState((state) => {
41
- Object.assign(state, deserialized);
42
- state.step = "status";
43
- });
44
- }
45
-
46
25
  async function run() {
47
26
  const actions = Store.getState().actions;
48
27
 
@@ -129,10 +129,6 @@ async function run() {
129
129
  }
130
130
 
131
131
  actions.error("Unable to rebase.");
132
- if (!argv.verbose) {
133
- actions.error("Try again with `--verbose` to see more information.");
134
- }
135
-
136
132
  actions.exit(16);
137
133
  }
138
134
 
@@ -146,7 +142,7 @@ async function run() {
146
142
  // always hard reset and clean to allow subsequent checkout
147
143
  // if there are files checkout will fail and cascade fail subsequent commands
148
144
  cli.sync(`git reset --hard`, spawn_options);
149
- cli.sync(`git clean -df`, spawn_options);
145
+ cli.sync(`git clean -fd`, spawn_options);
150
146
 
151
147
  // always put self back in original branch
152
148
  cli.sync(`git checkout ${branch_name}`, spawn_options);
@@ -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;
@@ -1,5 +1,7 @@
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
  import last from "lodash/last";
5
7
 
@@ -10,6 +12,7 @@ import { cli } from "~/core/cli";
10
12
  import { colors } from "~/core/colors";
11
13
  import * as github from "~/core/github";
12
14
  import { invariant } from "~/core/invariant";
15
+ import { safe_exists } from "~/core/safe_exists";
13
16
 
14
17
  import type * as CommitMetadata from "~/core/CommitMetadata";
15
18
 
@@ -81,9 +84,7 @@ async function run() {
81
84
  invariant(last_commit, "last_commit must exist");
82
85
 
83
86
  // push group in isolation if master_base is set
84
- // for the first group (i > 0) we can skip this
85
- // since it'll be based off master anyway
86
- if (group.master_base && i > 0) {
87
+ if (group.master_base) {
87
88
  await push_master_group(group);
88
89
  continue;
89
90
  }
@@ -159,6 +160,15 @@ async function run() {
159
160
  actions.unregister_abort_handler();
160
161
 
161
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
162
172
  state.step = "post-rebase-status";
163
173
  });
164
174
  } catch (err) {
@@ -167,10 +177,6 @@ async function run() {
167
177
  }
168
178
 
169
179
  actions.error("Unable to sync.");
170
- if (!argv.verbose) {
171
- actions.error("Try again with `--verbose` to see more information.");
172
- }
173
-
174
180
  actions.exit(15);
175
181
  }
176
182
 
@@ -188,9 +194,13 @@ async function run() {
188
194
 
189
195
  const group = commit_range.group_list[index];
190
196
 
191
- if (group.id !== commit_range.UNASSIGNED) {
192
- push_group_list.unshift(group);
193
- }
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);
194
204
  }
195
205
 
196
206
  return push_group_list;
@@ -301,29 +311,43 @@ async function run() {
301
311
  }
302
312
 
303
313
  async function push_master_group(group: CommitMetadataGroup) {
304
- const worktree_path = `.git/git-stack-worktrees/push_master_group`;
305
-
306
- // ensure previous instance of worktree is removed
307
- await cli(`git worktree remove --force ${worktree_path}`, { ignoreExitCode: true });
308
-
309
- // create temp worktree at master (or group.base if you prefer)
310
- await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
311
-
312
- try {
313
- // cherry-pick the group commits onto that base
314
- const cp_commit_list = group.commits.map((c) => c.sha);
315
- await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
316
-
317
- `git -C ${worktree_path} push -f origin HEAD:refs/heads/${group.id}`;
318
-
319
- const push_target = `HEAD:refs/heads/${group.id}`;
320
- const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
321
-
322
- await cli(git_push_command);
323
- } finally {
324
- // clean up even if push fails
325
- await cli(`git worktree remove --force ${worktree_path}`);
314
+ invariant(repo_root, "repo_root must exist");
315
+
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);
318
+
319
+ // ensure worktree for pushing master groups
320
+ if (!(await safe_exists(worktree_path))) {
321
+ actions.output(
322
+ <Ink.Text color={colors.white}>
323
+ Creating <Ink.Text color={colors.yellow}>{repo_rel_worktree_path}</Ink.Text>
324
+ </Ink.Text>,
325
+ );
326
+ actions.output(
327
+ <Ink.Text color={colors.gray}>(this may take a moment the first time…)</Ink.Text>,
328
+ );
329
+ await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
326
330
  }
331
+
332
+ // ensure worktree is clean + on the right base before applying commits
333
+ // - abort any in-progress cherry-pick/rebase
334
+ // - drop local changes/untracked files (including ignored) for a truly fresh state
335
+ // - reset to the desired base
336
+ await cli(`git -C ${worktree_path} cherry-pick --abort`, { ignoreExitCode: true });
337
+ await cli(`git -C ${worktree_path} rebase --abort`, { ignoreExitCode: true });
338
+ await cli(`git -C ${worktree_path} merge --abort`, { ignoreExitCode: true });
339
+ await cli(`git -C ${worktree_path} checkout -f ${master_branch}`);
340
+ await cli(`git -C ${worktree_path} reset --hard ${master_branch}`);
341
+ await cli(`git -C ${worktree_path} clean -fd`);
342
+
343
+ // cherry-pick the group commits onto that base
344
+ const cp_commit_list = group.commits.map((c) => c.sha).join(" ");
345
+ await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
346
+
347
+ const push_target = `HEAD:refs/heads/${group.id}`;
348
+ const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
349
+
350
+ await cli(git_push_command);
327
351
  }
328
352
  }
329
353
 
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(
@@ -160,20 +164,6 @@ const DefaultOptions = {
160
164
  "Disable with --no-template",
161
165
  ].join("\n"),
162
166
  },
163
-
164
- "write-state-json": {
165
- hidden: true,
166
- type: "boolean",
167
- default: false,
168
- description: "Write state to local json file for debugging",
169
- },
170
-
171
- "mock-metadata": {
172
- hidden: true,
173
- type: "boolean",
174
- default: false,
175
- description: "Mock local store metadata for testing",
176
- },
177
167
  } satisfies YargsOptions;
178
168
 
179
169
  const FixupOptions = {
@@ -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 = {
@@ -184,7 +184,7 @@ Rebase.run = async function run(props: Props) {
184
184
  // always hard reset and clean to allow subsequent checkout
185
185
  // if there are files checkout will fail and cascade fail subsequent commands
186
186
  cli.sync(`git reset --hard`, spawn_options);
187
- cli.sync(`git clean -df`, spawn_options);
187
+ cli.sync(`git clean -fd`, spawn_options);
188
188
 
189
189
  // always put self back in original branch
190
190
  cli.sync(`git checkout ${branch_name}`, spawn_options);
@@ -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,21 +167,65 @@ export async function range(commit_group_map?: CommitGroupMap) {
163
167
  // console.debug(" ", "group.base", group.base);
164
168
  }
165
169
 
170
+ if (DEBUG) {
171
+ console.debug({ group });
172
+ }
173
+
166
174
  if (!group.pr) {
167
175
  group.dirty = true;
168
176
  } else {
169
177
  if (group.pr.baseRefName !== group.base) {
178
+ // console.debug("PR_BASEREF_MISMATCH");
170
179
  group.dirty = true;
171
- } else if (group.master_base && i > 0) {
180
+ } else if (group.master_base) {
181
+ // console.debug("MASTER_BASE_DIFF_COMPARE");
182
+
172
183
  // special case
173
184
  // master_base groups cannot be compared by commit sha
174
185
  // instead compare the literal diff local against origin
175
186
  // gh pr diff --color=never 110
176
187
  // git --no-pager diff --color=never 00c8fe0~1..00c8fe0
177
- const diff_github = await github.pr_diff(group.pr.number);
178
- const diff_local = await git.get_diff(group.commits);
179
- if (diff_github !== diff_local) {
188
+ let diff_github = await github.pr_diff(group.pr.number);
189
+ diff_github = normalize_diff(diff_github);
190
+
191
+ let diff_local = await git.get_diff(group.commits);
192
+ diff_local = normalize_diff(diff_local);
193
+
194
+ if (DEBUG) {
195
+ console.debug({ diff_local, diff_github });
196
+ }
197
+
198
+ // find the first differing character index
199
+ let compare_length = Math.max(diff_github.length, diff_local.length);
200
+ let diff_index = -1;
201
+ for (let c_i = 0; c_i < compare_length; c_i++) {
202
+ if (diff_github[c_i] !== diff_local[c_i]) {
203
+ diff_index = c_i;
204
+ break;
205
+ }
206
+ }
207
+ if (diff_index > -1) {
180
208
  group.dirty = true;
209
+
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);
215
+
216
+ diff_github = diff_github.substring(start_index, end_index);
217
+ diff_github = JSON.stringify(diff_github).slice(1, -1);
218
+
219
+ diff_local = diff_local.substring(start_index, end_index);
220
+ diff_local = JSON.stringify(diff_local).slice(1, -1);
221
+
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
+ }
181
229
  }
182
230
  } else if (!group.master_base && previous_group && previous_group.master_base) {
183
231
  // special case
@@ -197,8 +245,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
197
245
 
198
246
  // compare all commits against pr commits
199
247
  if (group.pr.commits.length !== all_commits.length) {
248
+ // console.debug("BOUNDARY_COMMIT_LENGTH_MISMATCH");
200
249
  group.dirty = true;
201
250
  } else {
251
+ // console.debug("BOUNDARY_COMMIT_SHA_COMPARISON");
202
252
  for (let i = 0; i < group.pr.commits.length; i++) {
203
253
  const pr_commit = group.pr.commits[i];
204
254
  const local_commit = all_commits[i];
@@ -209,8 +259,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
209
259
  }
210
260
  }
211
261
  } else if (group.pr.commits.length !== group.commits.length) {
262
+ // console.debug("COMMIT_LENGTH_MISMATCH");
212
263
  group.dirty = true;
213
264
  } else {
265
+ // console.debug("COMMIT_SHA_COMPARISON");
214
266
  // if we still haven't marked this dirty, check each commit
215
267
  // comapre literal commit shas in group
216
268
  for (let i = 0; i < group.pr.commits.length; i++) {
@@ -239,3 +291,18 @@ export async function range(commit_group_map?: CommitGroupMap) {
239
291
  }
240
292
 
241
293
  export const UNASSIGNED = "unassigned";
294
+
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;
299
+ }
300
+
301
+ const RE = {
302
+ // index 8b7c5f7b37688..84124e0a677ca 100644
303
+ // https://regex101.com/r/YBwF6P/1
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,
308
+ };