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/dist/js/index.js +89 -143
- package/package.json +1 -1
- package/scripts/bun-build.ts +5 -1
- package/scripts/core/get_define.ts +1 -1
- package/src/app/App.tsx +0 -2
- package/src/app/AutoUpdate.tsx +6 -0
- package/src/app/Await.tsx +4 -11
- package/src/app/Exit.tsx +14 -0
- package/src/app/GithubApiError.tsx +6 -1
- package/src/app/LocalCommitStatus.tsx +4 -25
- package/src/app/ManualRebase.tsx +1 -5
- package/src/app/PostRebaseStatus.tsx +7 -4
- package/src/app/PreManualRebase.tsx +6 -1
- package/src/app/Status.tsx +6 -1
- package/src/app/Store.tsx +3 -7
- package/src/app/SyncGithub.tsx +56 -32
- package/src/command.ts +5 -15
- package/src/commands/Config.tsx +6 -1
- package/src/commands/Fixup.tsx +6 -1
- package/src/commands/Log.tsx +7 -1
- package/src/commands/Rebase.tsx +1 -1
- package/src/core/CommitMetadata.ts +71 -4
- package/src/core/github.tsx +87 -29
- package/src/__fixtures__/metadata.ts +0 -666
- package/src/app/Debug.tsx +0 -51
package/package.json
CHANGED
package/scripts/bun-build.ts
CHANGED
|
@@ -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: "
|
|
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>
|
package/src/app/AutoUpdate.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
package/src/app/ManualRebase.tsx
CHANGED
|
@@ -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 -
|
|
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
|
|
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
|
|
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() {
|
package/src/app/Status.tsx
CHANGED
|
@@ -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
|
|
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;
|
package/src/app/SyncGithub.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
//
|
|
310
|
-
await
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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) =>
|
|
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 = {
|
package/src/commands/Config.tsx
CHANGED
|
@@ -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
|
|
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();
|
package/src/commands/Fixup.tsx
CHANGED
|
@@ -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
|
|
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() {
|
package/src/commands/Log.tsx
CHANGED
|
@@ -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
|
|
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 = {
|
package/src/commands/Rebase.tsx
CHANGED
|
@@ -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 -
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
};
|