git-stack-cli 1.13.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.
@@ -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;
@@ -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
- Rebase.run = async function run() {
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
- process.once("SIGINT", handle_exit);
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(6);
212
+ actions.exit(code);
190
213
  }
191
214
  };
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;