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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
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
 
@@ -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={run}
35
+ function={async function () {
36
+ await run({ abort_handler });
37
+ }}
25
38
  />
26
39
  );
27
40
  }
28
41
 
29
- async function run() {
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
- process.once("SIGINT", handle_exit);
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
- // get latest merge_base relative to local master
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
- // immediately paint all commit to preserve selected commit ranges
50
- let commit_range = await CommitMetadata.range(commit_map);
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
- // reverse group list to ensure we create git revise in correct order
53
- commit_range.group_list.reverse();
73
+ // immediately paint all commit to preserve selected commit ranges
74
+ let commit_range = await CommitMetadata.range(commit_map);
54
75
 
55
- for (const commit of commit_range.commit_list) {
56
- const group_from_map = commit_map[commit.sha];
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
- await GitReviseTodo.execute({
62
- rebase_group_index: 0,
63
- rebase_merge_base: merge_base,
64
- commit_range,
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
- let DEFAULT_PR_BODY = "";
68
- if (state.pr_template_body) {
69
- DEFAULT_PR_BODY = state.pr_template_body;
70
- }
85
+ await GitReviseTodo.execute({
86
+ rebase_group_index: 0,
87
+ rebase_merge_base: merge_base,
88
+ commit_range,
89
+ });
71
90
 
72
- const temp_branch_name = `${branch_name}_${short_id()}`;
91
+ commit_range = await CommitMetadata.range(commit_map);
73
92
 
74
- commit_range = await CommitMetadata.range(commit_map);
93
+ // reverse commit list so that we can cherry-pick in order
94
+ commit_range.group_list.reverse();
75
95
 
76
- // reverse commit list so that we can cherry-pick in order
77
- commit_range.group_list.reverse();
96
+ let rebase_merge_base = merge_base;
97
+ let rebase_group_index = 0;
78
98
 
79
- let rebase_merge_base = merge_base;
80
- let rebase_group_index = 0;
99
+ for (let i = 0; i < commit_range.group_list.length; i++) {
100
+ const group = commit_range.group_list[i];
81
101
 
82
- for (let i = 0; i < commit_range.group_list.length; i++) {
83
- const group = commit_range.group_list[i];
102
+ if (!group.dirty) {
103
+ continue;
104
+ }
84
105
 
85
- if (!group.dirty) {
86
- continue;
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
- if (i > 0) {
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
- break;
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
- // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
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
- actions.set((state) => {
135
- state.step = "post-rebase-status";
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(5);
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