git-stack-cli 1.13.0 → 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +527 -373
- package/package.json +1 -1
- package/src/app/Main.tsx +4 -0
- package/src/app/ManualRebase.tsx +78 -281
- package/src/app/Store.tsx +8 -0
- package/src/app/SyncGithub.tsx +338 -0
- package/src/commands/Rebase.tsx +29 -6
- package/src/core/github.tsx +8 -1
- package/src/index.tsx +4 -1
package/package.json
CHANGED
package/src/app/Main.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { PreSelectCommitRanges } from "~/app/PreSelectCommitRanges";
|
|
|
10
10
|
import { SelectCommitRanges } from "~/app/SelectCommitRanges";
|
|
11
11
|
import { Status } from "~/app/Status";
|
|
12
12
|
import { Store } from "~/app/Store";
|
|
13
|
+
import { SyncGithub } from "~/app/SyncGithub";
|
|
13
14
|
import { assertNever } from "~/core/assertNever";
|
|
14
15
|
|
|
15
16
|
export function Main() {
|
|
@@ -43,6 +44,9 @@ export function Main() {
|
|
|
43
44
|
case "manual-rebase":
|
|
44
45
|
return <ManualRebase />;
|
|
45
46
|
|
|
47
|
+
case "sync-github":
|
|
48
|
+
return <SyncGithub />;
|
|
49
|
+
|
|
46
50
|
case "post-rebase-status":
|
|
47
51
|
return <PostRebaseStatus />;
|
|
48
52
|
|
package/src/app/ManualRebase.tsx
CHANGED
|
@@ -6,27 +6,44 @@ import * as Ink from "ink-cjs";
|
|
|
6
6
|
|
|
7
7
|
import { Await } from "~/app/Await";
|
|
8
8
|
import { Brackets } from "~/app/Brackets";
|
|
9
|
-
import { FormatText } from "~/app/FormatText";
|
|
10
9
|
import { Store } from "~/app/Store";
|
|
11
10
|
import * as CommitMetadata from "~/core/CommitMetadata";
|
|
12
11
|
import { GitReviseTodo } from "~/core/GitReviseTodo";
|
|
13
|
-
import * as StackSummaryTable from "~/core/StackSummaryTable";
|
|
14
12
|
import { cli } from "~/core/cli";
|
|
15
13
|
import { colors } from "~/core/colors";
|
|
16
|
-
import * as github from "~/core/github";
|
|
17
14
|
import { invariant } from "~/core/invariant";
|
|
18
15
|
import { short_id } from "~/core/short_id";
|
|
19
16
|
|
|
20
17
|
export function ManualRebase() {
|
|
18
|
+
const abort_handler = React.useRef(() => {});
|
|
19
|
+
|
|
20
|
+
React.useEffect(function listen_sigint() {
|
|
21
|
+
process.once("SIGINT", sigint_handler);
|
|
22
|
+
|
|
23
|
+
return function cleanup() {
|
|
24
|
+
process.removeListener("SIGINT", sigint_handler);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function sigint_handler() {
|
|
28
|
+
abort_handler.current();
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
21
32
|
return (
|
|
22
33
|
<Await
|
|
23
34
|
fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
|
|
24
|
-
function={
|
|
35
|
+
function={async function () {
|
|
36
|
+
await run({ abort_handler });
|
|
37
|
+
}}
|
|
25
38
|
/>
|
|
26
39
|
);
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
type Args = {
|
|
43
|
+
abort_handler: React.MutableRefObject<() => void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async function run(args: Args) {
|
|
30
47
|
const state = Store.getState();
|
|
31
48
|
const actions = state.actions;
|
|
32
49
|
const argv = state.argv;
|
|
@@ -41,77 +58,70 @@ async function run() {
|
|
|
41
58
|
invariant(repo_root, "repo_root must exist");
|
|
42
59
|
|
|
43
60
|
// always listen for SIGINT event and restore git state
|
|
44
|
-
|
|
61
|
+
args.abort_handler.current = function sigint_handler() {
|
|
62
|
+
actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
|
|
63
|
+
handle_exit(15);
|
|
64
|
+
};
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
|
|
66
|
+
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
48
67
|
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
try {
|
|
69
|
+
// get latest merge_base relative to local master
|
|
70
|
+
const merge_base = (await cli(`git merge-base HEAD ${master_branch}`))
|
|
71
|
+
.stdout;
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
// immediately paint all commit to preserve selected commit ranges
|
|
74
|
+
let commit_range = await CommitMetadata.range(commit_map);
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
commit.branch_id = group_from_map.id;
|
|
58
|
-
commit.title = group_from_map.title;
|
|
59
|
-
}
|
|
76
|
+
// reverse group list to ensure we create git revise in correct order
|
|
77
|
+
commit_range.group_list.reverse();
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
for (const commit of commit_range.commit_list) {
|
|
80
|
+
const group_from_map = commit_map[commit.sha];
|
|
81
|
+
commit.branch_id = group_from_map.id;
|
|
82
|
+
commit.title = group_from_map.title;
|
|
83
|
+
}
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
await GitReviseTodo.execute({
|
|
86
|
+
rebase_group_index: 0,
|
|
87
|
+
rebase_merge_base: merge_base,
|
|
88
|
+
commit_range,
|
|
89
|
+
});
|
|
71
90
|
|
|
72
|
-
|
|
91
|
+
commit_range = await CommitMetadata.range(commit_map);
|
|
73
92
|
|
|
74
|
-
|
|
93
|
+
// reverse commit list so that we can cherry-pick in order
|
|
94
|
+
commit_range.group_list.reverse();
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
let rebase_merge_base = merge_base;
|
|
97
|
+
let rebase_group_index = 0;
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
100
|
+
const group = commit_range.group_list[i];
|
|
81
101
|
|
|
82
|
-
|
|
83
|
-
|
|
102
|
+
if (!group.dirty) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
if (i > 0) {
|
|
107
|
+
const prev_group = commit_range.group_list[i - 1];
|
|
108
|
+
const prev_commit = prev_group.commits[prev_group.commits.length - 1];
|
|
109
|
+
rebase_merge_base = prev_commit.sha;
|
|
110
|
+
rebase_group_index = i;
|
|
111
|
+
}
|
|
88
112
|
|
|
89
|
-
|
|
90
|
-
const prev_group = commit_range.group_list[i - 1];
|
|
91
|
-
const prev_commit = prev_group.commits[prev_group.commits.length - 1];
|
|
92
|
-
rebase_merge_base = prev_commit.sha;
|
|
93
|
-
rebase_group_index = i;
|
|
113
|
+
break;
|
|
94
114
|
}
|
|
95
115
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
|
|
100
|
-
actions.debug(`rebase_group_index = ${rebase_group_index}`);
|
|
116
|
+
actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
|
|
117
|
+
actions.debug(`rebase_group_index = ${rebase_group_index}`);
|
|
101
118
|
|
|
102
|
-
|
|
119
|
+
// actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
|
|
103
120
|
|
|
104
|
-
try {
|
|
105
121
|
// must perform rebase from repo root for applying git patch
|
|
106
122
|
process.chdir(repo_root);
|
|
107
123
|
await cli(`pwd`);
|
|
108
124
|
|
|
109
|
-
actions.output(
|
|
110
|
-
<Ink.Text color={colors.yellow} wrap="truncate-end">
|
|
111
|
-
Rebasing…
|
|
112
|
-
</Ink.Text>
|
|
113
|
-
);
|
|
114
|
-
|
|
115
125
|
// create temporary branch
|
|
116
126
|
await cli(`git checkout -b ${temp_branch_name}`);
|
|
117
127
|
|
|
@@ -125,15 +135,18 @@ async function run() {
|
|
|
125
135
|
// of original branch to the newly created temporary branch
|
|
126
136
|
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
127
137
|
|
|
128
|
-
if (argv.sync) {
|
|
129
|
-
await sync_github();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
138
|
restore_git();
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
state
|
|
136
|
-
|
|
140
|
+
if (argv.sync) {
|
|
141
|
+
actions.set((state) => {
|
|
142
|
+
state.step = "sync-github";
|
|
143
|
+
state.sync_github = { commit_range, rebase_group_index };
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
actions.set((state) => {
|
|
147
|
+
state.step = "post-rebase-status";
|
|
148
|
+
});
|
|
149
|
+
}
|
|
137
150
|
} catch (err) {
|
|
138
151
|
if (err instanceof Error) {
|
|
139
152
|
actions.error(err.message);
|
|
@@ -144,220 +157,7 @@ async function run() {
|
|
|
144
157
|
actions.error("Try again with `--verbose` to see more information.");
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
handle_exit();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function sync_github() {
|
|
151
|
-
// in order to sync we walk from rebase_group_index to HEAD
|
|
152
|
-
// checking out each group and syncing to github
|
|
153
|
-
|
|
154
|
-
// start from HEAD and work backward to rebase_group_index
|
|
155
|
-
const push_group_list = [];
|
|
156
|
-
let lookback_index = 0;
|
|
157
|
-
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
158
|
-
const index = commit_range.group_list.length - 1 - i;
|
|
159
|
-
|
|
160
|
-
// do not go past rebase_group_index
|
|
161
|
-
if (index < rebase_group_index) {
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const group = commit_range.group_list[index];
|
|
166
|
-
// console.debug({ i, index, group });
|
|
167
|
-
|
|
168
|
-
if (i > 0) {
|
|
169
|
-
const prev_group = commit_range.group_list[index + 1];
|
|
170
|
-
lookback_index += prev_group.commits.length;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// console.debug(`git show head~${lookback_index}`);
|
|
174
|
-
|
|
175
|
-
// push group and lookback_index onto front of push_group_list
|
|
176
|
-
push_group_list.unshift({ group, lookback_index });
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
actions.output(
|
|
180
|
-
<FormatText
|
|
181
|
-
wrapper={<Ink.Text color={colors.yellow} wrap="truncate-end" />}
|
|
182
|
-
message="Syncing {group_list}…"
|
|
183
|
-
values={{
|
|
184
|
-
group_list: (
|
|
185
|
-
<React.Fragment>
|
|
186
|
-
{push_group_list.map((push_group) => {
|
|
187
|
-
const group = push_group.group;
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<Brackets key={group.id}>
|
|
191
|
-
{group.pr?.title || group.title || group.id}
|
|
192
|
-
</Brackets>
|
|
193
|
-
);
|
|
194
|
-
})}
|
|
195
|
-
</React.Fragment>
|
|
196
|
-
),
|
|
197
|
-
}}
|
|
198
|
-
/>
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
// for all push targets in push_group_list
|
|
202
|
-
// things that can be done in parallel are grouped by numbers
|
|
203
|
-
//
|
|
204
|
-
// -----------------------------------
|
|
205
|
-
// 1 (before_push) temp mark draft
|
|
206
|
-
// --------------------------------------
|
|
207
|
-
// 2 push simultaneously to github
|
|
208
|
-
// --------------------------------------
|
|
209
|
-
// 2 create PR / edit PR
|
|
210
|
-
// 2 (after_push) undo temp mark draft
|
|
211
|
-
// --------------------------------------
|
|
212
|
-
|
|
213
|
-
const before_push_tasks = [];
|
|
214
|
-
for (const push_group of push_group_list) {
|
|
215
|
-
before_push_tasks.push(before_push(push_group));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
await Promise.all(before_push_tasks);
|
|
219
|
-
|
|
220
|
-
const push_target_list = push_group_list.map((push_group) => {
|
|
221
|
-
return `HEAD~${push_group.lookback_index}:${push_group.group.id}`;
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const push_target_args = push_target_list.join(" ");
|
|
225
|
-
|
|
226
|
-
const git_push_command = [`git push -f origin ${push_target_args}`];
|
|
227
|
-
|
|
228
|
-
if (argv.verify === false) {
|
|
229
|
-
git_push_command.push("--no-verify");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
await cli(git_push_command);
|
|
233
|
-
|
|
234
|
-
const pr_url_list = commit_range.group_list.map(get_group_url);
|
|
235
|
-
|
|
236
|
-
const after_push_tasks = [];
|
|
237
|
-
for (const push_group of push_group_list) {
|
|
238
|
-
const group = push_group.group;
|
|
239
|
-
after_push_tasks.push(after_push({ group, pr_url_list }));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
await Promise.all(after_push_tasks);
|
|
243
|
-
|
|
244
|
-
// finally, ensure all prs have the updated stack table from updated pr_url_list
|
|
245
|
-
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
246
|
-
const group = commit_range.group_list[i];
|
|
247
|
-
|
|
248
|
-
// use the updated pr_url_list to get the actual selected_url
|
|
249
|
-
const selected_url = pr_url_list[i];
|
|
250
|
-
|
|
251
|
-
invariant(group.base, "group.base must exist");
|
|
252
|
-
|
|
253
|
-
const body = group.pr?.body || DEFAULT_PR_BODY;
|
|
254
|
-
|
|
255
|
-
const update_body = StackSummaryTable.write({
|
|
256
|
-
body,
|
|
257
|
-
pr_url_list,
|
|
258
|
-
selected_url,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (update_body === body) {
|
|
262
|
-
actions.debug(`Skipping body update for ${selected_url}`);
|
|
263
|
-
} else {
|
|
264
|
-
actions.debug(`Update body for ${selected_url}`);
|
|
265
|
-
|
|
266
|
-
await github.pr_edit({
|
|
267
|
-
branch: group.id,
|
|
268
|
-
base: group.base,
|
|
269
|
-
body: update_body,
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function before_push(args: { group: CommitMetadataGroup }) {
|
|
276
|
-
const { group } = args;
|
|
277
|
-
|
|
278
|
-
invariant(group.base, "group.base must exist");
|
|
279
|
-
|
|
280
|
-
// we may temporarily mark PR as a draft before editing it
|
|
281
|
-
// if it is not already a draft PR, to avoid notification spam
|
|
282
|
-
let is_temp_draft = !group.pr?.isDraft;
|
|
283
|
-
|
|
284
|
-
// before pushing reset base to master temporarily
|
|
285
|
-
// avoid accidentally pointing to orphaned parent commit
|
|
286
|
-
// should hopefully fix issues where a PR includes a bunch of commits after pushing
|
|
287
|
-
if (group.pr) {
|
|
288
|
-
if (!group.pr.isDraft) {
|
|
289
|
-
is_temp_draft = true;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (is_temp_draft) {
|
|
293
|
-
await github.pr_draft({
|
|
294
|
-
branch: group.id,
|
|
295
|
-
draft: true,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
await github.pr_edit({
|
|
300
|
-
branch: group.id,
|
|
301
|
-
base: master_branch,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
async function after_push(args: {
|
|
307
|
-
group: CommitMetadataGroup;
|
|
308
|
-
pr_url_list: Array<string>;
|
|
309
|
-
}) {
|
|
310
|
-
const { group, pr_url_list } = args;
|
|
311
|
-
|
|
312
|
-
invariant(group.base, "group.base must exist");
|
|
313
|
-
|
|
314
|
-
const selected_url = get_group_url(group);
|
|
315
|
-
|
|
316
|
-
if (group.pr) {
|
|
317
|
-
// ensure base matches pr in github
|
|
318
|
-
await github.pr_edit({
|
|
319
|
-
branch: group.id,
|
|
320
|
-
base: group.base,
|
|
321
|
-
body: StackSummaryTable.write({
|
|
322
|
-
body: group.pr.body,
|
|
323
|
-
pr_url_list,
|
|
324
|
-
selected_url,
|
|
325
|
-
}),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// we may temporarily mark PR as a draft before editing it
|
|
329
|
-
// if it is not already a draft PR, to avoid notification spam
|
|
330
|
-
let is_temp_draft = !group.pr?.isDraft;
|
|
331
|
-
|
|
332
|
-
if (is_temp_draft) {
|
|
333
|
-
// mark pr as ready for review again
|
|
334
|
-
await github.pr_draft({
|
|
335
|
-
branch: group.id,
|
|
336
|
-
draft: false,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
// create pr in github
|
|
341
|
-
const pr_url = await github.pr_create({
|
|
342
|
-
branch: group.id,
|
|
343
|
-
base: group.base,
|
|
344
|
-
title: group.title,
|
|
345
|
-
body: DEFAULT_PR_BODY,
|
|
346
|
-
draft: argv.draft,
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
if (!pr_url) {
|
|
350
|
-
throw new Error("unable to create pr");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// update pr_url_list with created pr_url
|
|
354
|
-
for (let i = 0; i < pr_url_list.length; i++) {
|
|
355
|
-
const url = pr_url_list[i];
|
|
356
|
-
if (url === selected_url) {
|
|
357
|
-
pr_url_list[i] = pr_url;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
160
|
+
handle_exit(16);
|
|
361
161
|
}
|
|
362
162
|
|
|
363
163
|
// cleanup git operations if cancelled during manual rebase
|
|
@@ -385,7 +185,7 @@ async function run() {
|
|
|
385
185
|
cli.sync(`pwd`, spawn_options);
|
|
386
186
|
}
|
|
387
187
|
|
|
388
|
-
function handle_exit() {
|
|
188
|
+
function handle_exit(code: number) {
|
|
389
189
|
actions.output(
|
|
390
190
|
<Ink.Text color={colors.yellow}>
|
|
391
191
|
Restoring <Brackets>{branch_name}</Brackets>…
|
|
@@ -400,9 +200,6 @@ async function run() {
|
|
|
400
200
|
</Ink.Text>
|
|
401
201
|
);
|
|
402
202
|
|
|
403
|
-
actions.exit(
|
|
203
|
+
actions.exit(code);
|
|
404
204
|
}
|
|
405
205
|
}
|
|
406
|
-
|
|
407
|
-
type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
|
|
408
|
-
const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
|
package/src/app/Store.tsx
CHANGED
|
@@ -24,6 +24,11 @@ type MutateOutputArgs = {
|
|
|
24
24
|
withoutTimestamp?: boolean;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
type SyncGithubState = {
|
|
28
|
+
commit_range: CommitMetadata.CommitRange;
|
|
29
|
+
rebase_group_index: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
27
32
|
export type State = {
|
|
28
33
|
// set immediately in `index.tsx` so no `null` scenario
|
|
29
34
|
process_argv: Array<string>;
|
|
@@ -41,6 +46,7 @@ export type State = {
|
|
|
41
46
|
commit_map: null | CommitMap;
|
|
42
47
|
pr_templates: Array<string>;
|
|
43
48
|
pr_template_body: null | string;
|
|
49
|
+
sync_github: null | SyncGithubState;
|
|
44
50
|
|
|
45
51
|
step:
|
|
46
52
|
| "github-api-error"
|
|
@@ -52,6 +58,7 @@ export type State = {
|
|
|
52
58
|
| "select-commit-ranges"
|
|
53
59
|
| "pre-manual-rebase"
|
|
54
60
|
| "manual-rebase"
|
|
61
|
+
| "sync-github"
|
|
55
62
|
| "post-rebase-status";
|
|
56
63
|
|
|
57
64
|
output: Array<React.ReactNode>;
|
|
@@ -105,6 +112,7 @@ const BaseStore = createStore<State>()(
|
|
|
105
112
|
commit_map: null,
|
|
106
113
|
pr_templates: [],
|
|
107
114
|
pr_template_body: null,
|
|
115
|
+
sync_github: null,
|
|
108
116
|
|
|
109
117
|
step: "loading",
|
|
110
118
|
|