git-stack-cli 2.5.3 → 2.6.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.
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
  import * as Ink from "ink-cjs";
4
4
 
5
5
  import { Brackets } from "~/app/Brackets";
6
+ import { Command } from "~/app/Command";
6
7
  import { FormatText } from "~/app/FormatText";
7
8
  import { MultiSelect } from "~/app/MultiSelect";
8
9
  import { Parens } from "~/app/Parens";
@@ -78,10 +79,13 @@ function SelectCommitRangesInternal(props: Props) {
78
79
 
79
80
  // detect if there are unassigned commits
80
81
  let unassigned_count = 0;
82
+ let assigned_count = 0;
81
83
  for (const [, group_id] of commit_map.entries()) {
82
84
  if (group_id === null) {
83
85
  // console.debug("unassigned commit detected", sha);
84
86
  unassigned_count++;
87
+ } else {
88
+ assigned_count++;
85
89
  }
86
90
  }
87
91
 
@@ -118,36 +122,54 @@ function SelectCommitRangesInternal(props: Props) {
118
122
  current_index = 0;
119
123
  }
120
124
 
125
+ const has_unassigned_commits = unassigned_count > 0;
126
+ const has_assigned_commits = assigned_count > 0;
127
+
128
+ const sync_status = detect_sync_status();
129
+ // console.debug({ sync_status });
130
+
121
131
  Ink.useInput((input, key) => {
122
- const inputLower = input.toLowerCase();
132
+ const input_lower = input.toLowerCase();
123
133
 
124
- const hasUnassignedCommits = unassigned_count > 0;
134
+ if (input_lower === SYMBOL.s) {
135
+ // do not allow sync when inputting group title
136
+ if (group_input) {
137
+ return;
138
+ }
139
+
140
+ if (sync_status === "disabled") {
141
+ return;
142
+ }
125
143
 
126
- if (!hasUnassignedCommits && inputLower === "s") {
127
144
  actions.set((state) => {
128
- state.commit_map = {};
129
-
130
- for (const [sha, id] of commit_map.entries()) {
131
- if (id) {
132
- const group = group_list.find((g) => g.id === id);
133
- // console.debug({ sha, id, group });
134
- if (group) {
135
- state.commit_map[sha] = group;
136
- }
145
+ const state_commit_map: Record<string, SimpleGroup> = {};
146
+
147
+ for (let [sha, id] of commit_map.entries()) {
148
+ // console.debug({ sha, id });
149
+
150
+ // handle allow_unassigned case
151
+ if (!id) {
152
+ id = props.commit_range.UNASSIGNED;
153
+ const title = "allow_unassigned";
154
+ state_commit_map[sha] = { id, title };
155
+ continue;
137
156
  }
138
- }
139
157
 
140
- switch (inputLower) {
141
- case "s":
142
- state.step = "pre-manual-rebase";
143
- break;
158
+ const group = group_list.find((g) => g.id === id);
159
+ invariant(group, "group must exist");
160
+ // console.debug({ group });
161
+ state_commit_map[sha] = group;
144
162
  }
163
+
164
+ state.commit_map = state_commit_map;
165
+ state.step = "pre-manual-rebase";
145
166
  });
167
+
146
168
  return;
147
169
  }
148
170
 
149
171
  // only allow create when on unassigned group
150
- if (hasUnassignedCommits && inputLower === "c") {
172
+ if (has_unassigned_commits && input_lower === SYMBOL.c) {
151
173
  set_group_input(true);
152
174
  return;
153
175
  }
@@ -170,6 +192,10 @@ function SelectCommitRangesInternal(props: Props) {
170
192
  const multiselect_disabled = group_input;
171
193
  const multiselect_disableSelect = group.id === props.commit_range.UNASSIGNED;
172
194
 
195
+ const max_width = 80;
196
+ const [focused, set_focused] = React.useState("");
197
+ const has_groups = group.id !== props.commit_range.UNASSIGNED;
198
+
173
199
  const items = props.commit_range.commit_list.map((commit) => {
174
200
  const commit_metadata_id = commit_map.get(commit.sha);
175
201
 
@@ -179,6 +205,8 @@ function SelectCommitRangesInternal(props: Props) {
179
205
 
180
206
  if (group_input) {
181
207
  disabled = true;
208
+ } else if (!has_groups) {
209
+ disabled = true;
182
210
  } else {
183
211
  disabled = Boolean(selected && commit_metadata_id !== group.id);
184
212
  }
@@ -193,30 +221,102 @@ function SelectCommitRangesInternal(props: Props) {
193
221
 
194
222
  items.reverse();
195
223
 
196
- // <- (2/4) #742 Title A ->
224
+ return (
225
+ <Ink.Box flexDirection="column">
226
+ <Ink.Box height={1} />
227
+
228
+ {has_groups || group_input ? null : (
229
+ <Ink.Box flexDirection="column">
230
+ <Ink.Text bold color={colors.blue}>
231
+ 👋 Welcome to <Command>git stack</Command>!
232
+ </Ink.Text>
233
+ <Ink.Text color={colors.blue}>
234
+ <FormatText
235
+ message="Press {c} to {create} a new PR"
236
+ values={{
237
+ c: (
238
+ <Ink.Text bold color={colors.green}>
239
+ c
240
+ </Ink.Text>
241
+ ),
242
+ create: (
243
+ <Ink.Text bold color={colors.green}>
244
+ <Parens>c</Parens>reate
245
+ </Ink.Text>
246
+ ),
247
+ }}
248
+ />
249
+ </Ink.Text>
250
+ </Ink.Box>
251
+ )}
197
252
 
198
- const left_arrow = `${SYMBOL.left} `;
199
- const right_arrow = ` ${SYMBOL.right}`;
200
- const group_position = `(${current_index + 1}/${group_list.length}) `;
253
+ {!has_groups || group_input ? null : (
254
+ <React.Fragment>
255
+ <Ink.Box width={max_width} flexDirection="row">
256
+ <Ink.Box flexDirection="row">
257
+ <Ink.Text bold color={colors.green}>
258
+ {SYMBOL.left}
259
+ </Ink.Text>
260
+ <Ink.Box width={1} />
261
+ <Ink.Text color={colors.gray}>Pull request</Ink.Text>
262
+ <Ink.Box width={1} />
263
+ <Ink.Text color={colors.gray}>
264
+ {`(${current_index + 1}/${group_list.length})`}
265
+ </Ink.Text>
266
+ <Ink.Box width={1} />
267
+ <Ink.Text bold color={colors.green}>
268
+ {SYMBOL.right}
269
+ </Ink.Text>
270
+ </Ink.Box>
271
+ </Ink.Box>
272
+
273
+ <Ink.Box width={max_width}>
274
+ <Ink.Text wrap="truncate-end" bold color={colors.white}>
275
+ {group.title}
276
+ </Ink.Text>
277
+ </Ink.Box>
278
+ </React.Fragment>
279
+ )}
201
280
 
202
- const max_group_label_width = 80;
203
- let group_title_width = max_group_label_width;
204
- group_title_width -= group_position.length;
205
- group_title_width -= left_arrow.length + right_arrow.length;
206
- group_title_width = Math.min(group.title.length, group_title_width);
281
+ {!group_input ? null : (
282
+ <React.Fragment>
283
+ <Ink.Box height={1} />
207
284
 
208
- let max_item_width = max_group_label_width;
209
- max_item_width -= left_arrow.length + right_arrow.length;
285
+ <FormatText
286
+ wrapper={<Ink.Text color={colors.gray} />}
287
+ message="Enter a title for the PR {note}"
288
+ values={{
289
+ note: (
290
+ <Parens>
291
+ <FormatText
292
+ message="press {enter} to submit"
293
+ values={{
294
+ enter: (
295
+ <Ink.Text bold color={colors.green}>
296
+ {SYMBOL.enter}
297
+ </Ink.Text>
298
+ ),
299
+ }}
300
+ />
301
+ </Parens>
302
+ ),
303
+ }}
304
+ />
210
305
 
211
- const [focused, set_focused] = React.useState("");
306
+ <TextInput
307
+ defaultValue={focused}
308
+ onSubmit={submit_group_input}
309
+ onCancel={() => set_group_input(false)}
310
+ />
311
+ </React.Fragment>
312
+ )}
212
313
 
213
- return (
214
- <Ink.Box flexDirection="column">
215
314
  <Ink.Box height={1} />
216
315
 
217
316
  <MultiSelect
317
+ startIndex={items.length - 1}
218
318
  items={items}
219
- maxWidth={max_item_width}
319
+ maxWidth={max_width}
220
320
  disabled={multiselect_disabled}
221
321
  disableSelect={multiselect_disableSelect}
222
322
  onFocus={(args) => {
@@ -242,116 +342,76 @@ function SelectCommitRangesInternal(props: Props) {
242
342
 
243
343
  <Ink.Box height={1} />
244
344
 
245
- <Ink.Box width={max_group_label_width} flexDirection="row">
246
- <Ink.Text>{left_arrow}</Ink.Text>
247
- <Ink.Text>{group_position}</Ink.Text>
248
-
249
- <Ink.Box width={group_title_width} justifyContent="center">
250
- <Ink.Text wrap="truncate-end">{group.title}</Ink.Text>
251
- </Ink.Box>
252
-
253
- <Ink.Text>{right_arrow}</Ink.Text>
254
- </Ink.Box>
255
-
256
- <Ink.Box height={1} />
257
-
258
- {unassigned_count > 0 ? (
259
- <FormatText
260
- wrapper={<Ink.Text color={colors.gray} />}
261
- message="{count} unassigned commits, press {c} to {create} a new group"
262
- values={{
263
- count: (
264
- <Ink.Text color={colors.yellow} bold>
265
- {unassigned_count}
266
- </Ink.Text>
267
- ),
268
- c: (
269
- <Ink.Text bold color={colors.green}>
270
- c
271
- </Ink.Text>
272
- ),
273
- create: (
274
- <Ink.Text bold color={colors.green}>
275
- <Parens>c</Parens>reate
276
- </Ink.Text>
277
- ),
278
- }}
279
- />
280
- ) : (
345
+ {has_unassigned_commits ? (
281
346
  <React.Fragment>
282
- {argv.sync ? (
347
+ <FormatText
348
+ wrapper={<Ink.Text color={colors.gray} />}
349
+ message="{count} unassigned commits"
350
+ values={{
351
+ count: (
352
+ <Ink.Text color={colors.yellow} bold>
353
+ {unassigned_count}
354
+ </Ink.Text>
355
+ ),
356
+ }}
357
+ />
358
+
359
+ {group_input ? null : (
283
360
  <FormatText
284
- wrapper={<Ink.Text />}
285
- message="🎉 Done! Press {s} to {sync} the commits to Github"
361
+ wrapper={<Ink.Text color={colors.gray} />}
362
+ message="Press {c} to {create} a new PR"
286
363
  values={{
287
- s: (
364
+ c: (
288
365
  <Ink.Text bold color={colors.green}>
289
- s
366
+ c
290
367
  </Ink.Text>
291
368
  ),
292
- sync: (
369
+ create: (
293
370
  <Ink.Text bold color={colors.green}>
294
- <Parens>s</Parens>ync
371
+ <Parens>c</Parens>reate
295
372
  </Ink.Text>
296
373
  ),
297
374
  }}
298
375
  />
299
- ) : (
376
+ )}
377
+
378
+ {sync_status !== "allow_unassigned" ? null : (
300
379
  <FormatText
301
- wrapper={<Ink.Text />}
302
- message="🎉 Done! Press {s} to {save} the commits locally"
380
+ wrapper={<Ink.Text color={colors.gray} />}
381
+ message="Press {s} to {sync} the {count} assigned commits to Github"
303
382
  values={{
304
- s: (
305
- <Ink.Text bold color={colors.green}>
306
- s
307
- </Ink.Text>
308
- ),
309
- save: (
310
- <Ink.Text bold color={colors.green}>
311
- <Parens>s</Parens>save
383
+ ...S_TO_SYNC_VALUES,
384
+ count: (
385
+ <Ink.Text color={colors.yellow} bold>
386
+ {assigned_count}
312
387
  </Ink.Text>
313
388
  ),
314
389
  }}
315
390
  />
316
391
  )}
317
392
  </React.Fragment>
318
- )}
319
-
320
- {!group_input ? null : (
393
+ ) : (
321
394
  <React.Fragment>
322
- <Ink.Box height={1} />
323
-
324
- <FormatText
325
- wrapper={<Ink.Text color={colors.gray} />}
326
- message="Enter a title for the PR {note}"
327
- values={{
328
- note: (
329
- <Parens>
330
- <FormatText
331
- message="press {enter} to submit"
332
- values={{
333
- enter: (
334
- <Ink.Text bold color={colors.green}>
335
- {SYMBOL.enter}
336
- </Ink.Text>
337
- ),
338
- }}
339
- />
340
- </Parens>
341
- ),
342
- }}
343
- />
344
-
345
- <TextInput defaultValue={focused} onSubmit={submit_group_input} />
346
-
347
- <Ink.Box height={1} />
395
+ {argv.sync ? (
396
+ <FormatText
397
+ wrapper={<Ink.Text />}
398
+ message="🎉 Done! Press {s} to {sync} the commits to Github"
399
+ values={S_TO_SYNC_VALUES}
400
+ />
401
+ ) : (
402
+ <FormatText
403
+ wrapper={<Ink.Text />}
404
+ message="🎉 Done! Press {s} to {save} the commits locally"
405
+ values={S_TO_SYNC_VALUES}
406
+ />
407
+ )}
348
408
  </React.Fragment>
349
409
  )}
350
410
 
351
411
  <Ink.Box>
352
412
  <FormatText
353
413
  wrapper={<Ink.Text color={colors.gray} />}
354
- message="Press {left} and {right} to view PR groups"
414
+ message="Press {left} and {right} to view PRs"
355
415
  values={{
356
416
  left: (
357
417
  <Ink.Text bold color={colors.green}>
@@ -396,13 +456,14 @@ function SelectCommitRangesInternal(props: Props) {
396
456
 
397
457
  return `${branch_prefix}${gs_short_id()}`;
398
458
  }
459
+
399
460
  function submit_group_input(title: string) {
400
461
  const id = get_group_id();
401
462
 
402
463
  actions.output(
403
464
  <FormatText
404
465
  wrapper={<Ink.Text dimColor />}
405
- message="Created new group {group} {note}"
466
+ message="Created new PR {group} {note}"
406
467
  values={{
407
468
  group: <Brackets>{title}</Brackets>,
408
469
  note: <Parens>{id}</Parens>,
@@ -415,10 +476,64 @@ function SelectCommitRangesInternal(props: Props) {
415
476
  set_selected_group_id(id);
416
477
  set_group_input(false);
417
478
  }
479
+
480
+ function detect_sync_status() {
481
+ if (!has_unassigned_commits) {
482
+ return "allow";
483
+ }
484
+
485
+ if (!has_assigned_commits) {
486
+ return "disabled";
487
+ }
488
+
489
+ let allow_unassigned_sync = null;
490
+
491
+ for (let i = 0; i < props.commit_range.commit_list.length; i++) {
492
+ const commit = props.commit_range.commit_list[i];
493
+ const group_id = commit_map.get(commit.sha);
494
+ // console.debug(commit.sha, group_id);
495
+
496
+ // before detecting unassigned we are null
497
+ if (allow_unassigned_sync === null) {
498
+ if (group_id === null) {
499
+ // console.debug("allow_unassigned_sync TRUE", { i });
500
+ allow_unassigned_sync = true;
501
+ }
502
+ } else {
503
+ // after detecting unassigned we assume we can unassigned sync
504
+ // unless we detect an invariant violation, i.e. commit assigned to group
505
+ if (group_id) {
506
+ // console.debug("allow_unassigned_sync FALSE", { i });
507
+ allow_unassigned_sync = false;
508
+ }
509
+ }
510
+ }
511
+
512
+ if (allow_unassigned_sync) {
513
+ return "allow_unassigned";
514
+ }
515
+
516
+ return "disabled";
517
+ }
418
518
  }
419
519
 
420
520
  const SYMBOL = {
421
521
  left: "←",
422
522
  right: "→",
423
523
  enter: "Enter",
524
+ c: "c",
525
+ s: "s",
526
+ };
527
+
528
+ const S_TO_SYNC_VALUES = {
529
+ s: (
530
+ <Ink.Text bold color={colors.green}>
531
+ s
532
+ </Ink.Text>
533
+ ),
534
+ sync: (
535
+ <Ink.Text bold color={colors.green}>
536
+ <Parens>s</Parens>ync
537
+ </Ink.Text>
538
+ ),
424
539
  };
@@ -42,11 +42,7 @@ async function run() {
42
42
  Store.setState((state) => {
43
43
  state.step = "pre-local-merge-rebase";
44
44
  });
45
- } else if (needs_update) {
46
- Store.setState((state) => {
47
- state.step = "pre-select-commit-ranges";
48
- });
49
- } else if (argv.force) {
45
+ } else if (needs_update || argv.force) {
50
46
  Store.setState((state) => {
51
47
  state.step = "select-commit-ranges";
52
48
  });
package/src/app/Store.tsx CHANGED
@@ -51,6 +51,7 @@ export type State = {
51
51
  master_branch: string;
52
52
  head: null | string;
53
53
  branch_name: null | string;
54
+ merge_base: null | string;
54
55
  commit_range: null | CommitMetadata.CommitRange;
55
56
  commit_map: null | CommitMap;
56
57
  pr_templates: Array<string>;
@@ -66,7 +67,6 @@ export type State = {
66
67
  | "status"
67
68
  | "pre-local-merge-rebase"
68
69
  | "local-merge-rebase"
69
- | "pre-select-commit-ranges"
70
70
  | "select-commit-ranges"
71
71
  | "pre-manual-rebase"
72
72
  | "manual-rebase"
@@ -122,6 +122,7 @@ const BaseStore = createStore<State>()(
122
122
  master_branch: "origin/master",
123
123
  head: null,
124
124
  branch_name: null,
125
+ merge_base: null,
125
126
  commit_range: null,
126
127
  commit_map: null,
127
128
  pr_templates: [],
@@ -42,6 +42,9 @@ async function run() {
42
42
 
43
43
  const push_group_list = get_push_group_list();
44
44
 
45
+ // console.debug({ push_group_list });
46
+ // throw new Error("STOP");
47
+
45
48
  // for all push targets in push_group_list
46
49
  // things that can be done in parallel are grouped by numbers
47
50
  //
@@ -92,7 +95,7 @@ async function run() {
92
95
 
93
96
  await cli(git_push_command);
94
97
 
95
- const pr_url_list = commit_range.group_list.map(get_group_url);
98
+ const pr_url_list = push_group_list.map(get_group_url);
96
99
 
97
100
  const after_push_tasks = [];
98
101
  for (const group of push_group_list) {
@@ -105,8 +108,8 @@ async function run() {
105
108
  // this step must come after the after_push since that step may create new PRs
106
109
  // we need the urls for all prs at this step so we run it after the after_push
107
110
  const update_pr_body_tasks = [];
108
- for (let i = 0; i < commit_range.group_list.length; i++) {
109
- const group = commit_range.group_list[i];
111
+ for (let i = 0; i < push_group_list.length; i++) {
112
+ const group = push_group_list[i];
110
113
 
111
114
  // use the updated pr_url_list to get the actual selected_url
112
115
  const selected_url = pr_url_list[i];
@@ -149,7 +152,9 @@ async function run() {
149
152
 
150
153
  const group = commit_range.group_list[index];
151
154
 
152
- push_group_list.unshift(group);
155
+ if (group.id !== commit_range.UNASSIGNED) {
156
+ push_group_list.unshift(group);
157
+ }
153
158
  }
154
159
 
155
160
  return push_group_list;
@@ -10,6 +10,7 @@ type Props = {
10
10
  value?: string;
11
11
  onChange?: (value: string) => void;
12
12
  onSubmit?: (value: string) => void;
13
+ onCancel?: () => void;
13
14
  };
14
15
 
15
16
  export function TextInput(props: Props) {
@@ -44,7 +45,13 @@ export function TextInput(props: Props) {
44
45
 
45
46
  // console.debug("[useInput]", { input, key });
46
47
 
47
- if (key.backspace || key.delete) {
48
+ if (key.escape) {
49
+ if (value === "") {
50
+ props.onCancel?.();
51
+ } else {
52
+ next_value = "";
53
+ }
54
+ } else if (key.backspace || key.delete) {
48
55
  next_value = value.slice(0, -1);
49
56
  } else if (key.return) {
50
57
  props.onSubmit?.(next_value);
@@ -22,7 +22,7 @@ type Props = {
22
22
  export function Rebase(props: Props) {
23
23
  return (
24
24
  <Await
25
- fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
25
+ fallback={<Ink.Text color={colors.yellow}>Rebasing…</Ink.Text>}
26
26
  function={() => Rebase.run(props)}
27
27
  />
28
28
  );
@@ -48,6 +48,7 @@ Rebase.run = async function run(props: Props) {
48
48
  return 19;
49
49
  });
50
50
 
51
+ const master_branch_name = master_branch.replace(/^origin\//, "");
51
52
  const temp_branch_name = `${branch_name}_${short_id()}`;
52
53
 
53
54
  try {
@@ -58,9 +59,56 @@ Rebase.run = async function run(props: Props) {
58
59
  await cli(`pwd`);
59
60
 
60
61
  // fetch origin master branch for latest sha
61
- const master_branch_name = master_branch.replace(/^origin\//, "");
62
62
  await cli(`git fetch --no-tags -v origin ${master_branch_name}`);
63
63
 
64
+ if (branch_name === master_branch_name) {
65
+ await rebase_master();
66
+ } else {
67
+ await rebase_branch();
68
+ }
69
+
70
+ actions.unregister_abort_handler();
71
+ } catch (err) {
72
+ actions.error("Unable to rebase.");
73
+
74
+ if (err instanceof Error) {
75
+ actions.error(err.message);
76
+ }
77
+
78
+ actions.exit(20);
79
+ }
80
+
81
+ const next_commit_range = await CommitMetadata.range();
82
+
83
+ actions.output(
84
+ <FormatText
85
+ wrapper={<Ink.Text color={colors.green} />}
86
+ message="✅ {branch_name} in sync with {origin_branch}"
87
+ values={{
88
+ branch_name: <Brackets>{branch_name}</Brackets>,
89
+ origin_branch: <Brackets>{master_branch}</Brackets>,
90
+ }}
91
+ />,
92
+ );
93
+
94
+ actions.set((state) => {
95
+ state.commit_range = next_commit_range;
96
+ });
97
+
98
+ if (props.onComplete) {
99
+ props.onComplete();
100
+ } else {
101
+ actions.output(<Status />);
102
+ actions.exit(0);
103
+ }
104
+
105
+ async function rebase_master() {
106
+ await cli(`git switch -C "${master_branch_name}" "${master_branch}"`);
107
+ }
108
+
109
+ async function rebase_branch() {
110
+ invariant(commit_range, "commit_range must exist");
111
+
64
112
  const master_sha = (await cli(`git rev-parse ${master_branch}`)).stdout;
65
113
  const rebase_merge_base = master_sha;
66
114
 
@@ -120,40 +168,6 @@ Rebase.run = async function run(props: Props) {
120
168
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
121
169
 
122
170
  restore_git();
123
-
124
- actions.unregister_abort_handler();
125
- } catch (err) {
126
- actions.error("Unable to rebase.");
127
-
128
- if (err instanceof Error) {
129
- actions.error(err.message);
130
- }
131
-
132
- actions.exit(20);
133
- }
134
-
135
- const next_commit_range = await CommitMetadata.range();
136
-
137
- actions.output(
138
- <FormatText
139
- wrapper={<Ink.Text color={colors.green} />}
140
- message="✅ {branch_name} in sync with {origin_branch}"
141
- values={{
142
- branch_name: <Brackets>{branch_name}</Brackets>,
143
- origin_branch: <Brackets>{master_branch}</Brackets>,
144
- }}
145
- />,
146
- );
147
-
148
- actions.set((state) => {
149
- state.commit_range = next_commit_range;
150
- });
151
-
152
- if (props.onComplete) {
153
- props.onComplete();
154
- } else {
155
- actions.output(<Status />);
156
- actions.exit(0);
157
171
  }
158
172
 
159
173
  // cleanup git operations if cancelled during manual rebase