git-stack-cli 1.12.0 → 1.13.1
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 +11 -1
- package/dist/cjs/index.cjs +543 -460
- package/package.json +1 -1
- package/src/app/AutoUpdate.tsx +5 -3
- package/src/app/CherryPickCheck.tsx +4 -4
- package/src/app/Debug.tsx +12 -9
- package/src/app/DependencyCheck.tsx +0 -6
- package/src/app/Main.tsx +4 -0
- package/src/app/ManualRebase.tsx +85 -357
- package/src/app/PreManualRebase.tsx +6 -5
- package/src/app/RebaseCheck.tsx +3 -3
- package/src/app/Store.tsx +8 -0
- package/src/app/SyncGithub.tsx +322 -0
- package/src/command.ts +0 -16
- package/src/commands/Rebase.tsx +31 -21
- package/src/core/GitReviseTodo.ts +3 -3
- package/src/core/github.tsx +18 -9
- package/src/core/read_json.ts +3 -3
- package/src/core/safe_exists.ts +10 -0
- package/src/core/safe_rm.ts +10 -0
- package/src/index.tsx +4 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import * as Ink from "ink-cjs";
|
|
@@ -11,6 +11,7 @@ import { FormatText } from "~/app/FormatText";
|
|
|
11
11
|
import { Store } from "~/app/Store";
|
|
12
12
|
import { colors } from "~/core/colors";
|
|
13
13
|
import { invariant } from "~/core/invariant";
|
|
14
|
+
import { safe_exists } from "~/core/safe_exists";
|
|
14
15
|
|
|
15
16
|
export function PreManualRebase() {
|
|
16
17
|
return <Await fallback={null} function={run} />;
|
|
@@ -40,8 +41,8 @@ async function run() {
|
|
|
40
41
|
for (const key of PR_TEMPLATE_KEY_LIST) {
|
|
41
42
|
const pr_template_fn = PR_TEMPLATE[key as keyof typeof PR_TEMPLATE];
|
|
42
43
|
|
|
43
|
-
if (
|
|
44
|
-
pr_template_body = fs.
|
|
44
|
+
if (await safe_exists(pr_template_fn(repo_root))) {
|
|
45
|
+
pr_template_body = await fs.readFile(pr_template_fn(repo_root), "utf-8");
|
|
45
46
|
|
|
46
47
|
actions.output(
|
|
47
48
|
<FormatText
|
|
@@ -59,8 +60,8 @@ async function run() {
|
|
|
59
60
|
|
|
60
61
|
// ./.github/PULL_REQUEST_TEMPLATE/*.md
|
|
61
62
|
let pr_templates: Array<string> = [];
|
|
62
|
-
if (
|
|
63
|
-
pr_templates = fs.
|
|
63
|
+
if (await safe_exists(PR_TEMPLATE.TemplateDir(repo_root))) {
|
|
64
|
+
pr_templates = await fs.readdir(PR_TEMPLATE.TemplateDir(repo_root));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// check if repo has multiple pr templates
|
package/src/app/RebaseCheck.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
|
|
6
5
|
import * as Ink from "ink-cjs";
|
|
@@ -11,6 +10,7 @@ import { Store } from "~/app/Store";
|
|
|
11
10
|
import { YesNoPrompt } from "~/app/YesNoPrompt";
|
|
12
11
|
import { cli } from "~/core/cli";
|
|
13
12
|
import { colors } from "~/core/colors";
|
|
13
|
+
import { safe_exists } from "~/core/safe_exists";
|
|
14
14
|
|
|
15
15
|
type Props = {
|
|
16
16
|
children: React.ReactNode;
|
|
@@ -74,8 +74,8 @@ export function RebaseCheck(props: Props) {
|
|
|
74
74
|
const git_dir = (await cli(`git rev-parse --absolute-git-dir`)).stdout;
|
|
75
75
|
|
|
76
76
|
let is_rebase = false;
|
|
77
|
-
is_rebase ||=
|
|
78
|
-
is_rebase ||=
|
|
77
|
+
is_rebase ||= await safe_exists(path.join(git_dir, "rebase-apply"));
|
|
78
|
+
is_rebase ||= await safe_exists(path.join(git_dir, "rebase-merge"));
|
|
79
79
|
|
|
80
80
|
const status = is_rebase ? "prompt" : "done";
|
|
81
81
|
patch({ status });
|
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
|
|
|
@@ -0,0 +1,322 @@
|
|
|
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
|
+
// git push -f origin HEAD~6:OtVX7Qvrw HEAD~3:E63ytp5dj HEAD~2:gs-NBabNSjXA HEAD~1:gs-UGVJdKNoD HEAD~0:gs-6LAx-On4
|
|
97
|
+
|
|
98
|
+
const git_push_command = [`git push -f origin`];
|
|
99
|
+
|
|
100
|
+
if (argv.verify === false) {
|
|
101
|
+
git_push_command.push("--no-verify");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const group of push_group_list) {
|
|
105
|
+
const last_commit = last(group.commits);
|
|
106
|
+
invariant(last_commit, "last_commit must exist");
|
|
107
|
+
const target = `${last_commit.sha}:${group.id}`;
|
|
108
|
+
git_push_command.push(target);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await cli(git_push_command);
|
|
112
|
+
|
|
113
|
+
const pr_url_list = commit_range.group_list.map(get_group_url);
|
|
114
|
+
|
|
115
|
+
const after_push_tasks = [];
|
|
116
|
+
for (const group of push_group_list) {
|
|
117
|
+
after_push_tasks.push(after_push({ group, pr_url_list }));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await Promise.all(after_push_tasks);
|
|
121
|
+
|
|
122
|
+
// finally, ensure all prs have the updated stack table from updated pr_url_list
|
|
123
|
+
// this step must come after the after_push since that step may create new PRs
|
|
124
|
+
// we need the urls for all prs at this step so we run it after the after_push
|
|
125
|
+
const update_pr_body_tasks = [];
|
|
126
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
127
|
+
const group = commit_range.group_list[i];
|
|
128
|
+
|
|
129
|
+
// use the updated pr_url_list to get the actual selected_url
|
|
130
|
+
const selected_url = pr_url_list[i];
|
|
131
|
+
|
|
132
|
+
const task = update_pr_body({ group, selected_url, pr_url_list });
|
|
133
|
+
update_pr_body_tasks.push(task);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await Promise.all(update_pr_body_tasks);
|
|
137
|
+
|
|
138
|
+
actions.set((state) => {
|
|
139
|
+
state.step = "post-rebase-status";
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
if (err instanceof Error) {
|
|
143
|
+
actions.error(err.message);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
actions.error("Unable to sync.");
|
|
147
|
+
if (!argv.verbose) {
|
|
148
|
+
actions.error("Try again with `--verbose` to see more information.");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await handle_exit(18);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function get_push_group_list() {
|
|
155
|
+
// start from HEAD and work backward to rebase_group_index
|
|
156
|
+
const push_group_list = [];
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < commit_range.group_list.length; i++) {
|
|
159
|
+
const index = commit_range.group_list.length - 1 - i;
|
|
160
|
+
|
|
161
|
+
// do not go past rebase_group_index
|
|
162
|
+
if (index < rebase_group_index) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const group = commit_range.group_list[index];
|
|
167
|
+
|
|
168
|
+
push_group_list.unshift(group);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return push_group_list;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function before_push(args: { group: CommitMetadataGroup }) {
|
|
175
|
+
const { group } = args;
|
|
176
|
+
|
|
177
|
+
invariant(group.base, "group.base must exist");
|
|
178
|
+
|
|
179
|
+
// we may temporarily mark PR as a draft before editing it
|
|
180
|
+
// if it is not already a draft PR, to avoid notification spam
|
|
181
|
+
let is_temp_draft = !group.pr?.isDraft;
|
|
182
|
+
|
|
183
|
+
// before pushing reset base to master temporarily
|
|
184
|
+
// avoid accidentally pointing to orphaned parent commit
|
|
185
|
+
// should hopefully fix issues where a PR includes a bunch of commits after pushing
|
|
186
|
+
if (group.pr) {
|
|
187
|
+
if (!group.pr.isDraft) {
|
|
188
|
+
is_temp_draft = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (is_temp_draft) {
|
|
192
|
+
await github.pr_draft({
|
|
193
|
+
branch: group.id,
|
|
194
|
+
draft: true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await github.pr_edit({
|
|
199
|
+
branch: group.id,
|
|
200
|
+
base: master_branch,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function after_push(args: {
|
|
206
|
+
group: CommitMetadataGroup;
|
|
207
|
+
pr_url_list: Array<string>;
|
|
208
|
+
}) {
|
|
209
|
+
const { group, pr_url_list } = args;
|
|
210
|
+
|
|
211
|
+
invariant(group.base, "group.base must exist");
|
|
212
|
+
|
|
213
|
+
const selected_url = get_group_url(group);
|
|
214
|
+
|
|
215
|
+
if (group.pr) {
|
|
216
|
+
// ensure base matches pr in github
|
|
217
|
+
await github.pr_edit({
|
|
218
|
+
branch: group.id,
|
|
219
|
+
base: group.base,
|
|
220
|
+
body: StackSummaryTable.write({
|
|
221
|
+
body: group.pr.body,
|
|
222
|
+
pr_url_list,
|
|
223
|
+
selected_url,
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// we may temporarily mark PR as a draft before editing it
|
|
228
|
+
// if it is not already a draft PR, to avoid notification spam
|
|
229
|
+
let is_temp_draft = !group.pr?.isDraft;
|
|
230
|
+
|
|
231
|
+
if (is_temp_draft) {
|
|
232
|
+
// mark pr as ready for review again
|
|
233
|
+
await github.pr_draft({
|
|
234
|
+
branch: group.id,
|
|
235
|
+
draft: false,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// create pr in github
|
|
240
|
+
const pr_url = await github.pr_create({
|
|
241
|
+
branch: group.id,
|
|
242
|
+
base: group.base,
|
|
243
|
+
title: group.title,
|
|
244
|
+
body: DEFAULT_PR_BODY,
|
|
245
|
+
draft: argv.draft,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
if (!pr_url) {
|
|
249
|
+
throw new Error("unable to create pr");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// update pr_url_list with created pr_url
|
|
253
|
+
for (let i = 0; i < pr_url_list.length; i++) {
|
|
254
|
+
const url = pr_url_list[i];
|
|
255
|
+
if (url === selected_url) {
|
|
256
|
+
pr_url_list[i] = pr_url;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function update_pr_body(args: {
|
|
263
|
+
group: CommitMetadataGroup;
|
|
264
|
+
selected_url: string;
|
|
265
|
+
pr_url_list: Array<string>;
|
|
266
|
+
}) {
|
|
267
|
+
const { group, selected_url, pr_url_list } = args;
|
|
268
|
+
|
|
269
|
+
invariant(group.base, "group.base must exist");
|
|
270
|
+
|
|
271
|
+
const body = group.pr?.body || DEFAULT_PR_BODY;
|
|
272
|
+
|
|
273
|
+
const update_body = StackSummaryTable.write({
|
|
274
|
+
body,
|
|
275
|
+
pr_url_list,
|
|
276
|
+
selected_url,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (update_body === body) {
|
|
280
|
+
actions.debug(`Skipping body update for ${selected_url}`);
|
|
281
|
+
} else {
|
|
282
|
+
actions.debug(`Update body for ${selected_url}`);
|
|
283
|
+
|
|
284
|
+
await github.pr_edit({
|
|
285
|
+
branch: group.id,
|
|
286
|
+
base: group.base,
|
|
287
|
+
body: update_body,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handle_exit(code: number) {
|
|
293
|
+
actions.output(
|
|
294
|
+
<Ink.Text color={colors.yellow}>Restoring PR state…</Ink.Text>
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
for (const group of push_group_list) {
|
|
298
|
+
// we may temporarily mark PR as a draft before editing it
|
|
299
|
+
// if it is not already a draft PR, to avoid notification spam
|
|
300
|
+
let is_temp_draft = !group.pr?.isDraft;
|
|
301
|
+
|
|
302
|
+
// restore PR to non-draft state
|
|
303
|
+
if (is_temp_draft) {
|
|
304
|
+
github
|
|
305
|
+
.pr_draft({
|
|
306
|
+
branch: group.id,
|
|
307
|
+
draft: false,
|
|
308
|
+
})
|
|
309
|
+
.catch(actions.error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
actions.output(
|
|
314
|
+
<Ink.Text color={colors.yellow}>Restored PR state.</Ink.Text>
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
actions.exit(code);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
type CommitMetadataGroup = CommitMetadata.CommitRange["group_list"][number];
|
|
322
|
+
const get_group_url = (group: CommitMetadataGroup) => group.pr?.url || group.id;
|
package/src/command.ts
CHANGED
|
@@ -52,11 +52,6 @@ export async function command() {
|
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const Rebase = Object.freeze({
|
|
56
|
-
"git-revise": "git-revise",
|
|
57
|
-
"cherry-pick": "cherry-pick",
|
|
58
|
-
});
|
|
59
|
-
|
|
60
55
|
const GlobalOptions = {
|
|
61
56
|
verbose: {
|
|
62
57
|
type: "boolean",
|
|
@@ -95,17 +90,6 @@ const DefaultOptions = {
|
|
|
95
90
|
"Run git hooks such as pre-commit and pre-push, disable with --no-verify",
|
|
96
91
|
},
|
|
97
92
|
|
|
98
|
-
"rebase": {
|
|
99
|
-
type: "string",
|
|
100
|
-
choices: [Rebase["git-revise"], Rebase["cherry-pick"]],
|
|
101
|
-
default: Rebase["git-revise"],
|
|
102
|
-
description: [
|
|
103
|
-
"Strategy used for syncing branches",
|
|
104
|
-
`${Rebase["git-revise"]}: perform faster in-memory rebase`,
|
|
105
|
-
`${Rebase["cherry-pick"]}: use disk and incrementally rebase each commit`,
|
|
106
|
-
].join(" | "),
|
|
107
|
-
},
|
|
108
|
-
|
|
109
93
|
"update": {
|
|
110
94
|
type: "boolean",
|
|
111
95
|
alias: ["u", "upgrade"],
|
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
|
|
|
@@ -111,9 +134,8 @@ Rebase.run = async function run() {
|
|
|
111
134
|
await cli(`git cherry-pick --keep-redundant-commits ${sha_list}`);
|
|
112
135
|
}
|
|
113
136
|
|
|
114
|
-
// after all commits have been cherry-picked
|
|
115
|
-
//
|
|
116
|
-
// now we are locally in sync with github and on the original branch
|
|
137
|
+
// after all commits have been cherry-picked move the pointer
|
|
138
|
+
// of original branch to the newly created temporary branch
|
|
117
139
|
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
118
140
|
|
|
119
141
|
restore_git();
|
|
@@ -144,7 +166,7 @@ Rebase.run = async function run() {
|
|
|
144
166
|
}
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
handle_exit();
|
|
169
|
+
handle_exit(20);
|
|
148
170
|
}
|
|
149
171
|
|
|
150
172
|
// cleanup git operations if cancelled during manual rebase
|
|
@@ -154,9 +176,6 @@ Rebase.run = async function run() {
|
|
|
154
176
|
// all children processes receive the SIGINT signal
|
|
155
177
|
const spawn_options = { ignoreExitCode: true };
|
|
156
178
|
|
|
157
|
-
// always clean up any patch files
|
|
158
|
-
cli.sync(`rm ${PATCH_FILE}`, spawn_options);
|
|
159
|
-
|
|
160
179
|
// always hard reset and clean to allow subsequent checkout
|
|
161
180
|
// if there are files checkout will fail and cascade fail subsequent commands
|
|
162
181
|
cli.sync(`git reset --hard`, spawn_options);
|
|
@@ -168,13 +187,6 @@ Rebase.run = async function run() {
|
|
|
168
187
|
// ...and cleanup temporary branch
|
|
169
188
|
cli.sync(`git branch -D ${temp_branch_name}`, spawn_options);
|
|
170
189
|
|
|
171
|
-
if (commit_range) {
|
|
172
|
-
// ...and cleanup pr group branches
|
|
173
|
-
for (const group of commit_range.group_list) {
|
|
174
|
-
cli.sync(`git branch -D ${group.id}`, spawn_options);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
190
|
// restore back to original dir
|
|
179
191
|
if (fs.existsSync(cwd)) {
|
|
180
192
|
process.chdir(cwd);
|
|
@@ -182,7 +194,7 @@ Rebase.run = async function run() {
|
|
|
182
194
|
cli.sync(`pwd`, spawn_options);
|
|
183
195
|
}
|
|
184
196
|
|
|
185
|
-
function handle_exit() {
|
|
197
|
+
function handle_exit(code: number) {
|
|
186
198
|
actions.output(
|
|
187
199
|
<Ink.Text color={colors.yellow}>
|
|
188
200
|
Restoring <Brackets>{branch_name}</Brackets>…
|
|
@@ -197,8 +209,6 @@ Rebase.run = async function run() {
|
|
|
197
209
|
</Ink.Text>
|
|
198
210
|
);
|
|
199
211
|
|
|
200
|
-
actions.exit(
|
|
212
|
+
actions.exit(code);
|
|
201
213
|
}
|
|
202
214
|
};
|
|
203
|
-
|
|
204
|
-
const PATCH_FILE = "git-stack-cli-patch.patch";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -120,10 +120,10 @@ GitReviseTodo.execute = async function grt_execute(args: ExecuteArgs) {
|
|
|
120
120
|
const GIT_SEQUENCE_EDITOR_SCRIPT = `process.env.GIT_SEQUENCE_EDITOR_SCRIPT`;
|
|
121
121
|
|
|
122
122
|
// write script to temporary path
|
|
123
|
-
fs.
|
|
123
|
+
await fs.writeFile(tmp_git_sequence_editor_path, GIT_SEQUENCE_EDITOR_SCRIPT);
|
|
124
124
|
|
|
125
125
|
// ensure script is executable
|
|
126
|
-
fs.
|
|
126
|
+
await fs.chmod(tmp_git_sequence_editor_path, "755");
|
|
127
127
|
|
|
128
128
|
const git_revise_todo = GitReviseTodo(args);
|
|
129
129
|
|
package/src/core/github.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
|
|
3
|
-
import fs from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ import { cli } from "~/core/cli";
|
|
|
12
12
|
import { colors } from "~/core/colors";
|
|
13
13
|
import { invariant } from "~/core/invariant";
|
|
14
14
|
import { safe_quote } from "~/core/safe_quote";
|
|
15
|
+
import { safe_rm } from "~/core/safe_rm";
|
|
15
16
|
|
|
16
17
|
export async function pr_list(): Promise<Array<PullRequest>> {
|
|
17
18
|
const state = Store.getState();
|
|
@@ -148,7 +149,8 @@ export async function pr_edit(args: EditPullRequestArgs) {
|
|
|
148
149
|
const command_parts = [`gh pr edit ${args.branch} --base ${args.base}`];
|
|
149
150
|
|
|
150
151
|
if (args.body) {
|
|
151
|
-
|
|
152
|
+
const body_file = await write_body_file(args);
|
|
153
|
+
command_parts.push(`--body-file="${body_file}"`);
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
const command = command_parts.join(" ");
|
|
@@ -166,6 +168,10 @@ type DraftPullRequestArgs = {
|
|
|
166
168
|
};
|
|
167
169
|
|
|
168
170
|
export async function pr_draft(args: DraftPullRequestArgs) {
|
|
171
|
+
// https://cli.github.com/manual/gh_api
|
|
172
|
+
// https://docs.github.com/en/graphql/reference/mutations#convertpullrequesttodraft
|
|
173
|
+
// https://docs.github.com/en/graphql/reference/mutations#markpullrequestreadyforreview
|
|
174
|
+
|
|
169
175
|
const mutation_name = args.draft
|
|
170
176
|
? "convertPullRequestToDraft"
|
|
171
177
|
: "markPullRequestReadyForReview";
|
|
@@ -220,7 +226,7 @@ async function gh_json<T>(command: string): Promise<T | Error> {
|
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
// read from file
|
|
223
|
-
const json_str = fs.
|
|
229
|
+
const json_str = await fs.readFile(tmp_pr_json, "utf-8");
|
|
224
230
|
const json = JSON.parse(json_str);
|
|
225
231
|
return json;
|
|
226
232
|
}
|
|
@@ -237,13 +243,16 @@ function handle_error(output: string): never {
|
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
// convert a string to a file for use via github cli `--body-file`
|
|
240
|
-
function
|
|
246
|
+
async function write_body_file(args: EditPullRequestArgs) {
|
|
247
|
+
invariant(args.body, "args.body must exist");
|
|
248
|
+
|
|
241
249
|
const temp_dir = os.tmpdir();
|
|
242
|
-
const temp_path = path.join(temp_dir,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fs.
|
|
250
|
+
const temp_path = path.join(temp_dir, `git-stack-body-${args.base}`);
|
|
251
|
+
|
|
252
|
+
await safe_rm(temp_path);
|
|
253
|
+
|
|
254
|
+
await fs.writeFile(temp_path, args.body);
|
|
255
|
+
|
|
247
256
|
return temp_path;
|
|
248
257
|
}
|
|
249
258
|
|
package/src/core/read_json.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
2
|
|
|
3
|
-
export function read_json<T = unknown>(path: string): null | T {
|
|
3
|
+
export async function read_json<T = unknown>(path: string): Promise<null | T> {
|
|
4
4
|
try {
|
|
5
|
-
const file_buffer = fs.
|
|
5
|
+
const file_buffer = await fs.readFile(path);
|
|
6
6
|
const json_str = String(file_buffer);
|
|
7
7
|
const json = JSON.parse(json_str);
|
|
8
8
|
return json;
|
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;
|