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
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
import last from "lodash/last";
|
|
5
|
+
|
|
6
|
+
import { Await } from "~/app/Await";
|
|
7
|
+
import { Store } from "~/app/Store";
|
|
8
|
+
import * as StackSummaryTable from "~/core/StackSummaryTable";
|
|
9
|
+
import { cli } from "~/core/cli";
|
|
10
|
+
import { colors } from "~/core/colors";
|
|
11
|
+
import * as github from "~/core/github";
|
|
12
|
+
import { invariant } from "~/core/invariant";
|
|
13
|
+
|
|
14
|
+
import type * as CommitMetadata from "~/core/CommitMetadata";
|
|
15
|
+
|
|
16
|
+
export function SyncGithub() {
|
|
17
|
+
const abort_handler = React.useRef(() => {});
|
|
18
|
+
|
|
19
|
+
React.useEffect(function listen_sigint() {
|
|
20
|
+
process.once("SIGINT", sigint_handler);
|
|
21
|
+
|
|
22
|
+
return function cleanup() {
|
|
23
|
+
process.removeListener("SIGINT", sigint_handler);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function sigint_handler() {
|
|
27
|
+
abort_handler.current();
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Await
|
|
33
|
+
fallback={<Ink.Text color={colors.yellow}>Syncing…</Ink.Text>}
|
|
34
|
+
function={async function () {
|
|
35
|
+
await run({ abort_handler });
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Args = {
|
|
42
|
+
abort_handler: React.MutableRefObject<() => void>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
async function run(args: Args) {
|
|
46
|
+
const state = Store.getState();
|
|
47
|
+
const actions = state.actions;
|
|
48
|
+
const argv = state.argv;
|
|
49
|
+
const branch_name = state.branch_name;
|
|
50
|
+
const commit_map = state.commit_map;
|
|
51
|
+
const master_branch = state.master_branch;
|
|
52
|
+
const repo_root = state.repo_root;
|
|
53
|
+
const sync_github = state.sync_github;
|
|
54
|
+
|
|
55
|
+
invariant(branch_name, "branch_name must exist");
|
|
56
|
+
invariant(commit_map, "commit_map must exist");
|
|
57
|
+
invariant(repo_root, "repo_root must exist");
|
|
58
|
+
invariant(sync_github, "sync_github must exist");
|
|
59
|
+
|
|
60
|
+
const commit_range = sync_github.commit_range;
|
|
61
|
+
const rebase_group_index = sync_github.rebase_group_index;
|
|
62
|
+
|
|
63
|
+
// always listen for SIGINT event and restore pr state
|
|
64
|
+
args.abort_handler.current = function sigint_handler() {
|
|
65
|
+
actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
|
|
66
|
+
handle_exit(17);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
let DEFAULT_PR_BODY = "";
|
|
70
|
+
if (state.pr_template_body) {
|
|
71
|
+
DEFAULT_PR_BODY = state.pr_template_body;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const push_group_list = get_push_group_list();
|
|
75
|
+
|
|
76
|
+
// for all push targets in push_group_list
|
|
77
|
+
// things that can be done in parallel are grouped by numbers
|
|
78
|
+
//
|
|
79
|
+
// -----------------------------------
|
|
80
|
+
// 1 (before_push) temp mark draft
|
|
81
|
+
// --------------------------------------
|
|
82
|
+
// 2 push simultaneously to github
|
|
83
|
+
// --------------------------------------
|
|
84
|
+
// 2 create PR / edit PR
|
|
85
|
+
// 2 (after_push) undo temp mark draft
|
|
86
|
+
// --------------------------------------
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const before_push_tasks = [];
|
|
90
|
+
for (const group of push_group_list) {
|
|
91
|
+
before_push_tasks.push(before_push({ group }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await Promise.all(before_push_tasks);
|
|
95
|
+
|
|
96
|
+
const git_push_command = [`git push -f origin`];
|
|
97
|
+
|
|
98
|
+
if (argv.verify === false) {
|
|
99
|
+
git_push_command.push("--no-verify");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const group of push_group_list) {
|
|
103
|
+
const last_commit = last(group.commits);
|
|
104
|
+
invariant(last_commit, "last_commit must exist");
|
|
105
|
+
|
|
106
|
+
// explicit refs/heads head branch to avoid push failing
|
|
107
|
+
//
|
|
108
|
+
// ❯ git push -f origin --no-verify f6e249051b4820a03deb957ddebc19acfd7dfd7c:gs-ED2etrzv2
|
|
109
|
+
// error: The destination you provided is not a full refname (i.e.,
|
|
110
|
+
// starting with "refs/"). We tried to guess what you meant by:
|
|
111
|
+
//
|
|
112
|
+
// - Looking for a ref that matches 'gs-ED2etrzv2' on the remote side.
|
|
113
|
+
// - Checking if the <src> being pushed ('f6e249051b4820a03deb957ddebc19acfd7dfd7c')
|
|
114
|
+
// is a ref in "refs/{heads,tags}/". If so we add a corresponding
|
|
115
|
+
// refs/{heads,tags}/ prefix on the remote side.
|
|
116
|
+
//
|
|
117
|
+
// Neither worked, so we gave up. You must fully qualify the ref.
|
|
118
|
+
// hint: The <src> part of the refspec is a commit object.
|
|
119
|
+
// hint: Did you mean to create a new branch by pushing to
|
|
120
|
+
// hint: 'f6e249051b4820a03deb957ddebc19acfd7dfd7c:refs/heads/gs-ED2etrzv2'?
|
|
121
|
+
// error: failed to push some refs to 'github.com:magus/git-multi-diff-playground.git'
|
|
122
|
+
//
|
|
123
|
+
const target = `${last_commit.sha}:refs/heads/${group.id}`;
|
|
124
|
+
git_push_command.push(target);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await cli(git_push_command);
|
|
128
|
+
|
|
129
|
+
const pr_url_list = commit_range.group_list.map(get_group_url);
|
|
130
|
+
|
|
131
|
+
const after_push_tasks = [];
|
|
132
|
+
for (const group of push_group_list) {
|
|
133
|
+
after_push_tasks.push(after_push({ group, pr_url_list }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await Promise.all(after_push_tasks);
|
|
137
|
+
|
|
138
|
+
// finally, ensure all prs have the updated stack table from updated pr_url_list
|
|
139
|
+
// this step must come after the after_push since that step may create new PRs
|
|
140
|
+
// we need the urls for all prs at this step so we run it after the after_push
|
|
141
|
+
const update_pr_body_tasks = [];
|
|
142
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
143
|
+
const group = commit_range.group_list[i];
|
|
144
|
+
|
|
145
|
+
// use the updated pr_url_list to get the actual selected_url
|
|
146
|
+
const selected_url = pr_url_list[i];
|
|
147
|
+
|
|
148
|
+
const task = update_pr_body({ group, selected_url, pr_url_list });
|
|
149
|
+
update_pr_body_tasks.push(task);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await Promise.all(update_pr_body_tasks);
|
|
153
|
+
|
|
154
|
+
actions.set((state) => {
|
|
155
|
+
state.step = "post-rebase-status";
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof Error) {
|
|
159
|
+
actions.error(err.message);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
actions.error("Unable to sync.");
|
|
163
|
+
if (!argv.verbose) {
|
|
164
|
+
actions.error("Try again with `--verbose` to see more information.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await handle_exit(18);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function get_push_group_list() {
|
|
171
|
+
// start from HEAD and work backward to rebase_group_index
|
|
172
|
+
const push_group_list = [];
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
175
|
+
const index = commit_range.group_list.length - 1 - i;
|
|
176
|
+
|
|
177
|
+
// do not go past rebase_group_index
|
|
178
|
+
if (index < rebase_group_index) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const group = commit_range.group_list[index];
|
|
183
|
+
|
|
184
|
+
push_group_list.unshift(group);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return push_group_list;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function before_push(args: { group: CommitMetadataGroup }) {
|
|
191
|
+
const { group } = args;
|
|
192
|
+
|
|
193
|
+
invariant(group.base, "group.base must exist");
|
|
194
|
+
|
|
195
|
+
// we may temporarily mark PR as a draft before editing it
|
|
196
|
+
// if it is not already a draft PR, to avoid notification spam
|
|
197
|
+
let is_temp_draft = !group.pr?.isDraft;
|
|
198
|
+
|
|
199
|
+
// before pushing reset base to master temporarily
|
|
200
|
+
// avoid accidentally pointing to orphaned parent commit
|
|
201
|
+
// should hopefully fix issues where a PR includes a bunch of commits after pushing
|
|
202
|
+
if (group.pr) {
|
|
203
|
+
if (!group.pr.isDraft) {
|
|
204
|
+
is_temp_draft = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (is_temp_draft) {
|
|
208
|
+
await github.pr_draft({
|
|
209
|
+
branch: group.id,
|
|
210
|
+
draft: true,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await github.pr_edit({
|
|
215
|
+
branch: group.id,
|
|
216
|
+
base: master_branch,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function after_push(args: {
|
|
222
|
+
group: CommitMetadataGroup;
|
|
223
|
+
pr_url_list: Array<string>;
|
|
224
|
+
}) {
|
|
225
|
+
const { group, pr_url_list } = args;
|
|
226
|
+
|
|
227
|
+
invariant(group.base, "group.base must exist");
|
|
228
|
+
|
|
229
|
+
const selected_url = get_group_url(group);
|
|
230
|
+
|
|
231
|
+
if (group.pr) {
|
|
232
|
+
// ensure base matches pr in github
|
|
233
|
+
await github.pr_edit({
|
|
234
|
+
branch: group.id,
|
|
235
|
+
base: group.base,
|
|
236
|
+
body: StackSummaryTable.write({
|
|
237
|
+
body: group.pr.body,
|
|
238
|
+
pr_url_list,
|
|
239
|
+
selected_url,
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// we may temporarily mark PR as a draft before editing it
|
|
244
|
+
// if it is not already a draft PR, to avoid notification spam
|
|
245
|
+
let is_temp_draft = !group.pr?.isDraft;
|
|
246
|
+
|
|
247
|
+
if (is_temp_draft) {
|
|
248
|
+
// mark pr as ready for review again
|
|
249
|
+
await github.pr_draft({
|
|
250
|
+
branch: group.id,
|
|
251
|
+
draft: false,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
// create pr in github
|
|
256
|
+
const pr_url = await github.pr_create({
|
|
257
|
+
branch: group.id,
|
|
258
|
+
base: group.base,
|
|
259
|
+
title: group.title,
|
|
260
|
+
body: DEFAULT_PR_BODY,
|
|
261
|
+
draft: argv.draft,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (!pr_url) {
|
|
265
|
+
throw new Error("unable to create pr");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// update pr_url_list with created pr_url
|
|
269
|
+
for (let i = 0; i < pr_url_list.length; i++) {
|
|
270
|
+
const url = pr_url_list[i];
|
|
271
|
+
if (url === selected_url) {
|
|
272
|
+
pr_url_list[i] = pr_url;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function update_pr_body(args: {
|
|
279
|
+
group: CommitMetadataGroup;
|
|
280
|
+
selected_url: string;
|
|
281
|
+
pr_url_list: Array<string>;
|
|
282
|
+
}) {
|
|
283
|
+
const { group, selected_url, pr_url_list } = args;
|
|
284
|
+
|
|
285
|
+
invariant(group.base, "group.base must exist");
|
|
286
|
+
|
|
287
|
+
const body = group.pr?.body || DEFAULT_PR_BODY;
|
|
288
|
+
|
|
289
|
+
const update_body = StackSummaryTable.write({
|
|
290
|
+
body,
|
|
291
|
+
pr_url_list,
|
|
292
|
+
selected_url,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (update_body === body) {
|
|
296
|
+
actions.debug(`Skipping body update for ${selected_url}`);
|
|
297
|
+
} else {
|
|
298
|
+
actions.debug(`Update body for ${selected_url}`);
|
|
299
|
+
|
|
300
|
+
await github.pr_edit({
|
|
301
|
+
branch: group.id,
|
|
302
|
+
base: group.base,
|
|
303
|
+
body: update_body,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function handle_exit(code: number) {
|
|
309
|
+
actions.output(
|
|
310
|
+
<Ink.Text color={colors.yellow}>Restoring PR state…</Ink.Text>
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
for (const group of push_group_list) {
|
|
314
|
+
// we may temporarily mark PR as a draft before editing it
|
|
315
|
+
// if it is not already a draft PR, to avoid notification spam
|
|
316
|
+
let is_temp_draft = !group.pr?.isDraft;
|
|
317
|
+
|
|
318
|
+
// restore PR to non-draft state
|
|
319
|
+
if (is_temp_draft) {
|
|
320
|
+
github
|
|
321
|
+
.pr_draft({
|
|
322
|
+
branch: group.id,
|
|
323
|
+
draft: false,
|
|
324
|
+
})
|
|
325
|
+
.catch(actions.error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
actions.output(
|
|
330
|
+
<Ink.Text color={colors.yellow}>Restored PR state.</Ink.Text>
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
actions.exit(code);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
|
|
338
|
+
const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
|
package/src/commands/Rebase.tsx
CHANGED
|
@@ -16,15 +16,35 @@ import { invariant } from "~/core/invariant";
|
|
|
16
16
|
import { short_id } from "~/core/short_id";
|
|
17
17
|
|
|
18
18
|
export function Rebase() {
|
|
19
|
+
const abort_handler = React.useRef(() => {});
|
|
20
|
+
|
|
21
|
+
React.useEffect(function listen_sigint() {
|
|
22
|
+
process.once("SIGINT", sigint_handler);
|
|
23
|
+
|
|
24
|
+
return function cleanup() {
|
|
25
|
+
process.removeListener("SIGINT", sigint_handler);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function sigint_handler() {
|
|
29
|
+
abort_handler.current();
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
19
33
|
return (
|
|
20
34
|
<Await
|
|
21
|
-
function={Rebase.run}
|
|
22
35
|
fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
|
|
36
|
+
function={async function () {
|
|
37
|
+
await Rebase.run({ abort_handler });
|
|
38
|
+
}}
|
|
23
39
|
/>
|
|
24
40
|
);
|
|
25
41
|
}
|
|
26
42
|
|
|
27
|
-
|
|
43
|
+
type Args = {
|
|
44
|
+
abort_handler: React.MutableRefObject<() => void>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
Rebase.run = async function run(args: Args) {
|
|
28
48
|
const state = Store.getState();
|
|
29
49
|
const actions = state.actions;
|
|
30
50
|
const branch_name = state.branch_name;
|
|
@@ -38,7 +58,10 @@ Rebase.run = async function run() {
|
|
|
38
58
|
invariant(repo_root, "repo_root must exist");
|
|
39
59
|
|
|
40
60
|
// always listen for SIGINT event and restore git state
|
|
41
|
-
|
|
61
|
+
args.abort_handler.current = async function sigint_handler() {
|
|
62
|
+
actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
|
|
63
|
+
handle_exit(19);
|
|
64
|
+
};
|
|
42
65
|
|
|
43
66
|
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
44
67
|
|
|
@@ -143,7 +166,7 @@ Rebase.run = async function run() {
|
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
|
|
146
|
-
handle_exit();
|
|
169
|
+
handle_exit(20);
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
// cleanup git operations if cancelled during manual rebase
|
|
@@ -171,7 +194,7 @@ Rebase.run = async function run() {
|
|
|
171
194
|
cli.sync(`pwd`, spawn_options);
|
|
172
195
|
}
|
|
173
196
|
|
|
174
|
-
function handle_exit() {
|
|
197
|
+
function handle_exit(code: number) {
|
|
175
198
|
actions.output(
|
|
176
199
|
<Ink.Text color={colors.yellow}>
|
|
177
200
|
Restoring <Brackets>{branch_name}</Brackets>…
|
|
@@ -186,6 +209,6 @@ Rebase.run = async function run() {
|
|
|
186
209
|
</Ink.Text>
|
|
187
210
|
);
|
|
188
211
|
|
|
189
|
-
actions.exit(
|
|
212
|
+
actions.exit(code);
|
|
190
213
|
}
|
|
191
214
|
};
|
package/src/core/github.tsx
CHANGED
|
@@ -123,7 +123,14 @@ type CreatePullRequestArgs = {
|
|
|
123
123
|
|
|
124
124
|
export async function pr_create(args: CreatePullRequestArgs) {
|
|
125
125
|
const title = safe_quote(args.title);
|
|
126
|
-
|
|
126
|
+
|
|
127
|
+
// explicit refs/heads head branch to avoid creation failing
|
|
128
|
+
//
|
|
129
|
+
// ❯ gh pr create --head origin/gs-ED2etrzv2 --base gs-6LAx-On45 --title="2024-01-05 test" --body=""
|
|
130
|
+
// pull request create failed: GraphQL: Head sha can't be blank, Base sha can't be blank, No commits between gs-6LAx-On45 and origin/gs-ED2etrzv2, Head ref must be a branch (createPullRequest)
|
|
131
|
+
//
|
|
132
|
+
// https://github.com/cli/cli/issues/5465
|
|
133
|
+
let command = `gh pr create --head refs/heads/${args.branch} --base ${args.base} --title="${title}" --body="${args.body}"`;
|
|
127
134
|
|
|
128
135
|
if (args.draft) {
|
|
129
136
|
command += " --draft";
|
package/src/index.tsx
CHANGED
|
@@ -10,7 +10,10 @@ import { command } from "~/command";
|
|
|
10
10
|
|
|
11
11
|
command()
|
|
12
12
|
.then((argv) => {
|
|
13
|
-
const ink = Ink.render(<App
|
|
13
|
+
const ink = Ink.render(<App />, {
|
|
14
|
+
// If true, each update will be rendered as a separate output, without replacing the previous one.
|
|
15
|
+
// debug: true,
|
|
16
|
+
});
|
|
14
17
|
|
|
15
18
|
Store.setState((state) => {
|
|
16
19
|
state.ink = ink;
|