git-stack-cli 2.9.0 → 2.9.1
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 +85 -139
- package/package.json +1 -1
- package/scripts/bun-build.ts +1 -1
- package/src/app/App.tsx +0 -2
- package/src/app/Await.tsx +4 -11
- package/src/app/Exit.tsx +14 -0
- package/src/app/LocalCommitStatus.tsx +4 -25
- package/src/app/ManualRebase.tsx +1 -5
- package/src/app/SyncGithub.tsx +39 -28
- package/src/command.ts +0 -14
- package/src/commands/Rebase.tsx +1 -1
- package/src/core/CommitMetadata.ts +52 -4
- package/src/__fixtures__/metadata.ts +0 -666
- package/src/app/Debug.tsx +0 -51
package/package.json
CHANGED
package/scripts/bun-build.ts
CHANGED
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/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;
|
|
@@ -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);
|
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
|
}
|
|
@@ -167,10 +168,6 @@ async function run() {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
actions.error("Unable to sync.");
|
|
170
|
-
if (!argv.verbose) {
|
|
171
|
-
actions.error("Try again with `--verbose` to see more information.");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
171
|
actions.exit(15);
|
|
175
172
|
}
|
|
176
173
|
|
|
@@ -301,29 +298,43 @@ async function run() {
|
|
|
301
298
|
}
|
|
302
299
|
|
|
303
300
|
async function push_master_group(group: CommitMetadataGroup) {
|
|
304
|
-
|
|
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}`);
|
|
301
|
+
invariant(repo_root, "repo_root must exist");
|
|
311
302
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
await cli(`git worktree
|
|
303
|
+
const worktree_path = `.git/git-stack-worktrees/push_master_group`;
|
|
304
|
+
const worktree_path_absolute = path.join(repo_root, worktree_path);
|
|
305
|
+
|
|
306
|
+
// ensure worktree for pushing master groups
|
|
307
|
+
if (!(await safe_exists(worktree_path_absolute))) {
|
|
308
|
+
actions.output(
|
|
309
|
+
<Ink.Text color={colors.white}>
|
|
310
|
+
Creating <Ink.Text color={colors.yellow}>{worktree_path}</Ink.Text>
|
|
311
|
+
</Ink.Text>,
|
|
312
|
+
);
|
|
313
|
+
actions.output(
|
|
314
|
+
<Ink.Text color={colors.gray}>(this may take a moment the first time…)</Ink.Text>,
|
|
315
|
+
);
|
|
316
|
+
await cli(`git worktree add -f ${worktree_path} ${master_branch}`);
|
|
326
317
|
}
|
|
318
|
+
|
|
319
|
+
// ensure worktree is clean + on the right base before applying commits
|
|
320
|
+
// - abort any in-progress cherry-pick/rebase
|
|
321
|
+
// - drop local changes/untracked files (including ignored) for a truly fresh state
|
|
322
|
+
// - reset to the desired base
|
|
323
|
+
await cli(`git -C ${worktree_path} cherry-pick --abort`, { ignoreExitCode: true });
|
|
324
|
+
await cli(`git -C ${worktree_path} rebase --abort`, { ignoreExitCode: true });
|
|
325
|
+
await cli(`git -C ${worktree_path} merge --abort`, { ignoreExitCode: true });
|
|
326
|
+
await cli(`git -C ${worktree_path} checkout -f ${master_branch}`);
|
|
327
|
+
await cli(`git -C ${worktree_path} reset --hard ${master_branch}`);
|
|
328
|
+
await cli(`git -C ${worktree_path} clean -fd`);
|
|
329
|
+
|
|
330
|
+
// cherry-pick the group commits onto that base
|
|
331
|
+
const cp_commit_list = group.commits.map((c) => c.sha).join(" ");
|
|
332
|
+
await cli(`git -C ${worktree_path} cherry-pick ${cp_commit_list}`);
|
|
333
|
+
|
|
334
|
+
const push_target = `HEAD:refs/heads/${group.id}`;
|
|
335
|
+
const git_push_command = create_git_push_command(`git -C ${worktree_path}`, push_target);
|
|
336
|
+
|
|
337
|
+
await cli(git_push_command);
|
|
327
338
|
}
|
|
328
339
|
}
|
|
329
340
|
|
package/src/command.ts
CHANGED
|
@@ -160,20 +160,6 @@ const DefaultOptions = {
|
|
|
160
160
|
"Disable with --no-template",
|
|
161
161
|
].join("\n"),
|
|
162
162
|
},
|
|
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
163
|
} satisfies YargsOptions;
|
|
178
164
|
|
|
179
165
|
const FixupOptions = {
|
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);
|
|
@@ -163,21 +163,57 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
163
163
|
// console.debug(" ", "group.base", group.base);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// console.debug({ group });
|
|
167
|
+
|
|
166
168
|
if (!group.pr) {
|
|
167
169
|
group.dirty = true;
|
|
168
170
|
} else {
|
|
169
171
|
if (group.pr.baseRefName !== group.base) {
|
|
172
|
+
// console.debug("PR_BASEREF_MISMATCH");
|
|
170
173
|
group.dirty = true;
|
|
171
|
-
} else if (group.master_base
|
|
174
|
+
} else if (group.master_base) {
|
|
175
|
+
// console.debug("MASTER_BASE_DIFF_COMPARE");
|
|
176
|
+
|
|
172
177
|
// special case
|
|
173
178
|
// master_base groups cannot be compared by commit sha
|
|
174
179
|
// instead compare the literal diff local against origin
|
|
175
180
|
// gh pr diff --color=never 110
|
|
176
181
|
// git --no-pager diff --color=never 00c8fe0~1..00c8fe0
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
let diff_github = await github.pr_diff(group.pr.number);
|
|
183
|
+
diff_github = diff_strip_index_lines(diff_github);
|
|
184
|
+
|
|
185
|
+
let diff_local = await git.get_diff(group.commits);
|
|
186
|
+
diff_local = diff_strip_index_lines(diff_local);
|
|
187
|
+
|
|
188
|
+
// find the first differing character index
|
|
189
|
+
let compare_length = Math.min(diff_github.length, diff_local.length);
|
|
190
|
+
let diff_index = -1;
|
|
191
|
+
for (let c_i = 0; c_i < compare_length; c_i++) {
|
|
192
|
+
if (diff_github[c_i] !== diff_local[c_i]) {
|
|
193
|
+
diff_index = c_i;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (diff_index > -1) {
|
|
180
198
|
group.dirty = true;
|
|
199
|
+
|
|
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
|
+
|
|
205
|
+
// diff_github = diff_github.substring(start_index, end_index);
|
|
206
|
+
// diff_github = JSON.stringify(diff_github).slice(1, -1);
|
|
207
|
+
|
|
208
|
+
// diff_local = diff_local.substring(start_index, end_index);
|
|
209
|
+
// diff_local = JSON.stringify(diff_local).slice(1, -1);
|
|
210
|
+
|
|
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}⌃`);
|
|
181
217
|
}
|
|
182
218
|
} else if (!group.master_base && previous_group && previous_group.master_base) {
|
|
183
219
|
// special case
|
|
@@ -197,8 +233,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
197
233
|
|
|
198
234
|
// compare all commits against pr commits
|
|
199
235
|
if (group.pr.commits.length !== all_commits.length) {
|
|
236
|
+
// console.debug("BOUNDARY_COMMIT_LENGTH_MISMATCH");
|
|
200
237
|
group.dirty = true;
|
|
201
238
|
} else {
|
|
239
|
+
// console.debug("BOUNDARY_COMMIT_SHA_COMPARISON");
|
|
202
240
|
for (let i = 0; i < group.pr.commits.length; i++) {
|
|
203
241
|
const pr_commit = group.pr.commits[i];
|
|
204
242
|
const local_commit = all_commits[i];
|
|
@@ -209,8 +247,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
209
247
|
}
|
|
210
248
|
}
|
|
211
249
|
} else if (group.pr.commits.length !== group.commits.length) {
|
|
250
|
+
// console.debug("COMMIT_LENGTH_MISMATCH");
|
|
212
251
|
group.dirty = true;
|
|
213
252
|
} else {
|
|
253
|
+
// console.debug("COMMIT_SHA_COMPARISON");
|
|
214
254
|
// if we still haven't marked this dirty, check each commit
|
|
215
255
|
// comapre literal commit shas in group
|
|
216
256
|
for (let i = 0; i < group.pr.commits.length; i++) {
|
|
@@ -239,3 +279,11 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
239
279
|
}
|
|
240
280
|
|
|
241
281
|
export const UNASSIGNED = "unassigned";
|
|
282
|
+
|
|
283
|
+
function diff_strip_index_lines(diff_text: string) {
|
|
284
|
+
return diff_text.replace(RE.diff_index_line, "");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const RE = {
|
|
288
|
+
diff_index_line: /^index [0-9a-f]+\.\.[0-9a-f]+.*?\n/gm,
|
|
289
|
+
};
|