git-stack-cli 2.6.0 → 2.7.0

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/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,
@@ -0,0 +1,106 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { Await } from "~/app/Await";
6
+ import { FormatText } from "~/app/FormatText";
7
+ import { Store } from "~/app/Store";
8
+ import { command } from "~/command";
9
+ import { colors } from "~/core/colors";
10
+ import { invariant } from "~/core/invariant";
11
+
12
+ export function Config() {
13
+ return <Await fallback={null} function={run} />;
14
+
15
+ async function run() {
16
+ const state = Store.getState();
17
+ const actions = state.actions;
18
+
19
+ const config = await get_explicit_args();
20
+ const config_json = JSON.stringify(config).replace(/"/g, '\\"');
21
+
22
+ actions.output(
23
+ <Ink.Box flexDirection="column" gap={1} paddingTop={1}>
24
+ <Ink.Text></Ink.Text>
25
+
26
+ <FormatText
27
+ message="Add the line below to your shell rc file ({zshrc}, {bashrc}, etc.)"
28
+ values={{
29
+ zshrc: <Ink.Text color={colors.gray}>.zshrc</Ink.Text>,
30
+ bashrc: <Ink.Text color={colors.gray}>.bashrc</Ink.Text>,
31
+ }}
32
+ />
33
+
34
+ <FormatText
35
+ message={`{export} {ENV_VAR}{e}{q}{config_json}{q}`}
36
+ values={{
37
+ export: <Ink.Text color={colors.purple}>export</Ink.Text>,
38
+ ENV_VAR: <Ink.Text color={colors.yellow}>{ENV_VAR}</Ink.Text>,
39
+ e: <Ink.Text color={colors.purple}>{"="}</Ink.Text>,
40
+ q: <Ink.Text color={colors.white}>{'"'}</Ink.Text>,
41
+ config_json: <Ink.Text color={colors.green}>{config_json}</Ink.Text>,
42
+ }}
43
+ />
44
+ </Ink.Box>,
45
+ );
46
+
47
+ actions.exit(0);
48
+ }
49
+ }
50
+
51
+ export async function argv_with_config_from_env() {
52
+ if (!process.env.GIT_STACK_CONFIG) {
53
+ return await command(process.argv);
54
+ }
55
+
56
+ const env_config = parse_env_config();
57
+ return await command(process.argv, { env_config });
58
+ }
59
+
60
+ function parse_env_config() {
61
+ const GIT_STACK_CONFIG = process.env.GIT_STACK_CONFIG;
62
+ invariant(GIT_STACK_CONFIG, "GIT_STACK_CONFIG must exist");
63
+
64
+ try {
65
+ const env_config = JSON.parse(GIT_STACK_CONFIG);
66
+ return env_config;
67
+ } catch (error) {
68
+ // eslint-disable-next-line no-console
69
+ console.error(`ERROR GIT_STACK_CONFIG=${GIT_STACK_CONFIG}`);
70
+ // eslint-disable-next-line no-console
71
+ console.error("ERROR GIT_STACK_CONFIG environment variable is not valid JSON");
72
+ process.exit(18);
73
+ }
74
+ }
75
+
76
+ async function get_explicit_args() {
77
+ const default_argv = await command(["git", "stack"], COMMAND_OPTIONS);
78
+ const state_argv = await command(process.argv, COMMAND_OPTIONS);
79
+
80
+ const config: Record<string, any> = {};
81
+
82
+ // find delta between default_argv and argv
83
+ for (const key of Object.keys(state_argv)) {
84
+ if (key === "_" || key === "$0") continue;
85
+
86
+ const state_value = state_argv[key];
87
+ const default_value = default_argv[key];
88
+ const is_set = default_value !== state_value;
89
+ if (is_set) {
90
+ config[key] = state_value;
91
+ }
92
+ }
93
+
94
+ return config;
95
+ }
96
+
97
+ const ENV_VAR = "GIT_STACK_CONFIG";
98
+
99
+ type CommandOptions = NonNullable<Parameters<typeof command>[1]>;
100
+
101
+ const COMMAND_OPTIONS = {
102
+ parserConfiguration: {
103
+ // Should aliases be removed before returning results? Default is `false`
104
+ "strip-aliased": true,
105
+ },
106
+ } satisfies CommandOptions;
@@ -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
@@ -0,0 +1,49 @@
1
+ import * as React from "react";
2
+
3
+ import { colors } from "~/core/colors";
4
+
5
+ type RenderOptions = {
6
+ color: string;
7
+ name: string;
8
+ };
9
+
10
+ type Props = {
11
+ children: (render_options: RenderOptions) => React.ReactNode;
12
+ };
13
+
14
+ export function ColorTest(props: Props) {
15
+ return (
16
+ <React.Fragment>
17
+ {Object.entries(colors).map(([key, color]) => {
18
+ const name = `colors:${key}`;
19
+ return props.children({ color, name });
20
+ })}
21
+
22
+ {INK_COLORS.map((color) => {
23
+ const name = `ink:${color}`;
24
+ return props.children({ color, name });
25
+ })}
26
+ </React.Fragment>
27
+ );
28
+ }
29
+
30
+ // ForegroundColor
31
+ // https://github.com/magus/git-stack-cli/blob/master/node_modules/.pnpm/chalk@5.3.0/node_modules/chalk/source/vendor/ansi-styles/index.d.ts#L75
32
+ const INK_COLORS = [
33
+ "black",
34
+ "red",
35
+ "green",
36
+ "yellow",
37
+ "blue",
38
+ "cyan",
39
+ "magenta",
40
+ "white",
41
+ "blackBright",
42
+ "redBright",
43
+ "greenBright",
44
+ "yellowBright",
45
+ "blueBright",
46
+ "cyanBright",
47
+ "magentaBright",
48
+ "whiteBright",
49
+ ];
package/src/core/cli.ts CHANGED
@@ -7,6 +7,7 @@ type SpawnOptions = Parameters<typeof child.spawn>[2];
7
7
 
8
8
  type Options = SpawnOptions & {
9
9
  ignoreExitCode?: boolean;
10
+ onOutput?: (data: string) => void;
10
11
  };
11
12
 
12
13
  type Return = {
@@ -51,6 +52,7 @@ export async function cli(
51
52
  function write_output(value: string) {
52
53
  output += value;
53
54
  state.actions.debug(value, id);
55
+ options.onOutput?.(value);
54
56
  }
55
57
 
56
58
  childProcess.stdout?.on("data", (data: Buffer) => {
@@ -1,7 +1,7 @@
1
1
  // ink uses chalk internally
2
2
  // https://github.com/vadimdemedes/ink#color
3
3
 
4
- export const colors = {
4
+ export const colors = Object.freeze({
5
5
  red: "rgb(248, 81, 73)",
6
6
  // red-emphasis rgb(218, 54, 51)
7
7
 
@@ -20,4 +20,6 @@ export const colors = {
20
20
  gray: "rgb(110, 118, 129)",
21
21
 
22
22
  lightGray: "rgb(125, 133, 144)",
23
- };
23
+
24
+ white: "whiteBright",
25
+ });
package/src/index.tsx CHANGED
@@ -11,13 +11,13 @@ import * as Ink from "ink-cjs";
11
11
 
12
12
  import { App } from "~/app/App";
13
13
  import { Store } from "~/app/Store";
14
- import { command } from "~/command";
14
+ import { argv_with_config_from_env } from "~/commands/Config";
15
15
  import { get_tmp_dir } from "~/core/get_tmp_dir";
16
16
  import { pretty_json } from "~/core/pretty_json";
17
17
 
18
18
  (async function main() {
19
19
  try {
20
- const argv = await command();
20
+ const argv = await argv_with_config_from_env();
21
21
 
22
22
  // required to get bun working with ink
23
23
  // https://github.com/oven-sh/bun/issues/6862#issuecomment-2429444852
@@ -4,6 +4,6 @@ declare namespace NodeJS {
4
4
  DEV?: "true" | "false";
5
5
  CLI_VERSION?: string;
6
6
  GIT_SEQUENCE_EDITOR_SCRIPT?: string;
7
- GIT_STACK_BRANCH_PREFIX?: string;
7
+ GIT_STACK_CONFIG?: string;
8
8
  }
9
9
  }
@@ -1,29 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { Store } from "~/app/Store";
4
- import { YesNoPrompt } from "~/app/YesNoPrompt";
5
-
6
- export function PreSelectCommitRanges() {
7
- const actions = Store.useActions();
8
- const argv = Store.useState((state) => state.argv);
9
-
10
- React.useEffect(() => {
11
- if (argv.force) {
12
- Store.setState((state) => {
13
- state.step = "select-commit-ranges";
14
- });
15
- }
16
- }, [argv]);
17
-
18
- return (
19
- <YesNoPrompt
20
- message="Some commits are new or outdated, would you like to select new commit ranges?"
21
- onYes={() => {
22
- actions.set((state) => {
23
- state.step = "select-commit-ranges";
24
- });
25
- }}
26
- onNo={() => actions.exit(0)}
27
- />
28
- );
29
- }
@@ -1,5 +0,0 @@
1
- import { short_id } from "~/core/short_id";
2
-
3
- export function gs_short_id() {
4
- return `gs-${short_id()}`;
5
- }