git-stack-cli 0.7.6 → 0.8.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/dist/app/App.js CHANGED
@@ -8,6 +8,7 @@ import { LocalCommitStatus } from "./LocalCommitStatus.js";
8
8
  import { Main } from "./Main.js";
9
9
  import { Output } from "./Output.js";
10
10
  import { Providers } from "./Providers.js";
11
+ import { RebaseCheck } from "./RebaseCheck.js";
11
12
  import { Store } from "./Store.js";
12
13
  export function App() {
13
14
  const actions = Store.useActions();
@@ -27,10 +28,15 @@ export function App() {
27
28
  return (React.createElement(Providers, null,
28
29
  React.createElement(Debug, null),
29
30
  React.createElement(Output, null),
30
- !argv.verbose ? null : React.createElement(GithubApiError, null),
31
- React.createElement(DependencyCheck, null,
32
- React.createElement(AutoUpdate, { name: "git-stack-cli", verbose: argv.verbose, onOutput: actions.output },
33
- React.createElement(GatherMetadata, null,
34
- React.createElement(LocalCommitStatus, null,
35
- React.createElement(Main, null)))))));
31
+ React.createElement(AutoUpdate, { name: "git-stack-cli", verbose: argv.verbose || argv.update, timeoutMs: argv.update ? 30 * 1000 : 2 * 1000, onOutput: actions.output, onDone: () => {
32
+ if (argv.update) {
33
+ actions.exit(0);
34
+ }
35
+ } },
36
+ React.createElement(RebaseCheck, null,
37
+ React.createElement(DependencyCheck, null,
38
+ !argv.verbose ? null : React.createElement(GithubApiError, null),
39
+ React.createElement(GatherMetadata, null,
40
+ React.createElement(LocalCommitStatus, null,
41
+ React.createElement(Main, null))))))));
36
42
  }
@@ -5,12 +5,12 @@ import * as Ink from "ink";
5
5
  import { cli } from "../core/cli.js";
6
6
  import { colors } from "../core/colors.js";
7
7
  import { fetch_json } from "../core/fetch_json.js";
8
+ import { is_finite_value } from "../core/is_finite_value.js";
8
9
  import { read_json } from "../core/read_json.js";
9
10
  import { semver_compare } from "../core/semver_compare.js";
10
11
  import { sleep } from "../core/sleep.js";
11
12
  import { Brackets } from "./Brackets.js";
12
13
  import { FormatText } from "./FormatText.js";
13
- import { Parens } from "./Parens.js";
14
14
  import { YesNoPrompt } from "./YesNoPrompt.js";
