git-stack-cli 1.0.2 → 1.0.4
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 +3 -3
- package/package.json +15 -8
- package/rollup.config.mjs +46 -0
- package/scripts/.eslintrc.cjs +61 -0
- package/scripts/build-standalone.ts +73 -0
- package/scripts/core/create_asset.ts +21 -0
- package/scripts/core/file.ts +36 -0
- package/scripts/core/spawn.ts +62 -0
- package/scripts/npm-prepublishOnly.ts +8 -0
- package/scripts/release-brew.ts +69 -0
- package/scripts/release-github.ts +34 -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,96 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import * as Ink from "ink-cjs";
|
|
7
|
+
|
|
8
|
+
import { Await } from "~/app/Await";
|
|
9
|
+
import { Store } from "~/app/Store";
|
|
10
|
+
import { YesNoPrompt } from "~/app/YesNoPrompt";
|
|
11
|
+
import { cli } from "~/core/cli";
|
|
12
|
+
import { colors } from "~/core/colors";
|
|
13
|
+
import { invariant } from "~/core/invariant";
|
|
14
|
+
|
|
15
|
+
type Props = {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type State = {
|
|
20
|
+
status: "init" | "prompt" | "done";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function reducer(state: State, patch: Partial<State>) {
|
|
24
|
+
return { ...state, ...patch };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function RebaseCheck(props: Props) {
|
|
28
|
+
const actions = Store.useActions();
|
|
29
|
+
const argv = Store.useState((state) => state.argv);
|
|
30
|
+
invariant(argv, "argv must exist");
|
|
31
|
+
|
|
32
|
+
const [state, patch] = React.useReducer(reducer, {
|
|
33
|
+
status: "init",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
switch (state.status) {
|
|
37
|
+
case "done":
|
|
38
|
+
return props.children;
|
|
39
|
+
|
|
40
|
+
case "prompt":
|
|
41
|
+
return (
|
|
42
|
+
<YesNoPrompt
|
|
43
|
+
message={
|
|
44
|
+
<Ink.Text color={colors.yellow}>
|
|
45
|
+
Rebase detected, would you like to abort it?
|
|
46
|
+
</Ink.Text>
|
|
47
|
+
}
|
|
48
|
+
onYes={async () => {
|
|
49
|
+
await cli(`git rebase --abort`);
|
|
50
|
+
patch({ status: "done" });
|
|
51
|
+
}}
|
|
52
|
+
onNo={async () => {
|
|
53
|
+
actions.exit(0);
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
default:
|
|
59
|
+
return (
|
|
60
|
+
<Await
|
|
61
|
+
fallback={
|
|
62
|
+
<Ink.Text color={colors.yellow}>Checking for rebase...</Ink.Text>
|
|
63
|
+
}
|
|
64
|
+
function={rebase_check}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function rebase_check() {
|
|
70
|
+
const actions = Store.getState().actions;
|
|
71
|
+
const argv = Store.getState().argv;
|
|
72
|
+
|
|
73
|
+
invariant(argv, "argv must exist");
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const repo_root = (await cli(`git rev-parse --absolute-git-dir`)).stdout;
|
|
77
|
+
|
|
78
|
+
let is_rebase = false;
|
|
79
|
+
is_rebase ||= fs.existsSync(path.join(repo_root, "rebase-apply"));
|
|
80
|
+
is_rebase ||= fs.existsSync(path.join(repo_root, "rebase-merge"));
|
|
81
|
+
|
|
82
|
+
const status = is_rebase ? "prompt" : "done";
|
|
83
|
+
patch({ status });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
actions.error("Must be run from within a git repository.");
|
|
86
|
+
|
|
87
|
+
if (err instanceof Error) {
|
|
88
|
+
if (actions.isDebug()) {
|
|
89
|
+
actions.error(err.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
actions.exit(9);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Brackets } from "~/app/Brackets";
|
|
6
|
+
import { FormatText } from "~/app/FormatText";
|
|
7
|
+
import { MultiSelect } from "~/app/MultiSelect";
|
|
8
|
+
import { Parens } from "~/app/Parens";
|
|
9
|
+
import { Store } from "~/app/Store";
|
|
10
|
+
import { TextInput } from "~/app/TextInput";
|
|
11
|
+
import { colors } from "~/core/colors";
|
|
12
|
+
import { invariant } from "~/core/invariant";
|
|
13
|
+
import { short_id } from "~/core/short_id";
|
|
14
|
+
import { wrap_index } from "~/core/wrap_index";
|
|
15
|
+
|
|
16
|
+
import type { State } from "~/app/Store";
|
|
17
|
+
|
|
18
|
+
export function SelectCommitRanges() {
|
|
19
|
+
const commit_range = Store.useState((state) => state.commit_range);
|
|
20
|
+
|
|
21
|
+
invariant(commit_range, "commit_range must exist");
|
|
22
|
+
|
|
23
|
+
return <SelectCommitRangesInternal commit_range={commit_range} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Props = {
|
|
27
|
+
commit_range: CommitRange;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CommitRange = NonNullable<State["commit_range"]>;
|
|
31
|
+
type SimpleGroup = { id: string; title: string };
|
|
32
|
+
|
|
33
|
+
function SelectCommitRangesInternal(props: Props) {
|
|
34
|
+
const actions = Store.useActions();
|
|
35
|
+
|
|
36
|
+
const [selected_group_id, set_selected_group_id] = React.useState(
|
|
37
|
+
props.commit_range.UNASSIGNED
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const [group_input, set_group_input] = React.useState(false);
|
|
41
|
+
|
|
42
|
+
const [new_group_list, create_group] = React.useReducer(
|
|
43
|
+
(group_list: Array<SimpleGroup>, group: SimpleGroup) => {
|
|
44
|
+
return group_list.concat(group);
|
|
45
|
+
},
|
|
46
|
+
[]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const [commit_map, update_commit_map] = React.useReducer(
|
|
50
|
+
(
|
|
51
|
+
map: Map<string, null | string>,
|
|
52
|
+
args: { key: string; value: null | string }
|
|
53
|
+
) => {
|
|
54
|
+
map.set(args.key, args.value);
|
|
55
|
+
|
|
56
|
+
// console.debug("update_commit_map", map, args);
|
|
57
|
+
return new Map(map);
|
|
58
|
+
},
|
|
59
|
+
new Map(),
|
|
60
|
+
(map) => {
|
|
61
|
+
for (const commit of props.commit_range.commit_list) {
|
|
62
|
+
map.set(commit.sha, commit.branch_id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new Map(map);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const group_list: Array<SimpleGroup> = [];
|
|
70
|
+
|
|
71
|
+
// detect if there are unassigned commits
|
|
72
|
+
let unassigned_count = 0;
|
|
73
|
+
for (const [, group_id] of commit_map.entries()) {
|
|
74
|
+
if (group_id === null) {
|
|
75
|
+
// console.debug("unassigned commit detected", sha);
|
|
76
|
+
unassigned_count++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
group_list.push(...new_group_list);
|
|
81
|
+
|
|
82
|
+
const total_group_count =
|
|
83
|
+
new_group_list.length + props.commit_range.group_list.length;
|
|
84
|
+
|
|
85
|
+
for (const group of props.commit_range.group_list) {
|
|
86
|
+
if (group.pr?.state === "MERGED") continue;
|
|
87
|
+
|
|
88
|
+
if (group.id === props.commit_range.UNASSIGNED) {
|
|
89
|
+
// only include unassigned group when there are no other groups
|
|
90
|
+
if (total_group_count === 1) {
|
|
91
|
+
group_list.push({
|
|
92
|
+
id: group.id,
|
|
93
|
+
title: "Unassigned",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
group_list.push({
|
|
101
|
+
id: group.id,
|
|
102
|
+
title: group.pr?.title || group.id,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let current_index = group_list.findIndex((g) => g.id === selected_group_id);
|
|
107
|
+
if (current_index === -1) {
|
|
108
|
+
current_index = 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Ink.useInput((input, key) => {
|
|
112
|
+
const inputLower = input.toLowerCase();
|
|
113
|
+
|
|
114
|
+
const hasUnassignedCommits = unassigned_count > 0;
|
|
115
|
+
|
|
116
|
+
if (!hasUnassignedCommits && inputLower === "s") {
|
|
117
|
+
actions.set((state) => {
|
|
118
|
+
state.commit_map = {};
|
|
119
|
+
|
|
120
|
+
for (const [sha, id] of commit_map.entries()) {
|
|
121
|
+
if (id) {
|
|
122
|
+
const group = group_list.find((g) => g.id === id);
|
|
123
|
+
// console.debug({ sha, id, group });
|
|
124
|
+
if (group) {
|
|
125
|
+
state.commit_map[sha] = group;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switch (inputLower) {
|
|
131
|
+
case "s":
|
|
132
|
+
state.step = "manual-rebase";
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// only allow create when on unassigned group
|
|
140
|
+
if (hasUnassignedCommits && inputLower === "c") {
|
|
141
|
+
set_group_input(true);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (key.leftArrow) {
|
|
146
|
+
const new_index = wrap_index(current_index - 1, group_list);
|
|
147
|
+
const next_group = group_list[new_index];
|
|
148
|
+
return set_selected_group_id(next_group.id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (key.rightArrow) {
|
|
152
|
+
const new_index = wrap_index(current_index + 1, group_list);
|
|
153
|
+
const next_group = group_list[new_index];
|
|
154
|
+
return set_selected_group_id(next_group.id);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const group = group_list[current_index];
|
|
159
|
+
|
|
160
|
+
const items = props.commit_range.commit_list.map((commit) => {
|
|
161
|
+
const commit_metadata_id = commit_map.get(commit.sha);
|
|
162
|
+
|
|
163
|
+
const selected = commit_metadata_id !== null;
|
|
164
|
+
|
|
165
|
+
let disabled;
|
|
166
|
+
|
|
167
|
+
if (group.id === props.commit_range.UNASSIGNED) {
|
|
168
|
+
disabled = true;
|
|
169
|
+
} else {
|
|
170
|
+
disabled = Boolean(selected && commit_metadata_id !== group.id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
label: commit.subject_line,
|
|
175
|
+
value: commit,
|
|
176
|
+
selected,
|
|
177
|
+
disabled,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
items.reverse();
|
|
182
|
+
|
|
183
|
+
// <- (2/4) #742 Title A ->
|
|
184
|
+
|
|
185
|
+
const left_arrow = `${SYMBOL.left} `;
|
|
186
|
+
const right_arrow = ` ${SYMBOL.right}`;
|
|
187
|
+
const group_position = `(${current_index + 1}/${group_list.length}) `;
|
|
188
|
+
|
|
189
|
+
const max_group_label_width = 80;
|
|
190
|
+
let group_title_width = max_group_label_width;
|
|
191
|
+
group_title_width -= group_position.length;
|
|
192
|
+
group_title_width -= left_arrow.length + right_arrow.length;
|
|
193
|
+
group_title_width = Math.min(group.title.length, group_title_width);
|
|
194
|
+
|
|
195
|
+
let max_item_width = max_group_label_width;
|
|
196
|
+
max_item_width -= left_arrow.length + right_arrow.length;
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<Ink.Box flexDirection="column">
|
|
200
|
+
<Ink.Box height={1} />
|
|
201
|
+
|
|
202
|
+
<MultiSelect
|
|
203
|
+
key={group.id}
|
|
204
|
+
items={items}
|
|
205
|
+
maxWidth={max_item_width}
|
|
206
|
+
disabled={group_input}
|
|
207
|
+
onSelect={(args) => {
|
|
208
|
+
// console.debug("onSelect", args);
|
|
209
|
+
|
|
210
|
+
const key = args.item.sha;
|
|
211
|
+
|
|
212
|
+
let value;
|
|
213
|
+
if (args.selected) {
|
|
214
|
+
value = group.id;
|
|
215
|
+
} else {
|
|
216
|
+
value = null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
update_commit_map({ key, value });
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
<Ink.Box height={1} />
|
|
224
|
+
|
|
225
|
+
<Ink.Box width={max_group_label_width} flexDirection="row">
|
|
226
|
+
<Ink.Text>{left_arrow}</Ink.Text>
|
|
227
|
+
<Ink.Text>{group_position}</Ink.Text>
|
|
228
|
+
|
|
229
|
+
<Ink.Box width={group_title_width} justifyContent="center">
|
|
230
|
+
<Ink.Text wrap="truncate-end">{group.title}</Ink.Text>
|
|
231
|
+
</Ink.Box>
|
|
232
|
+
|
|
233
|
+
<Ink.Text>{right_arrow}</Ink.Text>
|
|
234
|
+
</Ink.Box>
|
|
235
|
+
|
|
236
|
+
<Ink.Box height={1} />
|
|
237
|
+
|
|
238
|
+
{unassigned_count > 0 ? (
|
|
239
|
+
<FormatText
|
|
240
|
+
wrapper={<Ink.Text color={colors.gray} />}
|
|
241
|
+
message="{count} unassigned commits, press {c} to {create} a new group"
|
|
242
|
+
values={{
|
|
243
|
+
count: (
|
|
244
|
+
<Ink.Text color={colors.yellow} bold>
|
|
245
|
+
{unassigned_count}
|
|
246
|
+
</Ink.Text>
|
|
247
|
+
),
|
|
248
|
+
c: (
|
|
249
|
+
<Ink.Text bold color={colors.green}>
|
|
250
|
+
c
|
|
251
|
+
</Ink.Text>
|
|
252
|
+
),
|
|
253
|
+
create: (
|
|
254
|
+
<Ink.Text bold color={colors.green}>
|
|
255
|
+
<Parens>c</Parens>reate
|
|
256
|
+
</Ink.Text>
|
|
257
|
+
),
|
|
258
|
+
}}
|
|
259
|
+
/>
|
|
260
|
+
) : (
|
|
261
|
+
<React.Fragment>
|
|
262
|
+
<FormatText
|
|
263
|
+
wrapper={<Ink.Text />}
|
|
264
|
+
message="🎉 Done! Press {s} to {sync} the commits to Github"
|
|
265
|
+
values={{
|
|
266
|
+
s: (
|
|
267
|
+
<Ink.Text bold color={colors.green}>
|
|
268
|
+
s
|
|
269
|
+
</Ink.Text>
|
|
270
|
+
),
|
|
271
|
+
sync: (
|
|
272
|
+
<Ink.Text bold color={colors.green}>
|
|
273
|
+
<Parens>s</Parens>ync
|
|
274
|
+
</Ink.Text>
|
|
275
|
+
),
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
</React.Fragment>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{!group_input ? null : (
|
|
282
|
+
<React.Fragment>
|
|
283
|
+
<Ink.Box height={1} />
|
|
284
|
+
|
|
285
|
+
<FormatText
|
|
286
|
+
wrapper={<Ink.Text color={colors.gray} />}
|
|
287
|
+
message="Enter a title for the PR {note}"
|
|
288
|
+
values={{
|
|
289
|
+
note: (
|
|
290
|
+
<Parens>
|
|
291
|
+
<FormatText
|
|
292
|
+
message="press {enter} to submit"
|
|
293
|
+
values={{
|
|
294
|
+
enter: (
|
|
295
|
+
<Ink.Text bold color={colors.green}>
|
|
296
|
+
{SYMBOL.enter}
|
|
297
|
+
</Ink.Text>
|
|
298
|
+
),
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
</Parens>
|
|
302
|
+
),
|
|
303
|
+
}}
|
|
304
|
+
/>
|
|
305
|
+
|
|
306
|
+
<TextInput onSubmit={submit_group_input} />
|
|
307
|
+
|
|
308
|
+
<Ink.Box height={1} />
|
|
309
|
+
</React.Fragment>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<Ink.Box>
|
|
313
|
+
<FormatText
|
|
314
|
+
wrapper={<Ink.Text color={colors.gray} />}
|
|
315
|
+
message="Press {left} and {right} to view PR groups"
|
|
316
|
+
values={{
|
|
317
|
+
left: (
|
|
318
|
+
<Ink.Text bold color={colors.green}>
|
|
319
|
+
{SYMBOL.left}
|
|
320
|
+
</Ink.Text>
|
|
321
|
+
),
|
|
322
|
+
right: (
|
|
323
|
+
<Ink.Text bold color={colors.green}>
|
|
324
|
+
{SYMBOL.right}
|
|
325
|
+
</Ink.Text>
|
|
326
|
+
),
|
|
327
|
+
}}
|
|
328
|
+
/>
|
|
329
|
+
</Ink.Box>
|
|
330
|
+
|
|
331
|
+
<Ink.Box>
|
|
332
|
+
<FormatText
|
|
333
|
+
wrapper={<Ink.Text color={colors.gray} />}
|
|
334
|
+
message="Press {enter} to toggle commit selection"
|
|
335
|
+
values={{
|
|
336
|
+
enter: (
|
|
337
|
+
<Ink.Text bold color={colors.green}>
|
|
338
|
+
{SYMBOL.enter}
|
|
339
|
+
</Ink.Text>
|
|
340
|
+
),
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
</Ink.Box>
|
|
344
|
+
</Ink.Box>
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
function submit_group_input(title: string) {
|
|
348
|
+
const id = short_id();
|
|
349
|
+
|
|
350
|
+
actions.output(
|
|
351
|
+
<FormatText
|
|
352
|
+
wrapper={<Ink.Text dimColor />}
|
|
353
|
+
message="Created new group {group} {note}"
|
|
354
|
+
values={{
|
|
355
|
+
group: <Brackets>{title}</Brackets>,
|
|
356
|
+
note: <Parens>{id}</Parens>,
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// console.debug("submit_group_input", { title, id });
|
|
362
|
+
create_group({ id, title });
|
|
363
|
+
set_selected_group_id(id);
|
|
364
|
+
set_group_input(false);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const SYMBOL = {
|
|
369
|
+
left: "←",
|
|
370
|
+
right: "→",
|
|
371
|
+
enter: "Enter",
|
|
372
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
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 { colors } from "~/core/colors";
|
|
9
|
+
import { invariant } from "~/core/invariant";
|
|
10
|
+
|
|
11
|
+
import type { Argv } from "~/command";
|
|
12
|
+
|
|
13
|
+
export function Status() {
|
|
14
|
+
const argv = Store.useState((state) => state.argv);
|
|
15
|
+
invariant(argv, "argv must exist");
|
|
16
|
+
|
|
17
|
+
return <Await fallback={null} function={() => run({ argv })} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type Args = {
|
|
21
|
+
argv: Argv;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function run(args: Args) {
|
|
25
|
+
const actions = Store.getState().actions;
|
|
26
|
+
const commit_range = Store.getState().commit_range;
|
|
27
|
+
|
|
28
|
+
invariant(commit_range, "commit_range must exist");
|
|
29
|
+
|
|
30
|
+
actions.output(<StatusTable />);
|
|
31
|
+
|
|
32
|
+
let needs_rebase = false;
|
|
33
|
+
let needs_update = false;
|
|
34
|
+
|
|
35
|
+
for (const group of commit_range.group_list) {
|
|
36
|
+
if (group.dirty) {
|
|
37
|
+
needs_update = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (group.pr?.state === "MERGED") {
|
|
41
|
+
needs_rebase = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < commit_range.commit_list.length; i++) {
|
|
46
|
+
const commit = commit_range.commit_list[i];
|
|
47
|
+
const commit_pr = commit_range.pr_lookup[commit.branch_id || ""];
|
|
48
|
+
|
|
49
|
+
if (commit.branch_id && !commit_pr) {
|
|
50
|
+
needs_rebase = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (args.argv.check) {
|
|
55
|
+
actions.exit(0);
|
|
56
|
+
} else if (needs_rebase) {
|
|
57
|
+
Store.setState((state) => {
|
|
58
|
+
state.step = "pre-local-merge-rebase";
|
|
59
|
+
});
|
|
60
|
+
} else if (needs_update) {
|
|
61
|
+
Store.setState((state) => {
|
|
62
|
+
state.step = "pre-select-commit-ranges";
|
|
63
|
+
});
|
|
64
|
+
} else if (args.argv.force) {
|
|
65
|
+
Store.setState((state) => {
|
|
66
|
+
state.step = "select-commit-ranges";
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
actions.output(<Ink.Text>✅ Everything up to date.</Ink.Text>);
|
|
70
|
+
actions.output(
|
|
71
|
+
<Ink.Text color={colors.gray}>
|
|
72
|
+
<Ink.Text>Run with</Ink.Text>
|
|
73
|
+
<Ink.Text bold color={colors.yellow}>
|
|
74
|
+
{` --force `}
|
|
75
|
+
</Ink.Text>
|
|
76
|
+
<Ink.Text>to force update all pull requests.</Ink.Text>
|
|
77
|
+
</Ink.Text>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
actions.exit(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Store } from "~/app/Store";
|
|
6
|
+
import { Table } from "~/app/Table";
|
|
7
|
+
import { Url } from "~/app/Url";
|
|
8
|
+
import { assertNever } from "~/core/assertNever";
|
|
9
|
+
import { colors } from "~/core/colors";
|
|
10
|
+
import { invariant } from "~/core/invariant";
|
|
11
|
+
|
|
12
|
+
import type { TableColumnProps } from "~/app/Table";
|
|
13
|
+
|
|
14
|
+
export function StatusTable() {
|
|
15
|
+
const commit_range = Store.useState((state) => state.commit_range);
|
|
16
|
+
|
|
17
|
+
invariant(commit_range, "commit_range must exist");
|
|
18
|
+
|
|
19
|
+
const row_list = [];
|
|
20
|
+
|
|
21
|
+
for (const group of commit_range.group_list) {
|
|
22
|
+
const row: Row = {
|
|
23
|
+
count: "",
|
|
24
|
+
status: "NEW",
|
|
25
|
+
title: "",
|
|
26
|
+
url: "",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (group.id === commit_range.UNASSIGNED) {
|
|
30
|
+
row.status = "NEW";
|
|
31
|
+
row.title = "Unassigned";
|
|
32
|
+
row.count = `0/${group.commits.length}`;
|
|
33
|
+
row.url = "";
|
|
34
|
+
} else {
|
|
35
|
+
if (group.dirty) {
|
|
36
|
+
row.status = "OUTDATED";
|
|
37
|
+
} else {
|
|
38
|
+
row.status = "SYNCED";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (group.pr) {
|
|
42
|
+
if (group.pr.state === "MERGED") {
|
|
43
|
+
row.status = "MERGED";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
row.title = group.pr.title;
|
|
47
|
+
row.count = `${group.pr.commits.length}/${group.commits.length}`;
|
|
48
|
+
row.url = group.pr.url;
|
|
49
|
+
} else {
|
|
50
|
+
row.title = group.title || group.id;
|
|
51
|
+
row.count = `0/${group.commits.length}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
row_list.push(row);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Table
|
|
60
|
+
data={row_list}
|
|
61
|
+
fillColumn="title"
|
|
62
|
+
maxWidth={{
|
|
63
|
+
status: (v) => v + 2,
|
|
64
|
+
}}
|
|
65
|
+
columnGap={3}
|
|
66
|
+
columns={{
|
|
67
|
+
status: StatusColumn,
|
|
68
|
+
count: CountColumn,
|
|
69
|
+
title: TitleColumn,
|
|
70
|
+
url: UrlColumn,
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Row = {
|
|
77
|
+
status: "NEW" | "OUTDATED" | "MERGED" | "SYNCED";
|
|
78
|
+
count: string;
|
|
79
|
+
title: string;
|
|
80
|
+
url: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function StatusColumn(props: TableColumnProps<Row>) {
|
|
84
|
+
const value = props.row[props.column];
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Ink.Text
|
|
88
|
+
color={get_status_color(props.row)}
|
|
89
|
+
bold={get_status_bold(props.row)}
|
|
90
|
+
>
|
|
91
|
+
{get_status_icon(props.row)} {value}
|
|
92
|
+
</Ink.Text>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CountColumn(props: TableColumnProps<Row>) {
|
|
97
|
+
const value = props.row[props.column];
|
|
98
|
+
|
|
99
|
+
return <Ink.Text dimColor>{value}</Ink.Text>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function TitleColumn(props: TableColumnProps<Row>) {
|
|
103
|
+
const value = props.row[props.column];
|
|
104
|
+
|
|
105
|
+
return <Ink.Text wrap="truncate-end">{value}</Ink.Text>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function UrlColumn(props: TableColumnProps<Row>) {
|
|
109
|
+
const value = props.row[props.column];
|
|
110
|
+
|
|
111
|
+
return <Url dimColor>{value}</Url>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function get_status_icon(row: Row) {
|
|
115
|
+
switch (row.status) {
|
|
116
|
+
case "NEW":
|
|
117
|
+
return "⭑";
|
|
118
|
+
case "OUTDATED":
|
|
119
|
+
return "!";
|
|
120
|
+
case "MERGED":
|
|
121
|
+
return "↗";
|
|
122
|
+
case "SYNCED":
|
|
123
|
+
return "✔";
|
|
124
|
+
default:
|
|
125
|
+
assertNever(row.status);
|
|
126
|
+
return "?";
|
|
127
|
+
// unicode question mark in box
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function get_status_color(row: Row) {
|
|
132
|
+
switch (row.status) {
|
|
133
|
+
case "NEW":
|
|
134
|
+
return colors.yellow;
|
|
135
|
+
case "OUTDATED":
|
|
136
|
+
return colors.red;
|
|
137
|
+
case "MERGED":
|
|
138
|
+
return colors.purple;
|
|
139
|
+
case "SYNCED":
|
|
140
|
+
return colors.green;
|
|
141
|
+
default:
|
|
142
|
+
assertNever(row.status);
|
|
143
|
+
return colors.gray;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function get_status_bold(row: Row) {
|
|
148
|
+
switch (row.status) {
|
|
149
|
+
case "NEW":
|
|
150
|
+
case "OUTDATED":
|
|
151
|
+
return true;
|
|
152
|
+
default:
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|