git-stack-cli 1.0.1 → 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,308 @@
|
|
|
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 { Store } from "~/app/Store";
|
|
11
|
+
import * as CommitMetadata from "~/core/CommitMetadata";
|
|
12
|
+
import * as Metadata from "~/core/Metadata";
|
|
13
|
+
import * as StackSummaryTable from "~/core/StackSummaryTable";
|
|
14
|
+
import { cli } from "~/core/cli";
|
|
15
|
+
import { colors } from "~/core/colors";
|
|
16
|
+
import * as github from "~/core/github";
|
|
17
|
+
import { invariant } from "~/core/invariant";
|
|
18
|
+
import { short_id } from "~/core/short_id";
|
|
19
|
+
|
|
20
|
+
type Props = {
|
|
21
|
+
skipSync?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function ManualRebase(props: Props) {
|
|
25
|
+
return (
|
|
26
|
+
<Await
|
|
27
|
+
fallback={<Ink.Text color={colors.yellow}>Rebasing commits...</Ink.Text>}
|
|
28
|
+
function={() => run(props)}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function run(props: Props) {
|
|
34
|
+
const state = Store.getState();
|
|
35
|
+
const actions = state.actions;
|
|
36
|
+
const argv = state.argv;
|
|
37
|
+
const branch_name = state.branch_name;
|
|
38
|
+
const merge_base = state.merge_base;
|
|
39
|
+
const commit_map = state.commit_map;
|
|
40
|
+
const cwd = state.cwd;
|
|
41
|
+
const repo_root = state.repo_root;
|
|
42
|
+
|
|
43
|
+
invariant(argv, "argv must exist");
|
|
44
|
+
invariant(branch_name, "branch_name must exist");
|
|
45
|
+
invariant(merge_base, "merge_base must exist");
|
|
46
|
+
invariant(commit_map, "commit_map must exist");
|
|
47
|
+
invariant(cwd, "cwd must exist");
|
|
48
|
+
invariant(repo_root, "repo_root must exist");
|
|
49
|
+
|
|
50
|
+
// always listen for SIGINT event and restore git state
|
|
51
|
+
process.once("SIGINT", handle_exit);
|
|
52
|
+
|
|
53
|
+
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
54
|
+
|
|
55
|
+
const commit_range = await CommitMetadata.range(commit_map);
|
|
56
|
+
|
|
57
|
+
// reverse commit list so that we can cherry-pick in order
|
|
58
|
+
commit_range.group_list.reverse();
|
|
59
|
+
|
|
60
|
+
let rebase_merge_base = merge_base;
|
|
61
|
+
let rebase_group_index = 0;
|
|
62
|
+
|
|
63
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
64
|
+
const group = commit_range.group_list[i];
|
|
65
|
+
|
|
66
|
+
if (!group.dirty) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (i > 0) {
|
|
71
|
+
const last_group = commit_range.group_list[i - 1];
|
|
72
|
+
const last_commit = last_group.commits[last_group.commits.length - 1];
|
|
73
|
+
rebase_merge_base = last_commit.sha;
|
|
74
|
+
rebase_group_index = i;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// must perform rebase from repo root for applying git patch
|
|
82
|
+
process.chdir(repo_root);
|
|
83
|
+
await cli(`pwd`);
|
|
84
|
+
|
|
85
|
+
// create temporary branch based on merge base
|
|
86
|
+
await cli(`git checkout -b ${temp_branch_name} ${rebase_merge_base}`);
|
|
87
|
+
|
|
88
|
+
const pr_url_list = commit_range.group_list.map(get_group_url);
|
|
89
|
+
|
|
90
|
+
for (let i = rebase_group_index; i < commit_range.group_list.length; i++) {
|
|
91
|
+
const group = commit_range.group_list[i];
|
|
92
|
+
|
|
93
|
+
invariant(group.base, "group.base must exist");
|
|
94
|
+
|
|
95
|
+
actions.output(
|
|
96
|
+
<FormatText
|
|
97
|
+
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
98
|
+
message="Rebasing {group}…"
|
|
99
|
+
values={{
|
|
100
|
+
group: (
|
|
101
|
+
<Brackets>{group.pr?.title || group.title || group.id}</Brackets>
|
|
102
|
+
),
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const selected_url = get_group_url(group);
|
|
108
|
+
|
|
109
|
+
// cherry-pick and amend commits one by one
|
|
110
|
+
for (const commit of group.commits) {
|
|
111
|
+
// ensure clean base to avoid conflicts when applying patch
|
|
112
|
+
await cli(`git clean -fd`);
|
|
113
|
+
|
|
114
|
+
// create, apply and cleanup patch
|
|
115
|
+
await cli(`git format-patch -1 ${commit.sha} --stdout > ${PATCH_FILE}`);
|
|
116
|
+
await cli(`git apply ${PATCH_FILE}`);
|
|
117
|
+
await cli(`rm ${PATCH_FILE}`);
|
|
118
|
+
|
|
119
|
+
// add all changes to stage
|
|
120
|
+
await cli(`git add --all`);
|
|
121
|
+
|
|
122
|
+
const new_message = await Metadata.write(commit.full_message, group.id);
|
|
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
|
+
|
|
132
|
+
actions.output(
|
|
133
|
+
<FormatText
|
|
134
|
+
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
135
|
+
message="Syncing {group}…"
|
|
136
|
+
values={{
|
|
137
|
+
group: (
|
|
138
|
+
<Brackets>{group.pr?.title || group.title || group.id}</Brackets>
|
|
139
|
+
),
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!props.skipSync) {
|
|
145
|
+
// push to origin since github requires commit shas to line up perfectly
|
|
146
|
+
const git_push_command = [`git push -f origin HEAD:${group.id}`];
|
|
147
|
+
|
|
148
|
+
if (argv.verify === false) {
|
|
149
|
+
git_push_command.push("--no-verify");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await cli(git_push_command);
|
|
153
|
+
|
|
154
|
+
if (group.pr) {
|
|
155
|
+
// ensure base matches pr in github
|
|
156
|
+
await github.pr_edit({
|
|
157
|
+
branch: group.id,
|
|
158
|
+
base: group.base,
|
|
159
|
+
body: StackSummaryTable.write({
|
|
160
|
+
body: group.pr.body,
|
|
161
|
+
pr_url_list,
|
|
162
|
+
selected_url,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
// delete local group branch if leftover
|
|
167
|
+
await cli(`git branch -D ${group.id}`, { ignoreExitCode: true });
|
|
168
|
+
|
|
169
|
+
// move to temporary branch for creating pr
|
|
170
|
+
await cli(`git checkout -b ${group.id}`);
|
|
171
|
+
|
|
172
|
+
// create pr in github
|
|
173
|
+
const pr_url = await github.pr_create({
|
|
174
|
+
branch: group.id,
|
|
175
|
+
base: group.base,
|
|
176
|
+
title: group.title,
|
|
177
|
+
body: "",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!pr_url) {
|
|
181
|
+
throw new Error("unable to create pr");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// update pr_url_list with created pr_url
|
|
185
|
+
for (let i = 0; i < pr_url_list.length; i++) {
|
|
186
|
+
const url = pr_url_list[i];
|
|
187
|
+
if (url === selected_url) {
|
|
188
|
+
pr_url_list[i] = pr_url;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// move back to temp branch
|
|
193
|
+
await cli(`git checkout ${temp_branch_name}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// finally, ensure all prs have the updated stack table from updated pr_url_list
|
|
199
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
200
|
+
const group = commit_range.group_list[i];
|
|
201
|
+
|
|
202
|
+
// use the updated pr_url_list to get the actual selected_url
|
|
203
|
+
const selected_url = pr_url_list[i];
|
|
204
|
+
|
|
205
|
+
invariant(group.base, "group.base must exist");
|
|
206
|
+
|
|
207
|
+
const body = group.pr?.body || "";
|
|
208
|
+
|
|
209
|
+
const update_body = StackSummaryTable.write({
|
|
210
|
+
body,
|
|
211
|
+
pr_url_list,
|
|
212
|
+
selected_url,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (update_body === body) {
|
|
216
|
+
actions.debug(`Skipping body update for ${selected_url}`);
|
|
217
|
+
} else {
|
|
218
|
+
actions.debug(`Update body for ${selected_url}`);
|
|
219
|
+
|
|
220
|
+
await github.pr_edit({
|
|
221
|
+
branch: group.id,
|
|
222
|
+
base: group.base,
|
|
223
|
+
body: update_body,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// after all commits have been cherry-picked and amended
|
|
229
|
+
// move the branch pointer to the newly created temporary branch
|
|
230
|
+
// now we are in locally in sync with github and on the original branch
|
|
231
|
+
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
232
|
+
|
|
233
|
+
restore_git();
|
|
234
|
+
|
|
235
|
+
actions.set((state) => {
|
|
236
|
+
state.step = "post-rebase-status";
|
|
237
|
+
});
|
|
238
|
+
} catch (err) {
|
|
239
|
+
actions.error("Unable to rebase.");
|
|
240
|
+
|
|
241
|
+
if (err instanceof Error) {
|
|
242
|
+
if (actions.isDebug()) {
|
|
243
|
+
actions.error(err.message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
handle_exit();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// cleanup git operations if cancelled during manual rebase
|
|
251
|
+
function restore_git() {
|
|
252
|
+
// signint handler MUST run synchronously
|
|
253
|
+
// trying to use `await cli(...)` here will silently fail since
|
|
254
|
+
// all children processes receive the SIGINT signal
|
|
255
|
+
const spawn_options = { ignoreExitCode: true };
|
|
256
|
+
|
|
257
|
+
// always clean up any patch files
|
|
258
|
+
cli.sync(`rm ${PATCH_FILE}`, spawn_options);
|
|
259
|
+
|
|
260
|
+
// always hard reset and clean to allow subsequent checkout
|
|
261
|
+
// if there are files checkout will fail and cascade fail subsequent commands
|
|
262
|
+
cli.sync(`git reset --hard`, spawn_options);
|
|
263
|
+
cli.sync(`git clean -df`, spawn_options);
|
|
264
|
+
|
|
265
|
+
// always put self back in original branch
|
|
266
|
+
cli.sync(`git checkout ${branch_name}`, spawn_options);
|
|
267
|
+
|
|
268
|
+
// ...and cleanup temporary branch
|
|
269
|
+
cli.sync(`git branch -D ${temp_branch_name}`, spawn_options);
|
|
270
|
+
|
|
271
|
+
if (commit_range) {
|
|
272
|
+
// ...and cleanup pr group branches
|
|
273
|
+
for (const group of commit_range.group_list) {
|
|
274
|
+
cli.sync(`git branch -D ${group.id}`, spawn_options);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// restore back to original dir
|
|
279
|
+
invariant(cwd, "cwd must exist");
|
|
280
|
+
if (fs.existsSync(cwd)) {
|
|
281
|
+
process.chdir(cwd);
|
|
282
|
+
}
|
|
283
|
+
cli.sync(`pwd`, spawn_options);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function handle_exit() {
|
|
287
|
+
actions.output(
|
|
288
|
+
<Ink.Text color={colors.yellow}>
|
|
289
|
+
Restoring <Brackets>{branch_name}</Brackets>...
|
|
290
|
+
</Ink.Text>
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
restore_git();
|
|
294
|
+
|
|
295
|
+
actions.output(
|
|
296
|
+
<Ink.Text color={colors.yellow}>
|
|
297
|
+
Restored <Brackets>{branch_name}</Brackets>.
|
|
298
|
+
</Ink.Text>
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
actions.exit(5);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
|
|
306
|
+
const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
|
|
307
|
+
|
|
308
|
+
const PATCH_FILE = "git-stack-cli-patch.patch";
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { clamp } from "~/core/clamp";
|
|
6
|
+
import { colors } from "~/core/colors";
|
|
7
|
+
import { is_finite_value } from "~/core/is_finite_value";
|
|
8
|
+
import { wrap_index } from "~/core/wrap_index";
|
|
9
|
+
|
|
10
|
+
type Props<T> = {
|
|
11
|
+
items: Array<Item<T>>;
|
|
12
|
+
onSelect(args: SelectArgs<T>): void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
maxWidth?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Item<T> = {
|
|
18
|
+
label: string;
|
|
19
|
+
value: T;
|
|
20
|
+
selected?: ItemRowProps["selected"];
|
|
21
|
+
disabled?: ItemRowProps["disabled"];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SelectArgs<T> = {
|
|
25
|
+
item: T;
|
|
26
|
+
selected: boolean;
|
|
27
|
+
state: Array<T>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function MultiSelect<T>(props: Props<T>) {
|
|
31
|
+
const [selected_set, select] = React.useReducer(
|
|
32
|
+
(state: Set<number>, value: number) => {
|
|
33
|
+
const next = new Set(state);
|
|
34
|
+
|
|
35
|
+
if (next.has(value)) {
|
|
36
|
+
next.delete(value);
|
|
37
|
+
} else {
|
|
38
|
+
next.add(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return next;
|
|
42
|
+
},
|
|
43
|
+
new Set<number>(),
|
|
44
|
+
(set) => {
|
|
45
|
+
props.items.forEach((item, i) => {
|
|
46
|
+
if (item.selected) {
|
|
47
|
+
set.add(i);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return set;
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// clamp index to keep in item range
|
|
56
|
+
const [index, set_index] = React.useReducer(
|
|
57
|
+
(_: unknown, value: number) => {
|
|
58
|
+
return clamp(value, 0, props.items.length - 1);
|
|
59
|
+
},
|
|
60
|
+
0,
|
|
61
|
+
function find_initial_index() {
|
|
62
|
+
let last_enabled;
|
|
63
|
+
|
|
64
|
+
for (let i = props.items.length - 1; i >= 0; i--) {
|
|
65
|
+
const item = props.items[i];
|
|
66
|
+
|
|
67
|
+
if (!item.disabled) {
|
|
68
|
+
last_enabled = i;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!item.selected && !item.disabled) {
|
|
72
|
+
return i;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (is_finite_value(last_enabled)) {
|
|
77
|
+
return last_enabled;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const selectRef = React.useRef(false);
|
|
85
|
+
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
if (!selectRef.current) {
|
|
88
|
+
// console.debug("[MultiSelect]", "skip onSelect before selectRef");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const item = props.items[index].value;
|
|
93
|
+
const selected_list = Array.from(selected_set);
|
|
94
|
+
const selected = selected_set.has(index);
|
|
95
|
+
const state = selected_list.map((index) => props.items[index].value);
|
|
96
|
+
|
|
97
|
+
// console.debug({ item, selected, state });
|
|
98
|
+
props.onSelect({ item, selected, state });
|
|
99
|
+
}, [selected_set]);
|
|
100
|
+
|
|
101
|
+
Ink.useInput((input, key) => {
|
|
102
|
+
if (props.disabled) {
|
|
103
|
+
// console.debug("[MultiSelect] disabled, ignoring input");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const space = input === " ";
|
|
108
|
+
|
|
109
|
+
if (key.return || space) {
|
|
110
|
+
selectRef.current = true;
|
|
111
|
+
const item = props.items[index];
|
|
112
|
+
if (!item.disabled) {
|
|
113
|
+
return select(index);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (key.upArrow) {
|
|
118
|
+
let check = index;
|
|
119
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
120
|
+
check = wrap_index(check - 1, props.items);
|
|
121
|
+
// console.debug("up", { check, i, index });
|
|
122
|
+
|
|
123
|
+
const item = props.items[check];
|
|
124
|
+
if (!item.disabled) {
|
|
125
|
+
return set_index(check);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (key.downArrow) {
|
|
131
|
+
let check = index;
|
|
132
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
133
|
+
check = wrap_index(check + 1, props.items);
|
|
134
|
+
// console.debug("down", { check, i, index });
|
|
135
|
+
|
|
136
|
+
const item = props.items[check];
|
|
137
|
+
if (!item.disabled) {
|
|
138
|
+
return set_index(check);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Ink.Box flexDirection="column">
|
|
146
|
+
{props.items.map((item, i) => {
|
|
147
|
+
const active = i === index;
|
|
148
|
+
const selected = selected_set.has(i);
|
|
149
|
+
const disabled = item.disabled || false;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<ItemRow
|
|
153
|
+
key={i}
|
|
154
|
+
label={item.label}
|
|
155
|
+
active={active}
|
|
156
|
+
selected={selected}
|
|
157
|
+
disabled={disabled}
|
|
158
|
+
maxWidth={props.maxWidth}
|
|
159
|
+
/>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
162
|
+
</Ink.Box>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type ItemRowProps = {
|
|
167
|
+
label: string;
|
|
168
|
+
active: boolean;
|
|
169
|
+
selected: boolean;
|
|
170
|
+
disabled: boolean;
|
|
171
|
+
maxWidth?: number;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function ItemRow(props: ItemRowProps) {
|
|
175
|
+
let color;
|
|
176
|
+
let bold;
|
|
177
|
+
let underline;
|
|
178
|
+
let dimColor;
|
|
179
|
+
|
|
180
|
+
if (props.active) {
|
|
181
|
+
color = colors.blue;
|
|
182
|
+
underline = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (props.selected) {
|
|
186
|
+
// color = "";
|
|
187
|
+
bold = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (props.disabled) {
|
|
191
|
+
color = "";
|
|
192
|
+
bold = false;
|
|
193
|
+
underline = false;
|
|
194
|
+
dimColor = true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Ink.Box flexDirection="row" gap={1}>
|
|
199
|
+
<Radio selected={props.selected} disabled={props.disabled} />
|
|
200
|
+
|
|
201
|
+
<Ink.Box width={props.maxWidth}>
|
|
202
|
+
<Ink.Text
|
|
203
|
+
bold={bold}
|
|
204
|
+
underline={underline}
|
|
205
|
+
color={color}
|
|
206
|
+
dimColor={dimColor}
|
|
207
|
+
wrap="truncate-end"
|
|
208
|
+
>
|
|
209
|
+
{props.label}
|
|
210
|
+
</Ink.Text>
|
|
211
|
+
</Ink.Box>
|
|
212
|
+
</Ink.Box>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type RadioProps = {
|
|
217
|
+
selected: ItemRowProps["selected"];
|
|
218
|
+
disabled: ItemRowProps["disabled"];
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
function Radio(props: RadioProps) {
|
|
222
|
+
let display;
|
|
223
|
+
let color;
|
|
224
|
+
let dimColor;
|
|
225
|
+
|
|
226
|
+
if (props.selected) {
|
|
227
|
+
// display = "✓";
|
|
228
|
+
display = "◉";
|
|
229
|
+
color = colors.green;
|
|
230
|
+
} else {
|
|
231
|
+
// display = " ";
|
|
232
|
+
display = "◯";
|
|
233
|
+
color = "";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (props.disabled) {
|
|
237
|
+
color = colors.gray;
|
|
238
|
+
dimColor = true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<Ink.Text bold={props.selected} color={color} dimColor={dimColor}>
|
|
243
|
+
{display}
|
|
244
|
+
</Ink.Text>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Store } from "~/app/Store";
|
|
6
|
+
|
|
7
|
+
export function Output() {
|
|
8
|
+
const output = Store.useState((state) => state.output);
|
|
9
|
+
const pending_output = Store.useState((state) => state.pending_output);
|
|
10
|
+
const pending_output_items = Object.values(pending_output);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<React.Fragment>
|
|
14
|
+
<Ink.Static items={output}>
|
|
15
|
+
{(node, i) => {
|
|
16
|
+
return <Ink.Box key={i}>{node}</Ink.Box>;
|
|
17
|
+
}}
|
|
18
|
+
</Ink.Static>
|
|
19
|
+
|
|
20
|
+
{pending_output_items.map((node_list, i) => {
|
|
21
|
+
return (
|
|
22
|
+
<Ink.Box key={i}>
|
|
23
|
+
<Ink.Text>
|
|
24
|
+
{node_list.map((text, j) => {
|
|
25
|
+
return (
|
|
26
|
+
<React.Fragment key={j}>
|
|
27
|
+
<Ink.Text>{text}</Ink.Text>
|
|
28
|
+
</React.Fragment>
|
|
29
|
+
);
|
|
30
|
+
})}
|
|
31
|
+
</Ink.Text>
|
|
32
|
+
</Ink.Box>
|
|
33
|
+
);
|
|
34
|
+
})}
|
|
35
|
+
</React.Fragment>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { colors } from "~/core/colors";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function Parens(props: Props) {
|
|
12
|
+
const color = colors.blue;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Ink.Text>
|
|
16
|
+
<Ink.Text color={color}>(</Ink.Text>
|
|
17
|
+
{props.children}
|
|
18
|
+
<Ink.Text color={color}>)</Ink.Text>
|
|
19
|
+
</Ink.Text>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Await } from "~/app/Await";
|
|
6
|
+
import { StatusTable } from "~/app/StatusTable";
|
|
7
|
+
import { Store } from "~/app/Store";
|
|
8
|
+
import * as CommitMetadata from "~/core/CommitMetadata";
|
|
9
|
+
import { invariant } from "~/core/invariant";
|
|
10
|
+
|
|
11
|
+
export function PostRebaseStatus() {
|
|
12
|
+
const argv = Store.useState((state) => state.argv);
|
|
13
|
+
invariant(argv, "argv must exist");
|
|
14
|
+
|
|
15
|
+
return <Await fallback={null} function={run} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function run() {
|
|
19
|
+
const actions = Store.getState().actions;
|
|
20
|
+
|
|
21
|
+
// reset github pr cache before refreshing via commit range below
|
|
22
|
+
actions.reset_pr();
|
|
23
|
+
|
|
24
|
+
const commit_range = await CommitMetadata.range();
|
|
25
|
+
|
|
26
|
+
actions.set((state) => {
|
|
27
|
+
state.commit_range = commit_range;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
actions.output(<StatusTable />);
|
|
31
|
+
|
|
32
|
+
actions.output(<Ink.Text>✅ Everything up to date.</Ink.Text>);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { Store } from "~/app/Store";
|
|
4
|
+
import { YesNoPrompt } from "~/app/YesNoPrompt";
|
|
5
|
+
import { invariant } from "~/core/invariant";
|
|
6
|
+
|
|
7
|
+
export function PreLocalMergeRebase() {
|
|
8
|
+
const actions = Store.useActions();
|
|
9
|
+
const argv = Store.useState((state) => state.argv);
|
|
10
|
+
invariant(argv, "argv must exist");
|
|
11
|
+
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
if (argv.force) {
|
|
14
|
+
Store.setState((state) => {
|
|
15
|
+
state.step = "local-merge-rebase";
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}, [argv]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<YesNoPrompt
|
|
22
|
+
message="Local branch needs to be rebased, would you like to rebase to update your local branch?"
|
|
23
|
+
onYes={() => {
|
|
24
|
+
actions.set((state) => {
|
|
25
|
+
state.step = "local-merge-rebase";
|
|
26
|
+
});
|
|
27
|
+
}}
|
|
28
|
+
onNo={() => actions.exit(0)}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { Store } from "~/app/Store";
|
|
4
|
+
import { YesNoPrompt } from "~/app/YesNoPrompt";
|
|
5
|
+
import { invariant } from "~/core/invariant";
|
|
6
|
+
|
|
7
|
+
export function PreSelectCommitRanges() {
|
|
8
|
+
const actions = Store.useActions();
|
|
9
|
+
const argv = Store.useState((state) => state.argv);
|
|
10
|
+
invariant(argv, "argv must exist");
|
|
11
|
+
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
if (argv.force) {
|
|
14
|
+
Store.setState((state) => {
|
|
15
|
+
state.step = "select-commit-ranges";
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}, [argv]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<YesNoPrompt
|
|
22
|
+
message="Some commits are new or outdated, would you like to select new commit ranges?"
|
|
23
|
+
onYes={() => {
|
|
24
|
+
actions.set((state) => {
|
|
25
|
+
state.step = "select-commit-ranges";
|
|
26
|
+
});
|
|
27
|
+
}}
|
|
28
|
+
onNo={() => actions.exit(0)}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|