15
15
  function reducer(state, patch) {
16
16
  return { ...state, ...patch };
@@ -41,31 +41,33 @@ export function AutoUpdate(props) {
41
41
  let latest_version = null;
42
42
  async function auto_update() {
43
43
  if (props_ref.current.verbose) {
44
- handle_output(React.createElement(Ink.Text, { key: "init", dimColor: true }, "Checking for latest version..."));
44
+ handle_output(React.createElement(Ink.Text, { key: "init" }, "Checking for latest version..."));
45
45
  }
46
- const timeout_ms = 2 * 1000;
46
+ const timeout_ms = is_finite_value(props.timeoutMs)
47
+ ? props.timeoutMs
48
+ : 2 * 1000;
47
49
  const npm_json = await Promise.race([
48
50
  fetch_json(`https://registry.npmjs.org/${props.name}`),
49
51
  sleep(timeout_ms).then(() => {
50
- throw new Error("timeout");
52
+ throw new Error("Timeout");
51
53
  }),
52
54
  ]);
53
55
  latest_version = npm_json?.["dist-tags"]?.latest;
54
56
  if (!latest_version) {
55
- throw new Error("unable to retrieve latest version from npm");
57
+ throw new Error("Unable to retrieve latest version from npm");
56
58
  }
57
59
  const script_dir = path.dirname(fs.realpathSync(process.argv[1]));
58
60
  const package_json_path = path.join(script_dir, "..", "package.json");
59
61
  const package_json = read_json(package_json_path);
60
62
  if (!package_json) {
61
63
  // unable to find read package.json, skip auto update
62
- throw new Error(`unable to find read package.json [${package_json_path}]`);
64
+ throw new Error(`Unable to find read package.json [${package_json_path}]`);
63
65
  }
64
66
  local_version = package_json.version;
65
67
  if (props_ref.current.verbose) {
66
- handle_output(React.createElement(FormatText, { key: "versions", wrapper: React.createElement(Ink.Text, { dimColor: true }), message: "Auto update found latest version {latest_version} and current local version {local_version}", values: {
68
+ handle_output(React.createElement(FormatText, { key: "versions", wrapper: React.createElement(Ink.Text, null), message: "Auto update found latest version {latest_version} and current local version {local_version}", values: {
67
69
  latest_version: React.createElement(Brackets, null, latest_version),
68
- local_version: React.createElement(Parens, null, local_version),
70
+ local_version: React.createElement(Brackets, null, local_version),
69
71
  } }));
70
72
  }
71
73
  const semver_result = semver_compare(latest_version, local_version);
@@ -88,8 +90,11 @@ export function AutoUpdate(props) {
88
90
  patch({ status, error, local_version, latest_version });
89
91
  onError(error);
90
92
  if (props_ref.current.verbose) {
91
- handle_output(React.createElement(Ink.Text, { key: "error", dimColor: true, color: colors.red }, error?.message));
93
+ handle_output(React.createElement(Ink.Text, { key: "error", color: colors.red }, error?.message));
92
94
  }
95
+ })
96
+ .finally(() => {
97
+ props.onDone?.();
93
98
  });
94
99
  }, []);
95
100
  const status = (function render_status() {
@@ -105,7 +110,7 @@ export function AutoUpdate(props) {
105
110
  patch({ status: "install" });
106
111
  await cli(`npm install -g ${props.name}@latest`);
107
112
  patch({ status: "exit" });
108
- handle_output(React.createElement(Ink.Text, { key: "done", dimColor: true }, "Auto update done."));
113
+ handle_output(React.createElement(Ink.Text, { key: "done" }, "Auto update done."));
109
114
  }, onNo: () => {
110
115
  patch({ status: "done" });
111
116
  } }));
@@ -1,33 +1,60 @@
1
1
  import * as React from "react";
2
2
  import * as Ink from "ink";
3
3
  import { cli } from "../core/cli.js";
4
+ import { colors } from "../core/colors.js";
4
5
  import { invariant } from "../core/invariant.js";
5
6
  import { match_group } from "../core/match_group.js";
6
7
  import { Await } from "./Await.js";
8
+ import { Brackets } from "./Brackets.js";
9
+ import { FormatText } from "./FormatText.js";
7
10
  import { Store } from "./Store.js";
8
11
  export function GatherMetadata(props) {
9
12
  const argv = Store.useState((state) => state.argv);
10
13
  invariant(argv, "argv must exist");
11
- const fallback = (React.createElement(Ink.Text, { color: "yellow" }, "Gathering local git information..."));
14
+ const fallback = (React.createElement(Ink.Text, { color: colors.yellow }, "Gathering local git information..."));
12
15
  return (React.createElement(Await, { fallback: fallback, function: gather_metadata }, props.children));
13
16
  }
14
17
  async function gather_metadata() {
15
18
  const actions = Store.getState().actions;
19
+ const argv = Store.getState().argv;
20
+ invariant(argv, "argv must exist");
16
21
  try {
22
+ // default to master branch, fallback to main
23
+ let master_branch;
24
+ if (argv.branch) {
25
+ actions.debug(React.createElement(FormatText, { message: "Setting master branch to {branch}", values: {
26
+ branch: React.createElement(Brackets, null, argv.branch),
27
+ } }));
28
+ master_branch = argv.branch;
29
+ }
30
+ else {
31
+ const detect_master = await cli(`git branch --list "${BRANCH.master}" --color=never`);
32
+ if (detect_master.stdout !== "") {
33
+ master_branch = BRANCH.master;
34
+ }
35
+ else {
36
+ actions.debug(React.createElement(FormatText, { message: "Could not find {master} branch, falling back to {main}", values: {
37
+ master: React.createElement(Brackets, null, BRANCH.master),
38
+ main: React.createElement(Brackets, null, BRANCH.main),
39
+ } }));
40
+ master_branch = BRANCH.main;
41
+ }
42
+ }
17
43
  const branch_name = (await cli("git rev-parse --abbrev-ref HEAD")).stdout;
18
44
  // handle when there are no detected changes
19
- if (branch_name === "master") {
45
+ if (branch_name === master_branch) {
20
46
  actions.newline();
21
47
  actions.error("Must run within a branch.");
22
48
  actions.exit(0);
23
49
  return;
24
50
  }
25
51
  const head = (await cli("git rev-parse HEAD")).stdout;
26
- const merge_base = (await cli("git merge-base HEAD master")).stdout;
52
+ const merge_base = (await cli(`git merge-base HEAD ${master_branch}`))
53
+ .stdout;
27
54
  // handle when there are no detected changes
28
55
  if (head === merge_base) {
29
56
  actions.newline();
30
- actions.output(React.createElement(Ink.Text, { color: "gray" }, "No changes detected."));
57
+ actions.output(React.createElement(Ink.Text, { color: colors.gray }, "No changes detected."));
31
58
  actions.exit(0);
32
59
  return;
33
60
  }
@@ -37,6 +64,7 @@ async function gather_metadata() {
37
64
  const repo_path = match_group(origin_url, RE.repo_path, "repo_path");
38
65
  Store.setState((state) => {
39
66
  state.repo_path = repo_path;
67
+ state.master_branch = master_branch;
40
68
  state.head = head;
41
69
  state.merge_base = merge_base;
42
70
  state.branch_name = branch_name;
@@ -49,6 +77,7 @@ async function gather_metadata() {
49
77
  actions.error(err.message);
50
78
  }
51
79
  }
80
+ actions.exit(7);
52
81
  }
53
82
  }
54
83
  const RE = {
@@ -56,3 +85,7 @@ const RE = {
56
85
  // https://github.com/magus/git-multi-diff-playground.git
57
86
  repo_path: /(?<repo_path>[^:^/]+\/[^/]+)\.git/,
58
87
  };
88
+ const BRANCH = {
89
+ master: "master",
90
+ main: "main",
91
+ };
@@ -108,6 +108,9 @@ async function run() {
108
108
  const spawn_options = { ignoreExitCode: true };
109
109
  // always clean up any patch files
110
110
  cli.sync(`rm ${PATCH_FILE}`, spawn_options);
111
+ // always hard reset to allow subsequent checkout
112
+ // if there are files checkout will fail and cascade fail subsequent commands
113
+ cli.sync(`git reset --hard`, spawn_options);
111
114
  // always put self back in original branch
112
115
  cli.sync(`git checkout ${branch_name}`, spawn_options);
113
116
  // ...and cleanup temporary branch
@@ -167,6 +167,9 @@ async function run(props) {
167
167
  const spawn_options = { ignoreExitCode: true };
168
168
  // always clean up any patch files
169
169
  cli.sync(`rm ${PATCH_FILE}`, spawn_options);
170
+ // always hard reset to allow subsequent checkout
171
+ // if there are files checkout will fail and cascade fail subsequent commands
172
+ cli.sync(`git reset --hard`, spawn_options);
170
173
  // always put self back in original branch
171
174
  cli.sync(`git checkout ${branch_name}`, spawn_options);
172
175
  // ...and cleanup temporary branch
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import * as Ink from "ink";
5
+ import { cli } from "../core/cli.js";
6
+ import { colors } from "../core/colors.js";
7
+ import { invariant } from "../core/invariant.js";
8
+ import { Await } from "./Await.js";
9
+ import { Store } from "./Store.js";
10
+ import { YesNoPrompt } from "./YesNoPrompt.js";
11
+ function reducer(state, patch) {
12
+ return { ...state, ...patch };
13
+ }
14
+ export function RebaseCheck(props) {
15
+ const actions = Store.useActions();
16
+ const argv = Store.useState((state) => state.argv);
17
+ invariant(argv, "argv must exist");
18
+ const [state, patch] = React.useReducer(reducer, {
19
+ status: "init",
20
+ });
21
+ switch (state.status) {
22
+ case "done":
23
+ return props.children;
24
+ case "prompt":
25
+ return (React.createElement(YesNoPrompt, { message: React.createElement(Ink.Text, { color: colors.yellow }, "Rebase detected, would you like to abort it?"), onYes: async () => {
26
+ await cli(`git rebase --abort`);
27
+ patch({ status: "done" });
28
+ }, onNo: async () => {
29
+ actions.exit(0);
30
+ } }));
31
+ default:
32
+ return (React.createElement(Await, { fallback: React.createElement(Ink.Text, { color: colors.yellow }, "Checking for rebase..."), function: rebase_check }));
33
+ }
34
+ async function rebase_check() {
35
+ const actions = Store.getState().actions;
36
+ const argv = Store.getState().argv;
37
+ invariant(argv, "argv must exist");
38
+ try {
39
+ const repo_root = (await cli(`git rev-parse --absolute-git-dir`)).stdout;
40
+ let is_rebase = false;
41
+ is_rebase ||= fs.existsSync(path.join(repo_root, "rebase-apply"));
42
+ is_rebase ||= fs.existsSync(path.join(repo_root, "rebase-merge"));
43
+ const status = is_rebase ? "prompt" : "done";
44
+ patch({ status });
45
+ }
46
+ catch (err) {
47
+ actions.error("Unable to check for rebase.");
48
+ if (err instanceof Error) {
49
+ if (actions.isDebug()) {
50
+ actions.error(err.message);
51
+ }
52
+ }
53
+ actions.exit(9);
54
+ }
55
+ }
56
+ }
package/dist/command.js CHANGED
@@ -26,6 +26,12 @@ export async function command() {
26
26
  alias: ["v"],
27
27
  default: false,
28
28
  description: "Print more detailed logs for debugging internals",
29
+ })
30
+ .option("update", {
31
+ type: "boolean",
32
+ alias: ["u"],
33
+ default: false,
34
+ description: "Check for updates",
29
35
  })
30
36
  .option("branch", {
31
37
  type: "string",
@@ -1,12 +1,17 @@
1
1
  export function write(args) {
2
2
  const stack_list = [];
3
- for (const pr_url of args.pr_url_list) {
3
+ const digits = String(args.pr_url_list.length).length;
4
+ for (let i = 0; i < args.pr_url_list.length; i++) {
5
+ const pr_url = args.pr_url_list[i];
4
6
  if (pr_url) {
5
7
  const selected = args.selected_url === pr_url;
6
8
  const icon = selected ? "👉" : "⏳";
7
- stack_list.push(`- ${icon} ${pr_url}`);
9
+ const num = String(i + 1).padStart(digits, "0");
10
+ stack_list.push(`- ${icon} \`${num}\` ${pr_url}`);
8
11
  }
9
12
  }
13
+ // reverse order of pr list to match the order of git stack
14
+ stack_list.reverse();
10
15
  let stack_table;
11
16
  if (stack_list.length > 1) {
12
17
  stack_table = TEMPLATE.stack_table(["", ...stack_list, "", ""].join("\n"));
package/dist/core/cli.js CHANGED
@@ -2,8 +2,8 @@ import * as child from "node:child_process";
2
2
  import { Store } from "../app/Store.js";
3
3
  let i = 0;
4
4
  export async function cli(unsafe_command, unsafe_options) {
5
- const state = Store.getState();
6
5
  const options = Object.assign({}, unsafe_options);
6
+ const state = Store.getState();
7
7
  let command;
8
8
  if (Array.isArray(unsafe_command)) {
9
9
  command = unsafe_command.join(" ");
@@ -33,19 +33,19 @@ export async function cli(unsafe_command, unsafe_options) {
33
33
  stderr += value;
34
34
  write_output(value);
35
35
  });
36
- childProcess.on("close", (code) => {
36
+ childProcess.on("close", (unsafe_code) => {
37
37
  const result = {
38
38
  command,
39
- code: code || 0,
39
+ code: unsafe_code || 0,
40
40
  stdout: stdout.trimEnd(),
41
41
  stderr: stderr.trimEnd(),
42
42
  output: output.trimEnd(),
43
43
  };
44
44
  state.actions.set((state) => state.mutate.end_pending_output(state, id));
45
- state.actions.debug(`[end] ${command}`);
45
+ state.actions.debug(`[end] ${command} (${result.code})`);
46
46
  state.actions.debug(result.output);
47
- if (!options.ignoreExitCode && code !== 0) {
48
- reject(new Error(`[${command}] (${code})`));
47
+ if (!options.ignoreExitCode && result.code !== 0) {
48
+ reject(new Error(`[${command}] (${result.code})`));
49
49
  }
50
50
  else {
51
51
  resolve(result);
@@ -56,12 +56,31 @@ export async function cli(unsafe_command, unsafe_options) {
56
56
  });
57
57
  });
58
58
  }
59
- cli.sync = function cli_sync(command, unsafe_options) {
59
+ cli.sync = function cli_sync(unsafe_command, unsafe_options) {
60
60
  const options = Object.assign({}, unsafe_options);
61
+ const state = Store.getState();
62
+ let command;
63
+ if (Array.isArray(unsafe_command)) {
64
+ command = unsafe_command.join(" ");
65
+ }
66
+ else {
67
+ command = unsafe_command;
68
+ }
69
+ state.actions.debug(`[start] ${command}`);
61
70
  const spawn_return = child.spawnSync("sh", ["-c", command], options);
62
71
  const stdout = String(spawn_return.stdout);
63
72
  const stderr = String(spawn_return.stderr);
64
- const output = String(spawn_return.output);
65
- const code = spawn_return.status || 0;
66
- return { command, code, stdout, stderr, output };
73
+ const result = {
74
+ command,
75
+ code: spawn_return.status || 0,
76
+ stdout,
77
+ stderr,
78
+ output: [stdout, stderr].join(""),
79
+ };
80
+ state.actions.debug(`[end] ${command} (${result.code})`);
81
+ state.actions.debug(result.output);
82
+ if (!options.ignoreExitCode && result.code !== 0) {
83
+ throw new Error(`[${command}] (${result.code})`);
84
+ }
85
+ return result;
67
86
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",