git-stack-cli 2.6.1 → 2.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "2.6.1",
3
+ "version": "2.7.1",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
package/src/app/App.tsx CHANGED
@@ -17,6 +17,7 @@ import { RebaseCheck } from "~/app/RebaseCheck";
17
17
  import { RequireBranch } from "~/app/RequireBranch";
18
18
  import { Store } from "~/app/Store";
19
19
  import { VerboseDebugInfo } from "~/app/VerboseDebugInfo";
20
+ import { Config } from "~/commands/Config";
20
21
  import { Fixup } from "~/commands/Fixup";
21
22
  import { Log } from "~/commands/Log";
22
23
  import { Rebase } from "~/commands/Rebase";
@@ -92,6 +93,8 @@ function MaybeMain() {
92
93
  return <Log />;
93
94
  } else if (positional_list.has("update")) {
94
95
  return <Update />;
96
+ } else if (positional_list.has("config")) {
97
+ return <Config />;
95
98
  } else if (positional_list.has("rebase")) {
96
99
  return (
97
100
  <DependencyCheck>
@@ -5,6 +5,7 @@ import * as Ink from "ink-cjs";
5
5
  import { Brackets } from "~/app/Brackets";
6
6
  import { Command } from "~/app/Command";
7
7
  import { FormatText } from "~/app/FormatText";
8
+ import { Url } from "~/app/Url";
8
9
  import { YesNoPrompt } from "~/app/YesNoPrompt";
9
10
  import { assertNever } from "~/core/assertNever";
10
11
  import { cli } from "~/core/cli";
@@ -26,10 +27,9 @@ type Props = {
26
27
  };
27
28
 
28
29
  type State = {
29
- error: null | Error;
30
+ status: "init" | "prompt" | "install" | "done";
30
31
  local_version: null | string;
31
32
  latest_version: null | string;
32
- status: "init" | "prompt" | "install" | "done";
33
33
  is_brew_bun_standalone: boolean;
34
34
  };
35
35
 
@@ -44,24 +44,94 @@ export function AutoUpdate(props: Props) {
44
44
  const [output, set_output] = React.useState<Array<React.ReactNode>>([]);
45
45
 
46
46
  const [state, patch] = React.useReducer(reducer, {
47
- error: null,
47
+ status: "init",
48
48
  local_version: null,
49
49
  latest_version: null,
50
- status: "init",
51
50
  is_brew_bun_standalone: false,
51
+
52
+ // // debugging
53
+ // status: "prompt",
54
+ // local_version: "2.5.3",
55
+ // latest_version: "2.7.0",
56
+ // is_brew_bun_standalone: true,
52
57
  });
53
58
 
54
- function handle_output(node: React.ReactNode) {
55
- if (typeof props.onOutput === "function") {
56
- props.onOutput(node);
57
- } else {
58
- set_output((current) => {
59
- return [...current, node];
60
- });
59
+ React.useEffect(handle_init_state, []);
60
+ React.useEffect(handle_status, [state.latest_version]);
61
+ React.useEffect(handle_on_done, [state.status]);
62
+
63
+ const status = render_status();
64
+
65
+ return (
66
+ <React.Fragment>
67
+ {output}
68
+ {status}
69
+ </React.Fragment>
70
+ );
71
+
72
+ function render_status() {
73
+ switch (state.status) {
74
+ case "init":
75
+ return null;
76
+
77
+ case "install":
78
+ return null;
79
+
80
+ case "done":
81
+ return props.children;
82
+
83
+ case "prompt": {
84
+ let install_command = "";
85
+ if (state.is_brew_bun_standalone) {
86
+ install_command = "brew install magus/git-stack/git-stack";
87
+ } else {
88
+ install_command = `npm install -g ${props.name}@latest`;
89
+ }
90
+
91
+ return (
92
+ <YesNoPrompt
93
+ message={
94
+ <Ink.Box flexDirection="column" gap={1}>
95
+ <Command>{install_command}</Command>
96
+ <FormatText
97
+ wrapper={<Ink.Text color={colors.yellow} />}
98
+ message="Would you like to run the above command to update?"
99
+ />
100
+ </Ink.Box>
101
+ }
102
+ onNo={() => {
103
+ patch({ status: "done" });
104
+ }}
105
+ onYes={async () => {
106
+ info(<Command>{install_command}</Command>);
107
+
108
+ patch({ status: "install" });
109
+
110
+ await cli(install_command, {
111
+ env: {
112
+ ...process.env,
113
+ HOMEBREW_COLOR: "1",
114
+ },
115
+ onOutput: (data: string) => {
116
+ info(<Ink.Text>{data}</Ink.Text>);
117
+ },
118
+ });
119
+
120
+ info(
121
+ <Ink.Text key="done">
122
+ ✅ Installed <Brackets>{state.latest_version}</Brackets>
123
+ </Ink.Text>,
124
+ );
125
+
126
+ patch({ status: "done" });
127
+ }}
128
+ />
129
+ );
130
+ }
61
131
  }
62
132
  }
63
133
 
64
- React.useEffect(() => {
134
+ function handle_on_done() {
65
135
  switch (state.status) {
66
136
  case "init":
67
137
  case "prompt":
@@ -76,199 +146,165 @@ export function AutoUpdate(props: Props) {
76
146
  default:
77
147
  assertNever(state.status);
78
148
  }
79
- }, [state.status]);
149
+ }
80
150
 
81
- React.useEffect(() => {
82
- let status: State["status"] = "init";
83
- let latest_version: string | null = null;
84
- let is_brew_bun_standalone = false;
151
+ function handle_init_state() {
152
+ init_state().catch(abort);
85
153
 
86
- const local_version = process.env.CLI_VERSION;
87
- const is_output = props_ref.current.verbose || props_ref.current.force;
154
+ async function init_state() {
155
+ if (state.latest_version !== null) return;
88
156
 
89
- async function auto_update() {
90
- if (!local_version) {
91
- throw new Error("Auto update requires process.env.CLI_VERSION to be set");
92
- }
157
+ const local_version = process.env.CLI_VERSION;
158
+ const latest_version = await get_latest_version();
159
+ const is_brew_bun_standalone = get_is_brew_bun_standalone();
160
+ patch({ local_version, latest_version, is_brew_bun_standalone });
161
+ }
93
162
 
163
+ async function get_latest_version() {
94
164
  const timeout_ms = is_finite_value(props.timeoutMs) ? props.timeoutMs : 2 * 1000;
95
165
 
96
166
  const npm_json = await Promise.race([
97
167
  fetch_json(`https://registry.npmjs.org/${props.name}`),
98
168
 
99
169
  sleep(timeout_ms).then(() => {
100
- throw new Error("AutoUpdate timeout");
170
+ abort(new Error("AutoUpdate timeout"));
101
171
  }),
102
172
  ]);
103
173
 
104
- latest_version = npm_json?.["dist-tags"]?.latest;
174
+ const maybe_version = npm_json?.["dist-tags"]?.latest;
105
175
 
106
- if (!latest_version) {
107
- throw new Error("Unable to retrieve latest version from npm");
176
+ if (typeof maybe_version === "string") {
177
+ return maybe_version;
108
178
  }
109
179
 
180
+ throw new Error("Unable to retrieve latest version from npm");
181
+ }
182
+
183
+ function get_is_brew_bun_standalone() {
110
184
  const binary_path = process.argv[1];
185
+ debug(<Ink.Text dimColor>{JSON.stringify({ binary_path })}</Ink.Text>);
186
+
187
+ const is_bunfs_path = binary_path.startsWith("/$bunfs");
188
+ debug(
189
+ <Ink.Text dimColor>
190
+ {is_bunfs_path
191
+ ? "brew install detected (compiled bun standalone)"
192
+ : "npm install detected"}
193
+ </Ink.Text>,
194
+ );
195
+
196
+ return is_bunfs_path;
197
+ }
198
+ }
199
+
200
+ function handle_status() {
201
+ const latest_version = state.latest_version;
202
+
203
+ if (latest_version === null) {
204
+ return;
205
+ }
206
+
207
+ const local_version = state.local_version;
208
+
209
+ if (!local_version) {
210
+ throw new Error("Auto update requires process.env.CLI_VERSION to be set");
211
+ }
212
+
213
+ debug(
214
+ <FormatText
215
+ key="versions"
216
+ wrapper={<Ink.Text dimColor />}
217
+ message="Auto update found latest version {latest_version} and current local version {local_version}"
218
+ values={{
219
+ latest_version: <Brackets>{latest_version}</Brackets>,
220
+ local_version: <Brackets>{local_version}</Brackets>,
221
+ }}
222
+ />,
223
+ );
224
+
225
+ const semver_result = semver_compare(latest_version, local_version);
226
+ debug(<Ink.Text dimColor>{JSON.stringify({ semver_result })}</Ink.Text>);
227
+
228
+ switch (semver_result) {
229
+ case 0: {
230
+ info(
231
+ <Ink.Text>
232
+ ✅ Everything up to date. <Brackets>{latest_version}</Brackets>
233
+ </Ink.Text>,
234
+ );
111
235
 
112
- if (props_ref.current.verbose) {
113
- handle_output(<Ink.Text dimColor>{JSON.stringify({ binary_path })}</Ink.Text>);
236
+ return patch({ status: "done" });
114
237
  }
115
238
 
116
- is_brew_bun_standalone = binary_path.startsWith("/$bunfs");
239
+ case 1: {
240
+ const old_tag = local_version;
241
+ const new_tag = state.latest_version;
242
+ const url = `https://github.com/magus/git-stack-cli/compare/${old_tag}...${new_tag}`;
117
243
 
118
- if (props_ref.current.verbose) {
119
- if (is_brew_bun_standalone) {
120
- handle_output(
121
- <Ink.Text dimColor>brew install detected (compiled bun standalone)</Ink.Text>,
122
- );
123
- } else {
124
- handle_output(<Ink.Text dimColor>npm install detected</Ink.Text>);
125
- }
244
+ info(
245
+ <Ink.Box flexDirection="column" gap={1} paddingTop={1} paddingBottom={1}>
246
+ <Ink.Text>
247
+ 🆕 New version available! <Brackets>{latest_version}</Brackets>
248
+ </Ink.Text>
249
+ <Ink.Box flexDirection="column">
250
+ <Ink.Text dimColor>Changelog</Ink.Text>
251
+ <Url>{url}</Url>
252
+ </Ink.Box>
253
+ </Ink.Box>,
254
+ );
255
+
256
+ return patch({ status: "prompt" });
126
257
  }
127
258
 
128
- if (props_ref.current.verbose) {
129
- handle_output(
259
+ case -1: {
260
+ info(
130
261
  <FormatText
131
- key="versions"
132
- wrapper={<Ink.Text />}
133
- message="Auto update found latest version {latest_version} and current local version {local_version}"
262
+ message="⚠️ Local version {local_version} is newer than latest version {latest_version}"
134
263
  values={{
135
- latest_version: <Brackets>{latest_version}</Brackets>,
136
264
  local_version: <Brackets>{local_version}</Brackets>,
265
+ latest_version: <Brackets>{latest_version}</Brackets>,
137
266
  }}
138
267
  />,
139
268
  );
140
- }
141
269
 
142
- const semver_result = semver_compare(latest_version, local_version);
143
- if (props_ref.current.verbose) {
144
- handle_output(<Ink.Text dimColor>{JSON.stringify({ semver_result })}</Ink.Text>);
270
+ return patch({ status: "done" });
145
271
  }
146
272
 
147
- if (semver_result === 0) {
148
- status = "done";
149
-
150
- if (is_output) {
151
- handle_output(
152
- <Ink.Text>
153
- ✅ Everything up to date. <Brackets>{latest_version}</Brackets>
154
- </Ink.Text>,
155
- );
156
- }
157
- return;
158
- }
159
-
160
- if (semver_result === 1) {
161
- // trigger yes no prompt
162
- status = "prompt";
273
+ default: {
274
+ assertNever(semver_result);
275
+ abort(new Error("AutoUpdate failed"));
163
276
  }
277
+ }
278
+ }
164
279
 
165
- throw new Error("AutoUpdate failed");
280
+ function info(node: React.ReactNode) {
281
+ if (props_ref.current.verbose || props_ref.current.force) {
282
+ handle_output(node);
166
283
  }
284
+ }
167
285
 
168
- const onError = props_ref.current.onError || (() => {});
169
-
170
- auto_update()
171
- .then(() => {
172
- patch({ status, local_version, latest_version, is_brew_bun_standalone });
173
- })
174
- .catch((error) => {
175
- if (props_ref.current.verbose) {
176
- handle_output(
177
- <Ink.Text key="error" color={colors.red}>
178
- {error?.message}
179
- </Ink.Text>,
180
- );
181
- }
286
+ function debug(node: React.ReactNode) {
287
+ if (props_ref.current.verbose) {
288
+ handle_output(node);
289
+ }
290
+ }
291
+ function abort(error: Error) {
292
+ info(
293
+ <Ink.Text key="error" color={colors.red}>
294
+ {error.message}
295
+ </Ink.Text>,
296
+ );
297
+ patch({ status: "done" });
298
+ props_ref.current.onError?.(error);
299
+ }
182
300
 
183
- // ensure we always exit
184
- status = "done";
185
- patch({ status, error, local_version, latest_version, is_brew_bun_standalone });
186
- onError(error);
301
+ function handle_output(node: React.ReactNode) {
302
+ if (typeof props.onOutput === "function") {
303
+ props.onOutput(node);
304
+ } else {
305
+ set_output((current) => {
306
+ return [...current, node];
187
307
  });
188
- }, []);
189
-
190
- const status = (function render_status() {
191
- switch (state.status) {
192
- case "init":
193
- return null;
194
-
195
- case "prompt": {
196
- let install_command = "";
197
- if (state.is_brew_bun_standalone) {
198
- install_command = "brew install magus/git-stack/git-stack";
199
- } else {
200
- install_command = `npm install -g ${props.name}@latest`;
201
- }
202
-
203
- return (
204
- <YesNoPrompt
205
- message={
206
- <Ink.Box flexDirection="column">
207
- <Ink.Box flexDirection="column">
208
- <Ink.Text color={colors.yellow}>
209
- <FormatText
210
- wrapper={<Ink.Text />}
211
- message="New version available {latest_version}"
212
- values={{
213
- latest_version: <Brackets>{state.latest_version}</Brackets>,
214
- }}
215
- />
216
- ,
217
- </Ink.Text>
218
- <Ink.Text> </Ink.Text>
219
- <Command>{install_command}</Command>
220
- <Ink.Text> </Ink.Text>
221
- </Ink.Box>
222
- <Ink.Box>
223
- <FormatText
224
- wrapper={<Ink.Text color={colors.yellow} />}
225
- message="Would you like to run the above command to update?"
226
- />
227
- </Ink.Box>
228
- </Ink.Box>
229
- }
230
- onYes={async () => {
231
- handle_output(<Command>{install_command}</Command>);
232
-
233
- patch({ status: "install" });
234
-
235
- await cli(install_command, {
236
- env: {
237
- ...process.env,
238
- HOMEBREW_COLOR: "1",
239
- },
240
- onOutput: (data: string) => {
241
- handle_output(<Ink.Text>{data}</Ink.Text>);
242
- },
243
- });
244
-
245
- handle_output(
246
- <Ink.Text key="done">
247
- ✅ Installed <Brackets>{state.latest_version}</Brackets>
248
- </Ink.Text>,
249
- );
250
-
251
- patch({ status: "done" });
252
- }}
253
- onNo={() => {
254
- patch({ status: "done" });
255
- }}
256
- />
257
- );
258
- }
259
-
260
- case "install":
261
- return null;
262
-
263
- case "done":
264
- return props.children;
265
308
  }
266
- })();
267
-
268
- return (
269
- <React.Fragment>
270
- {output}
271
- {status}
272
- </React.Fragment>
273
- );
309
+ }
274
310
  }
@@ -10,8 +10,8 @@ import { Parens } from "~/app/Parens";
10
10
  import { Store } from "~/app/Store";
11
11
  import { TextInput } from "~/app/TextInput";
12
12
  import { colors } from "~/core/colors";
13
- import { gs_short_id } from "~/core/gs_short_id";
14
13
  import { invariant } from "~/core/invariant";
14
+ import { short_id } from "~/core/short_id";
15
15
  import { wrap_index } from "~/core/wrap_index";
16
16
 
17
17
  import type { State } from "~/app/Store";
@@ -35,6 +35,8 @@ function SelectCommitRangesInternal(props: Props) {
35
35
  const actions = Store.useActions();
36
36
 
37
37
  const argv = Store.useState((state) => state.argv);
38
+ const branch_name = Store.useState((state) => state.branch_name);
39
+ invariant(branch_name, "branch_name must exist");
38
40
 
39
41
  const [selected_group_id, set_selected_group_id] = React.useState(() => {
40
42
  const first_group = props.commit_range.group_list.find(
@@ -444,17 +446,7 @@ function SelectCommitRangesInternal(props: Props) {
444
446
  );
445
447
 
446
448
  function get_group_id() {
447
- let branch_prefix = "";
448
-
449
- // branch prefix via cli flag or env var
450
- // cli flag takes precedence since it is more explicit
451
- if (argv["branch-prefix"]) {
452
- branch_prefix = argv["branch-prefix"];
453
- } else if (process.env.GIT_STACK_BRANCH_PREFIX) {
454
- branch_prefix = process.env.GIT_STACK_BRANCH_PREFIX;
455
- }
456
-
457
- return `${branch_prefix}${gs_short_id()}`;
449
+ return `${branch_name}-${short_id()}`;
458
450
  }
459
451
 
460
452
  function submit_group_input(title: string) {
package/src/command.ts CHANGED
@@ -1,54 +1,78 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
 
4
- import type { Options, InferredOptionTypes, Arguments } from "yargs";
4
+ import type { Options, InferredOptionTypes, Arguments, ParserConfigurationOptions } from "yargs";
5
5
 
6
6
  export type Argv = Arguments & TGlobalOptions & TFixupOptions & TDefaultOptions;
7
7
 
8
- export async function command() {
8
+ type CommandOptions = {
9
+ env_config?: Partial<Argv>;
10
+ parserConfiguration?: Partial<ParserConfigurationOptions>;
11
+ };
12
+
13
+ export async function command(argv: string[], options: CommandOptions = {}) {
9
14
  // https://yargs.js.org/docs/#api-reference-optionkey-opt
10
- return (
11
- yargs(hideBin(process.argv))
12
- .scriptName("git stack")
13
- .usage("Usage: git stack [command] [options]")
14
-
15
- .command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions))
16
-
17
- .command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) =>
18
- yargs.positional("commit", FixupOptions.commit),
19
- )
20
-
21
- .command(
22
- "log [args...]",
23
- "Print an abbreviated log with numbered commits, useful for git stack fixup",
24
- (yargs) => yargs.strict(false),
25
- )
26
-
27
- .command(
28
- "rebase",
29
- "Update local branch via rebase with latest changes from origin master branch",
30
- (yargs) => yargs,
31
- )
32
-
33
- .command(
34
- ["update", "upgrade"],
35
- "Check and install the latest version of git stack",
36
- (yargs) => yargs,
37
- )
38
-
39
- .option("verbose", GlobalOptions.verbose)
40
-
41
- // yargs default wraps to 80 columns
42
- // passing null will wrap to terminal width
43
- // value below if what seems to look decent
44
- .wrap(123)
45
-
46
- // disallow unknown options
47
- .strict()
48
- .version(process.env.CLI_VERSION || "unknown")
49
- .showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
50
- .help("help", "Show usage via `git stack help`").argv as unknown as Promise<Argv>
51
- );
15
+ let builder = yargs(hideBin(argv));
16
+
17
+ if (options.parserConfiguration) {
18
+ builder = builder.parserConfiguration(options.parserConfiguration);
19
+ }
20
+
21
+ // apply overrides from config
22
+ // higher precedence than defaults, but lower precendence than cli flags
23
+ // perfect since that's what we want, prefer config only if not explicitly set on cli
24
+ if (options.env_config) {
25
+ builder = builder.config(options.env_config);
26
+ }
27
+
28
+ const parsed = await builder
29
+ .scriptName("git stack")
30
+ .usage("Usage: git stack [command] [options]")
31
+
32
+ .command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions))
33
+
34
+ .command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) =>
35
+ yargs.positional("commit", FixupOptions.commit),
36
+ )
37
+
38
+ .command(
39
+ "log [args...]",
40
+ "Print an abbreviated log with numbered commits, useful for git stack fixup",
41
+ (yargs) => yargs.strict(false),
42
+ )
43
+
44
+ .command(
45
+ "rebase",
46
+ "Update local branch via rebase with latest changes from origin master branch",
47
+ (yargs) => yargs,
48
+ )
49
+
50
+ .command(
51
+ ["update", "upgrade"],
52
+ "Check and install the latest version of git stack",
53
+ (yargs) => yargs,
54
+ )
55
+ .command(
56
+ "config",
57
+ "Generate a one-time configuration json based on the passed arguments",
58
+ (yargs) => yargs.options(DefaultOptions),
59
+ )
60
+
61
+ .option("verbose", GlobalOptions.verbose)
62
+
63
+ // yargs default wraps to 80 columns
64
+ // passing null will wrap to terminal width
65
+ // value below if what seems to look decent
66
+ .wrap(123)
67
+
68
+ // disallow unknown options
69
+ .strict()
70
+ .version(process.env.CLI_VERSION || "unknown")
71
+ .showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
72
+ .help("help", "Show usage via `git stack help`");
73
+
74
+ const result = parsed.argv as unknown as Argv;
75
+ return result;
52
76
  }
53
77
 
54
78
  const GlobalOptions = {
@@ -109,12 +133,6 @@ const DefaultOptions = {
109
133
  description: "Open all PRs as drafts",
110
134
  },
111
135
 
112
- "branch-prefix": {
113
- type: "string",
114
- default: "",
115
- description: "Prefix for generated branch names, e.g. dev/magus/",
116
- },
117
-
118
136
  "revise-sign": {
119
137
  type: "boolean",
120
138
  default: true,