git-stack-cli 1.0.2 → 1.0.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/README.md +1 -1
- package/dist/cjs/index.cjs +1 -1
- package/package.json +13 -7
- package/rollup.config.mjs +46 -0
- package/scripts/.eslintrc.cjs +61 -0
- package/scripts/core/file.ts +32 -0
- package/scripts/core/spawn.ts +41 -0
- package/scripts/npm-prepublishOnly.ts +8 -0
- package/scripts/prepare-standalone.ts +59 -0
- package/scripts/release-brew.ts +105 -0
- package/scripts/release-npm.ts +109 -0
- package/scripts/tsconfig.json +35 -0
- package/src/__fixtures__/metadata.ts +666 -0
- package/src/app/App.tsx +65 -0
- package/src/app/AutoUpdate.tsx +229 -0
- package/src/app/Await.tsx +82 -0
- package/src/app/Brackets.tsx +22 -0
- package/src/app/Command.tsx +19 -0
- package/src/app/Debug.tsx +52 -0
- package/src/app/DependencyCheck.tsx +155 -0
- package/src/app/Exit.tsx +25 -0
- package/src/app/FormatText.tsx +26 -0
- package/src/app/GatherMetadata.tsx +145 -0
- package/src/app/GithubApiError.tsx +78 -0
- package/src/app/LocalCommitStatus.tsx +70 -0
- package/src/app/LocalMergeRebase.tsx +230 -0
- package/src/app/LogTimestamp.tsx +12 -0
- package/src/app/Main.tsx +52 -0
- package/src/app/ManualRebase.tsx +308 -0
- package/src/app/MultiSelect.tsx +246 -0
- package/src/app/Output.tsx +37 -0
- package/src/app/Parens.tsx +21 -0
- package/src/app/PostRebaseStatus.tsx +33 -0
- package/src/app/PreLocalMergeRebase.tsx +31 -0
- package/src/app/PreSelectCommitRanges.tsx +31 -0
- package/src/app/Providers.tsx +11 -0
- package/src/app/RebaseCheck.tsx +96 -0
- package/src/app/SelectCommitRanges.tsx +372 -0
- package/src/app/Status.tsx +82 -0
- package/src/app/StatusTable.tsx +155 -0
- package/src/app/Store.tsx +252 -0
- package/src/app/Table.tsx +137 -0
- package/src/app/TextInput.tsx +88 -0
- package/src/app/Url.tsx +19 -0
- package/src/app/Waterfall.tsx +37 -0
- package/src/app/YesNoPrompt.tsx +73 -0
- package/src/command.ts +78 -0
- package/src/core/CommitMetadata.ts +212 -0
- package/src/core/Metadata.test.ts +41 -0
- package/src/core/Metadata.ts +51 -0
- package/src/core/StackSummaryTable.test.ts +157 -0
- package/src/core/StackSummaryTable.ts +127 -0
- package/src/core/Timer.ts +44 -0
- package/src/core/assertNever.ts +4 -0
- package/src/core/cache.ts +49 -0
- package/src/core/capitalize.ts +5 -0
- package/src/core/chalk.ts +103 -0
- package/src/core/clamp.ts +6 -0
- package/src/core/cli.ts +161 -0
- package/src/core/colors.ts +23 -0
- package/src/core/date.ts +25 -0
- package/src/core/fetch_json.ts +26 -0
- package/src/core/github.tsx +215 -0
- package/src/core/invariant.ts +5 -0
- package/src/core/is_command_available.ts +21 -0
- package/src/core/is_finite_value.ts +3 -0
- package/src/core/json.ts +32 -0
- package/src/core/match_group.ts +10 -0
- package/src/core/read_json.ts +12 -0
- package/src/core/safe_quote.ts +10 -0
- package/src/core/semver_compare.ts +27 -0
- package/src/core/short_id.ts +87 -0
- package/src/core/sleep.ts +3 -0
- package/src/core/wrap_index.ts +11 -0
- package/src/index.tsx +22 -0
- package/src/types/global.d.ts +7 -0
- package/tsconfig.json +53 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Await } from "~/app/Await";
|
|
6
|
+
import { Brackets } from "~/app/Brackets";
|
|
7
|
+
import { FormatText } from "~/app/FormatText";
|
|
8
|
+
import { Store } from "~/app/Store";
|
|
9
|
+
import { cli } from "~/core/cli";
|
|
10
|
+
import { colors } from "~/core/colors";
|
|
11
|
+
import { invariant } from "~/core/invariant";
|
|
12
|
+
import { match_group } from "~/core/match_group";
|
|
13
|
+
|
|
14
|
+
type Props = {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function GatherMetadata(props: Props) {
|
|
19
|
+
const argv = Store.useState((state) => state.argv);
|
|
20
|
+
invariant(argv, "argv must exist");
|
|
21
|
+
|
|
22
|
+
const fallback = (
|
|
23
|
+
<Ink.Text color={colors.yellow}>
|
|
24
|
+
Gathering local git information...
|
|
25
|
+
</Ink.Text>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Await fallback={fallback} function={gather_metadata}>
|
|
30
|
+
{props.children}
|
|
31
|
+
</Await>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function gather_metadata() {
|
|
36
|
+
const actions = Store.getState().actions;
|
|
37
|
+
const argv = Store.getState().argv;
|
|
38
|
+
|
|
39
|
+
invariant(argv, "argv must exist");
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// default to master branch, fallback to main
|
|
43
|
+
let master_branch: string;
|
|
44
|
+
|
|
45
|
+
if (argv.branch) {
|
|
46
|
+
actions.debug(
|
|
47
|
+
<FormatText
|
|
48
|
+
message="Setting master branch to {branch}"
|
|
49
|
+
values={{
|
|
50
|
+
branch: <Brackets>{argv.branch}</Brackets>,
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
master_branch = argv.branch;
|
|
56
|
+
} else {
|
|
57
|
+
const detect_master = await cli(
|
|
58
|
+
`git branch --list "${BRANCH.master}" --color=never`
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (detect_master.stdout !== "") {
|
|
62
|
+
master_branch = BRANCH.master;
|
|
63
|
+
} else {
|
|
64
|
+
actions.debug(
|
|
65
|
+
<FormatText
|
|
66
|
+
message="Could not find {master} branch, falling back to {main}"
|
|
67
|
+
values={{
|
|
68
|
+
master: <Brackets>{BRANCH.master}</Brackets>,
|
|
69
|
+
main: <Brackets>{BRANCH.main}</Brackets>,
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
master_branch = BRANCH.main;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const branch_name = (await cli("git rev-parse --abbrev-ref HEAD")).stdout;
|
|
79
|
+
|
|
80
|
+
// handle detahed head state
|
|
81
|
+
if (branch_name === "HEAD") {
|
|
82
|
+
actions.error("Must run within a branch.");
|
|
83
|
+
actions.exit(0);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// handle when there are no detected changes
|
|
88
|
+
if (branch_name === master_branch) {
|
|
89
|
+
actions.error("Must run within a branch.");
|
|
90
|
+
actions.exit(0);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const head = (await cli("git rev-parse HEAD")).stdout;
|
|
95
|
+
const merge_base = (await cli(`git merge-base HEAD ${master_branch}`))
|
|
96
|
+
.stdout;
|
|
97
|
+
|
|
98
|
+
// handle when there are no detected changes
|
|
99
|
+
if (head === merge_base) {
|
|
100
|
+
actions.newline();
|
|
101
|
+
actions.output(
|
|
102
|
+
<Ink.Text color={colors.gray}>No changes detected.</Ink.Text>
|
|
103
|
+
);
|
|
104
|
+
actions.exit(0);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// git@github.com:magus/git-multi-diff-playground.git
|
|
109
|
+
// https://github.com/magus/git-multi-diff-playground.git
|
|
110
|
+
const origin_url = (await cli(`git config --get remote.origin.url`)).stdout;
|
|
111
|
+
const repo_path = match_group(origin_url, RE.repo_path, "repo_path");
|
|
112
|
+
|
|
113
|
+
const repo_root = (await cli(`git rev-parse --show-toplevel`)).stdout;
|
|
114
|
+
|
|
115
|
+
Store.setState((state) => {
|
|
116
|
+
state.repo_path = repo_path;
|
|
117
|
+
state.repo_root = repo_root;
|
|
118
|
+
state.master_branch = master_branch;
|
|
119
|
+
state.head = head;
|
|
120
|
+
state.merge_base = merge_base;
|
|
121
|
+
state.branch_name = branch_name;
|
|
122
|
+
});
|
|
123
|
+
} catch (err) {
|
|
124
|
+
actions.error("Unable to gather git metadata.");
|
|
125
|
+
|
|
126
|
+
if (err instanceof Error) {
|
|
127
|
+
if (actions.isDebug()) {
|
|
128
|
+
actions.error(err.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
actions.exit(7);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const RE = {
|
|
137
|
+
// git@github.com:magus/git-multi-diff-playground.git
|
|
138
|
+
// https://github.com/magus/git-multi-diff-playground.git
|
|
139
|
+
repo_path: /(?<repo_path>[^:^/]+\/[^/]+)\.git/,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const BRANCH = {
|
|
143
|
+
master: "master",
|
|
144
|
+
main: "main",
|
|
145
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Await } from "~/app/Await";
|
|
6
|
+
import { Brackets } from "~/app/Brackets";
|
|
7
|
+
import { Parens } from "~/app/Parens";
|
|
8
|
+
import { Store } from "~/app/Store";
|
|
9
|
+
import { cli } from "~/core/cli";
|
|
10
|
+
import { colors } from "~/core/colors";
|
|
11
|
+
import * as date from "~/core/date";
|
|
12
|
+
import { invariant } from "~/core/invariant";
|
|
13
|
+
|
|
14
|
+
export function GithubApiError() {
|
|
15
|
+
const argv = Store.useState((state) => state.argv);
|
|
16
|
+
invariant(argv, "argv must exist");
|
|
17
|
+
|
|
18
|
+
return <Await fallback={null} function={run} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function run() {
|
|
22
|
+
const actions = Store.getState().actions;
|
|
23
|
+
|
|
24
|
+
const res = await cli(`gh api https://api.github.com/rate_limit`);
|
|
25
|
+
|
|
26
|
+
const res_json = JSON.parse(res.stdout);
|
|
27
|
+
|
|
28
|
+
const resources_graphql = res_json.resources.graphql;
|
|
29
|
+
|
|
30
|
+
const used = resources_graphql.used;
|
|
31
|
+
const limit = resources_graphql.limit;
|
|
32
|
+
const reset_date = new Date(resources_graphql.reset * 1000);
|
|
33
|
+
|
|
34
|
+
// calculate the time remaining in minutes
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const diff_seconds = (reset_date.getTime() - now.getTime()) / 1000;
|
|
37
|
+
const diff_minutes = Math.round(diff_seconds / 60);
|
|
38
|
+
|
|
39
|
+
const reset_time = date.format_time(reset_date);
|
|
40
|
+
|
|
41
|
+
let time_until;
|
|
42
|
+
if (diff_minutes < 0) {
|
|
43
|
+
time_until = `${diff_seconds} seconds`;
|
|
44
|
+
} else {
|
|
45
|
+
time_until = `${diff_minutes} minutes`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
actions.output(
|
|
49
|
+
<Ink.Text dimColor>
|
|
50
|
+
<Ink.Text>{"Github "}</Ink.Text>
|
|
51
|
+
|
|
52
|
+
<Brackets>graphql</Brackets>
|
|
53
|
+
|
|
54
|
+
<Ink.Text>{" API rate limit "}</Ink.Text>
|
|
55
|
+
|
|
56
|
+
<Brackets>
|
|
57
|
+
<Ink.Text>{used}</Ink.Text>
|
|
58
|
+
<Ink.Text>/</Ink.Text>
|
|
59
|
+
<Ink.Text>{limit}</Ink.Text>
|
|
60
|
+
</Brackets>
|
|
61
|
+
|
|
62
|
+
<Ink.Text>{" will reset at "}</Ink.Text>
|
|
63
|
+
|
|
64
|
+
<Ink.Text bold color={colors.yellow}>
|
|
65
|
+
{reset_time}
|
|
66
|
+
</Ink.Text>
|
|
67
|
+
|
|
68
|
+
<Ink.Text> </Ink.Text>
|
|
69
|
+
|
|
70
|
+
<Parens>
|
|
71
|
+
<Ink.Text>{"in "}</Ink.Text>
|
|
72
|
+
<Ink.Text bold color={colors.yellow}>
|
|
73
|
+
{time_until}
|
|
74
|
+
</Ink.Text>
|
|
75
|
+
</Parens>
|
|
76
|
+
</Ink.Text>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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 * as CommitMetadata from "~/core/CommitMetadata";
|
|
8
|
+
import { colors } from "~/core/colors";
|
|
9
|
+
import { invariant } from "~/core/invariant";
|
|
10
|
+
import * as json from "~/core/json";
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function LocalCommitStatus(props: Props) {
|
|
17
|
+
const argv = Store.useState((state) => state.argv);
|
|
18
|
+
invariant(argv, "argv must exist");
|
|
19
|
+
|
|
20
|
+
const fallback = (
|
|
21
|
+
<Ink.Text color={colors.yellow}>Fetching PR status from Github...</Ink.Text>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (argv["mock-metadata"]) {
|
|
25
|
+
return (
|
|
26
|
+
<Await fallback={fallback} function={mock_metadata}>
|
|
27
|
+
{props.children}
|
|
28
|
+
</Await>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Await fallback={fallback} function={gather_metadata}>
|
|
34
|
+
{props.children}
|
|
35
|
+
</Await>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function mock_metadata() {
|
|
40
|
+
const module = await import("../__fixtures__/metadata");
|
|
41
|
+
|
|
42
|
+
const deserialized = json.deserialize(module.METADATA);
|
|
43
|
+
|
|
44
|
+
Store.setState((state) => {
|
|
45
|
+
Object.assign(state, deserialized);
|
|
46
|
+
|
|
47
|
+
state.step = "status";
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function gather_metadata() {
|
|
52
|
+
const actions = Store.getState().actions;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const commit_range = await CommitMetadata.range();
|
|
56
|
+
|
|
57
|
+
Store.setState((state) => {
|
|
58
|
+
state.commit_range = commit_range;
|
|
59
|
+
state.step = "status";
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
actions.error("Unable to retrieve local commit status.");
|
|
63
|
+
|
|
64
|
+
if (err instanceof Error) {
|
|
65
|
+
if (actions.isDebug()) {
|
|
66
|
+
actions.error(err.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
|
|
5
|
+
import * as Ink from "ink-cjs";
|
|
6
|
+
|
|
7
|
+
import { Await } from "~/app/Await";
|
|
8
|
+
import { Brackets } from "~/app/Brackets";
|
|
9
|
+
import { FormatText } from "~/app/FormatText";
|
|
10
|
+
import { Parens } from "~/app/Parens";
|
|
11
|
+
import { Store } from "~/app/Store";
|
|
12
|
+
import * as CommitMetadata from "~/core/CommitMetadata";
|
|
13
|
+
import * as Metadata from "~/core/Metadata";
|
|
14
|
+
import { cli } from "~/core/cli";
|
|
15
|
+
import { colors } from "~/core/colors";
|
|
16
|
+
import { invariant } from "~/core/invariant";
|
|
17
|
+
import { short_id } from "~/core/short_id";
|
|
18
|
+
|
|
19
|
+
export function LocalMergeRebase() {
|
|
20
|
+
return (
|
|
21
|
+
<Await
|
|
22
|
+
fallback={<Ink.Text color={colors.yellow}>Rebasing commits...</Ink.Text>}
|
|
23
|
+
function={run}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function run() {
|
|
29
|
+
const state = Store.getState();
|
|
30
|
+
const actions = state.actions;
|
|
31
|
+
const argv = state.argv;
|
|
32
|
+
const branch_name = state.branch_name;
|
|
33
|
+
const commit_range = state.commit_range;
|
|
34
|
+
const master_branch = state.master_branch;
|
|
35
|
+
const cwd = state.cwd;
|
|
36
|
+
const repo_root = state.repo_root;
|
|
37
|
+
|
|
38
|
+
invariant(argv, "argv must exist");
|
|
39
|
+
invariant(branch_name, "branch_name must exist");
|
|
40
|
+
invariant(commit_range, "commit_range must exist");
|
|
41
|
+
invariant(cwd, "cwd must exist");
|
|
42
|
+
invariant(repo_root, "repo_root must exist");
|
|
43
|
+
|
|
44
|
+
// always listen for SIGINT event and restore git state
|
|
45
|
+
process.once("SIGINT", handle_exit);
|
|
46
|
+
|
|
47
|
+
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// must perform rebase from repo root for applying git patch
|
|
51
|
+
process.chdir(repo_root);
|
|
52
|
+
await cli(`pwd`);
|
|
53
|
+
|
|
54
|
+
await cli(
|
|
55
|
+
`git fetch --no-tags -v origin ${master_branch}:${master_branch}`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const master_sha = (await cli(`git rev-parse ${master_branch}`)).stdout;
|
|
59
|
+
|
|
60
|
+
const rebase_merge_base = master_sha;
|
|
61
|
+
|
|
62
|
+
// create temporary branch based on merge base
|
|
63
|
+
await cli(`git checkout -b ${temp_branch_name} ${rebase_merge_base}`);
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < commit_range.commit_list.length; i++) {
|
|
66
|
+
const commit = commit_range.commit_list[i];
|
|
67
|
+
const commit_pr = commit_range.pr_lookup[commit.branch_id || ""];
|
|
68
|
+
|
|
69
|
+
// drop commits that are in groups of merged PRs
|
|
70
|
+
const merged_pr = commit_pr?.state === "MERGED";
|
|
71
|
+
|
|
72
|
+
if (merged_pr) {
|
|
73
|
+
if (actions.isDebug()) {
|
|
74
|
+
actions.output(
|
|
75
|
+
<FormatText
|
|
76
|
+
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
77
|
+
message="Dropping {commit_message} {pr_status}"
|
|
78
|
+
values={{
|
|
79
|
+
commit_message: <Brackets>{commit.subject_line}</Brackets>,
|
|
80
|
+
pr_status: <Parens>MERGED</Parens>,
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// cherry-pick and amend commits one by one
|
|
90
|
+
if (actions.isDebug()) {
|
|
91
|
+
actions.output(
|
|
92
|
+
<FormatText
|
|
93
|
+
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
94
|
+
message="Picking {commit_message}"
|
|
95
|
+
values={{
|
|
96
|
+
commit_message: <Brackets>{commit.subject_line}</Brackets>,
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ensure clean base to avoid conflicts when applying patch
|
|
103
|
+
await cli(`git clean -fd`);
|
|
104
|
+
|
|
105
|
+
// create, apply and cleanup patch
|
|
106
|
+
await cli(`git format-patch -1 ${commit.sha} --stdout > ${PATCH_FILE}`);
|
|
107
|
+
await cli(`git apply ${PATCH_FILE}`);
|
|
108
|
+
await cli(`rm ${PATCH_FILE}`);
|
|
109
|
+
|
|
110
|
+
// add all changes to stage
|
|
111
|
+
await cli(`git add --all`);
|
|
112
|
+
|
|
113
|
+
let new_message;
|
|
114
|
+
if (commit.branch_id) {
|
|
115
|
+
new_message = await Metadata.write(
|
|
116
|
+
commit.full_message,
|
|
117
|
+
commit.branch_id
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
new_message = commit.full_message;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const git_commit_comand = [`git commit -m "${new_message}"`];
|
|
124
|
+
|
|
125
|
+
if (argv.verify === false) {
|
|
126
|
+
git_commit_comand.push("--no-verify");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await cli(git_commit_comand);
|
|
130
|
+
|
|
131
|
+
if (commit.branch_id && !commit_pr) {
|
|
132
|
+
if (actions.isDebug()) {
|
|
133
|
+
actions.output(
|
|
134
|
+
<FormatText
|
|
135
|
+
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
136
|
+
message="Cleaning up unused group {group}"
|
|
137
|
+
values={{
|
|
138
|
+
group: <Brackets>{commit.branch_id}</Brackets>,
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// missing PR, clear branch id from commit
|
|
145
|
+
const new_message = await Metadata.remove(commit.full_message);
|
|
146
|
+
await cli(`git commit --amend -m "${new_message}"`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// after all commits have been cherry-picked and amended
|
|
151
|
+
// move the branch pointer to the newly created temporary branch
|
|
152
|
+
// now we are in locally in sync with github and on the original branch
|
|
153
|
+
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
154
|
+
|
|
155
|
+
restore_git();
|
|
156
|
+
|
|
157
|
+
const next_commit_range = await CommitMetadata.range();
|
|
158
|
+
|
|
159
|
+
actions.set((state) => {
|
|
160
|
+
state.commit_range = next_commit_range;
|
|
161
|
+
state.step = "status";
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
actions.error("Unable to rebase.");
|
|
165
|
+
|
|
166
|
+
if (err instanceof Error) {
|
|
167
|
+
if (actions.isDebug()) {
|
|
168
|
+
actions.error(err.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
handle_exit();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// cleanup git operations if cancelled during manual rebase
|
|
176
|
+
function restore_git() {
|
|
177
|
+
// signint handler MUST run synchronously
|
|
178
|
+
// trying to use `await cli(...)` here will silently fail since
|
|
179
|
+
// all children processes receive the SIGINT signal
|
|
180
|
+
const spawn_options = { ignoreExitCode: true };
|
|
181
|
+
|
|
182
|
+
// always clean up any patch files
|
|
183
|
+
cli.sync(`rm ${PATCH_FILE}`, spawn_options);
|
|
184
|
+
|
|
185
|
+
// always hard reset and clean to allow subsequent checkout
|
|
186
|
+
// if there are files checkout will fail and cascade fail subsequent commands
|
|
187
|
+
cli.sync(`git reset --hard`, spawn_options);
|
|
188
|
+
cli.sync(`git clean -df`, spawn_options);
|
|
189
|
+
|
|
190
|
+
// always put self back in original branch
|
|
191
|
+
cli.sync(`git checkout ${branch_name}`, spawn_options);
|
|
192
|
+
|
|
193
|
+
// ...and cleanup temporary branch
|
|
194
|
+
cli.sync(`git branch -D ${temp_branch_name}`, spawn_options);
|
|
195
|
+
|
|
196
|
+
if (commit_range) {
|
|
197
|
+
// ...and cleanup pr group branches
|
|
198
|
+
for (const group of commit_range.group_list) {
|
|
199
|
+
cli.sync(`git branch -D ${group.id}`, spawn_options);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// restore back to original dir
|
|
204
|
+
invariant(cwd, "cwd must exist");
|
|
205
|
+
if (fs.existsSync(cwd)) {
|
|
206
|
+
process.chdir(cwd);
|
|
207
|
+
}
|
|
208
|
+
cli.sync(`pwd`, spawn_options);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handle_exit() {
|
|
212
|
+
actions.output(
|
|
213
|
+
<Ink.Text color={colors.yellow}>
|
|
214
|
+
Restoring <Brackets>{branch_name}</Brackets>...
|
|
215
|
+
</Ink.Text>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
restore_git();
|
|
219
|
+
|
|
220
|
+
actions.output(
|
|
221
|
+
<Ink.Text color={colors.yellow}>
|
|
222
|
+
Restored <Brackets>{branch_name}</Brackets>.
|
|
223
|
+
</Ink.Text>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
actions.exit(6);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const PATCH_FILE = "git-stack-cli-patch.patch";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
import { DateTime } from "luxon";
|
|
5
|
+
|
|
6
|
+
export function LogTimestamp() {
|
|
7
|
+
return (
|
|
8
|
+
<Ink.Text dimColor>
|
|
9
|
+
{DateTime.now().toFormat("[yyyy-MM-dd HH:mm:ss.SSS] ")}
|
|
10
|
+
</Ink.Text>
|
|
11
|
+
);
|
|
12
|
+
}
|
package/src/app/Main.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { GithubApiError } from "~/app/GithubApiError";
|
|
4
|
+
import { LocalMergeRebase } from "~/app/LocalMergeRebase";
|
|
5
|
+
import { ManualRebase } from "~/app/ManualRebase";
|
|
6
|
+
import { PostRebaseStatus } from "~/app/PostRebaseStatus";
|
|
7
|
+
import { PreLocalMergeRebase } from "~/app/PreLocalMergeRebase";
|
|
8
|
+
import { PreSelectCommitRanges } from "~/app/PreSelectCommitRanges";
|
|
9
|
+
import { SelectCommitRanges } from "~/app/SelectCommitRanges";
|
|
10
|
+
import { Status } from "~/app/Status";
|
|
11
|
+
import { Store } from "~/app/Store";
|
|
12
|
+
import { assertNever } from "~/core/assertNever";
|
|
13
|
+
|
|
14
|
+
export function Main() {
|
|
15
|
+
const step = Store.useState((state) => state.step);
|
|
16
|
+
|
|
17
|
+
switch (step) {
|
|
18
|
+
case "github-api-error":
|
|
19
|
+
return <GithubApiError />;
|
|
20
|
+
|
|
21
|
+
case "loading":
|
|
22
|
+
return null;
|
|
23
|
+
|
|
24
|
+
case "status":
|
|
25
|
+
return <Status />;
|
|
26
|
+
|
|
27
|
+
case "local-merge-rebase":
|
|
28
|
+
return <LocalMergeRebase />;
|
|
29
|
+
|
|
30
|
+
case "pre-local-merge-rebase":
|
|
31
|
+
return <PreLocalMergeRebase />;
|
|
32
|
+
|
|
33
|
+
case "pre-select-commit-ranges":
|
|
34
|
+
return <PreSelectCommitRanges />;
|
|
35
|
+
|
|
36
|
+
case "select-commit-ranges":
|
|
37
|
+
return <SelectCommitRanges />;
|
|
38
|
+
|
|
39
|
+
case "manual-rebase":
|
|
40
|
+
return <ManualRebase />;
|
|
41
|
+
|
|
42
|
+
case "manual-rebase-no-sync":
|
|
43
|
+
return <ManualRebase skipSync />;
|
|
44
|
+
|
|
45
|
+
case "post-rebase-status":
|
|
46
|
+
return <PostRebaseStatus />;
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
assertNever(step);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|