git-stack-cli 2.3.1 → 2.4.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/js/index.js CHANGED
@@ -32698,34 +32698,34 @@ async function sleep(time) {
32698
32698
  // src/app/Exit.tsx
32699
32699
  function Exit(props) {
32700
32700
  React14.useEffect(() => {
32701
- handle_exit().catch((err) => {
32701
+ Exit.handle_exit(props).catch((err) => {
32702
32702
  console.error(err);
32703
32703
  });
32704
- async function handle_exit() {
32705
- const state = Store.getState();
32706
- const actions = state.actions;
32707
- actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
32708
- let exit_code = props.code;
32709
- if (state.abort_handler) {
32710
- exit_code = await state.abort_handler();
32711
- }
32712
- if (state.is_dirty_check_stash) {
32713
- await cli("git stash pop");
32714
- actions.output(/* @__PURE__ */ React14.createElement(Text, {
32715
- color: colors.green
32716
- }, "✅ Changes restored from stash"));
32717
- }
32718
- await sleep(1);
32719
- if (props.clear) {
32720
- actions.clear();
32721
- }
32722
- actions.unmount();
32723
- process.exitCode = exit_code;
32724
- process.exit();
32725
- }
32726
32704
  }, [props.clear, props.code]);
32727
32705
  return null;
32728
32706
  }
32707
+ Exit.handle_exit = async function handle_exit(props) {
32708
+ const state = Store.getState();
32709
+ const actions = state.actions;
32710
+ actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
32711
+ let exit_code = props.code;
32712
+ if (state.abort_handler) {
32713
+ exit_code = await state.abort_handler();
32714
+ }
32715
+ if (state.is_dirty_check_stash) {
32716
+ await cli("git stash pop");
32717
+ actions.output(/* @__PURE__ */ React14.createElement(Text, {
32718
+ color: colors.green
32719
+ }, "✅ Changes restored from stash"));
32720
+ }
32721
+ await sleep(1);
32722
+ if (props.clear) {
32723
+ actions.clear();
32724
+ }
32725
+ actions.unmount();
32726
+ process.exitCode = exit_code;
32727
+ process.exit();
32728
+ };
32729
32729
 
32730
32730
  // src/app/LogTimestamp.tsx
32731
32731
  var React15 = __toESM(require_react(), 1);
@@ -39153,44 +39153,40 @@ async function run4() {
39153
39153
  // src/core/short_id.ts
39154
39154
  import crypto2 from "node:crypto";
39155
39155
  function short_id() {
39156
- const timestamp = Date.now();
39157
- const js_max_bits = 53;
39158
- const timestamp_bits = Math.floor(Math.log2(timestamp)) + 1;
39159
- const padding_bits = js_max_bits - timestamp_bits;
39160
- const random = crypto2.randomInt(0, Math.pow(2, padding_bits));
39161
- const combined = interleave_bits(timestamp, random);
39162
- return encode(combined);
39163
- }
39164
- function binary(value) {
39165
- return BigInt(value).toString(2);
39166
- }
39167
- function rand_index(list) {
39168
- return Math.floor(Math.random() * list.length);
39169
- }
39170
- function interleave_bits(a, b2) {
39171
- const a_binary = binary(a).split("");
39172
- const b_binary = binary(b2).split("");
39173
- while (b_binary.length) {
39174
- const b_index = rand_index(b_binary);
39175
- const [selected] = b_binary.splice(b_index, 1);
39176
- const a_index = rand_index(a_binary);
39177
- a_binary.splice(a_index, 0, selected);
39178
- }
39179
- const a_value = parseInt(a_binary.join(""), 2);
39180
- return a_value;
39181
- }
39182
- function encode(value) {
39183
- const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
39184
- const bits_per_char = Math.log2(chars.length);
39185
- const max_value_bits = 53;
39186
- const max_char_size = Math.ceil(max_value_bits / bits_per_char);
39156
+ const timestamp = BigInt(Date.now());
39157
+ const random = BigInt(crypto2.randomInt(0, 1 << RANDOM_BITS));
39158
+ const combined = timestamp << BigInt(RANDOM_BITS) | random;
39159
+ const id = encode(combined, ID_LENGTH);
39160
+ return id;
39161
+ }
39162
+ function encode(value, length) {
39187
39163
  let result = "";
39188
- while (value > 0) {
39189
- result = chars[value % chars.length] + result;
39190
- value = Math.floor(value / chars.length);
39164
+ if (value > 0n) {
39165
+ while (value > 0n) {
39166
+ const digit = to_number(value % BASE);
39167
+ result = CHARS[digit] + result;
39168
+ value /= BASE;
39169
+ }
39170
+ }
39171
+ if (length) {
39172
+ result = result.padStart(length, PAD);
39191
39173
  }
39192
- return result.padStart(max_char_size, "=");
39174
+ return result;
39175
+ }
39176
+ function to_number(value) {
39177
+ if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
39178
+ return Number(value);
39179
+ }
39180
+ throw new Error("BigInt value outside safe integer range");
39193
39181
  }
39182
+ var CHARS = "-0123456789_abcdefghijklmnopqrstuvwxyz".split("").sort().join("");
39183
+ var PAD = CHARS[0];
39184
+ var BASE = BigInt(CHARS.length);
39185
+ var CHAR_BITS = Math.log2(to_number(BASE));
39186
+ var TIMESTAMP_BITS = 53;
39187
+ var RANDOM_BITS = 30;
39188
+ var ID_BITS = TIMESTAMP_BITS + RANDOM_BITS;
39189
+ var ID_LENGTH = Math.ceil(ID_BITS / CHAR_BITS);
39194
39190
 
39195
39191
  // src/commands/Rebase.tsx
39196
39192
  function Rebase(props) {
@@ -39216,7 +39212,7 @@ Rebase.run = async function run5(props) {
39216
39212
  actions.output(/* @__PURE__ */ React34.createElement(Text, {
39217
39213
  color: colors.red
39218
39214
  }, "\uD83D\uDEA8 Abort"));
39219
- handle_exit();
39215
+ handle_exit2();
39220
39216
  return 19;
39221
39217
  });
39222
39218
  const temp_branch_name = `${branch_name}_${short_id()}`;
@@ -39310,7 +39306,7 @@ Rebase.run = async function run5(props) {
39310
39306
  }
39311
39307
  cli.sync(`pwd`, spawn_options);
39312
39308
  }
39313
- function handle_exit() {
39309
+ function handle_exit2() {
39314
39310
  actions.output(/* @__PURE__ */ React34.createElement(Text, {
39315
39311
  color: colors.yellow
39316
39312
  }, "Restoring ", /* @__PURE__ */ React34.createElement(Brackets, null, branch_name), "…"));
@@ -39360,7 +39356,7 @@ async function run6() {
39360
39356
  actions.output(/* @__PURE__ */ React36.createElement(Text, {
39361
39357
  color: colors.red
39362
39358
  }, "\uD83D\uDEA8 Abort"));
39363
- handle_exit();
39359
+ handle_exit2();
39364
39360
  return 15;
39365
39361
  });
39366
39362
  const temp_branch_name = `${branch_name}_${short_id()}`;
@@ -39439,7 +39435,7 @@ async function run6() {
39439
39435
  }
39440
39436
  cli.sync(`pwd`, spawn_options);
39441
39437
  }
39442
- function handle_exit() {
39438
+ function handle_exit2() {
39443
39439
  actions.output(/* @__PURE__ */ React36.createElement(Text, {
39444
39440
  color: colors.yellow
39445
39441
  }, "Restoring ", /* @__PURE__ */ React36.createElement(Brackets, null, branch_name), "…"));
@@ -40098,8 +40094,7 @@ function SelectCommitRangesInternal(props) {
40098
40094
  let branch_prefix = "";
40099
40095
  if (argv["branch-prefix"]) {
40100
40096
  branch_prefix = argv["branch-prefix"];
40101
- } else if (process.env.GIT_STACK_BRANCH_PREFIX) {
40102
- branch_prefix = process.env.GIT_STACK_BRANCH_PREFIX;
40097
+ } else if ("") {
40103
40098
  }
40104
40099
  return `${branch_prefix}${gs_short_id()}`;
40105
40100
  }
@@ -40258,7 +40253,7 @@ async function run9() {
40258
40253
  actions.output(/* @__PURE__ */ React44.createElement(Text, {
40259
40254
  color: colors.red
40260
40255
  }, "\uD83D\uDEA8 Abort"));
40261
- handle_exit();
40256
+ handle_exit2();
40262
40257
  return 17;
40263
40258
  });
40264
40259
  let DEFAULT_PR_BODY = "";
@@ -40403,7 +40398,7 @@ async function run9() {
40403
40398
  });
40404
40399
  }
40405
40400
  }
40406
- function handle_exit() {
40401
+ function handle_exit2() {
40407
40402
  actions.output(/* @__PURE__ */ React44.createElement(Text, {
40408
40403
  color: colors.yellow
40409
40404
  }, "Restoring PR state…"));
@@ -40709,7 +40704,9 @@ class ErrorBoundary extends React52.Component {
40709
40704
  component_stack = component_stack.split(`
40710
40705
  `).slice(1).join(`
40711
40706
  `);
40712
- this.setState({ component_stack });
40707
+ this.setState({ component_stack }, async () => {
40708
+ await Exit.handle_exit({ code: 30, clear: true });
40709
+ });
40713
40710
  }
40714
40711
  }
40715
40712
  render() {
@@ -45664,7 +45661,7 @@ var yargs_default = Yargs;
45664
45661
 
45665
45662
  // src/command.ts
45666
45663
  async function command2() {
45667
- return yargs_default(hideBin(process.argv)).scriptName("git stack").usage("Usage: git stack [command] [options]").command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions)).command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) => yargs.positional("commit", FixupOptions.commit)).command("log [args...]", "Print an abbreviated log with numbered commits, useful for git stack fixup", (yargs) => yargs.strict(false)).command("rebase", "Update local branch via rebase with latest changes from origin master branch", (yargs) => yargs).option("verbose", GlobalOptions.verbose).wrap(123).strict().version("2.3.1").showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`").help("help", "Show usage via `git stack help`").argv;
45664
+ return yargs_default(hideBin(process.argv)).scriptName("git stack").usage("Usage: git stack [command] [options]").command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions)).command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) => yargs.positional("commit", FixupOptions.commit)).command("log [args...]", "Print an abbreviated log with numbered commits, useful for git stack fixup", (yargs) => yargs.strict(false)).command("rebase", "Update local branch via rebase with latest changes from origin master branch", (yargs) => yargs).option("verbose", GlobalOptions.verbose).wrap(123).strict().version("2.4.0").showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`").help("help", "Show usage via `git stack help`").argv;
45668
45665
  }
45669
45666
  var GlobalOptions = {
45670
45667
  verbose: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
package/src/app/Exit.tsx CHANGED
@@ -15,44 +15,44 @@ type Props = {
15
15
  export function Exit(props: Props) {
16
16
  React.useEffect(() => {
17
17
  // immediately handle exit on mount
18
- handle_exit().catch((err) => {
18
+ Exit.handle_exit(props).catch((err) => {
19
19
  // eslint-disable-next-line no-console
20
20
  console.error(err);
21
21
  });
22
+ }, [props.clear, props.code]);
22
23
 
23
- async function handle_exit() {
24
- const state = Store.getState();
25
- const actions = state.actions;
24
+ return null;
25
+ }
26
26
 
27
- actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
27
+ Exit.handle_exit = async function handle_exit(props: Props) {
28
+ const state = Store.getState();
29
+ const actions = state.actions;
28
30
 
29
- let exit_code = props.code;
31
+ actions.debug(`[Exit] handle_exit ${JSON.stringify(props)}`);
30
32
 
31
- // run abort_handler if it exists
32
- if (state.abort_handler) {
33
- exit_code = await state.abort_handler();
34
- }
33
+ let exit_code = props.code;
35
34
 
36
- // restore git stash if necessary
37
- if (state.is_dirty_check_stash) {
38
- await cli("git stash pop");
39
- actions.output(<Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>);
40
- }
35
+ // run abort_handler if it exists
36
+ if (state.abort_handler) {
37
+ exit_code = await state.abort_handler();
38
+ }
41
39
 
42
- // ensure output has a chance to render
43
- await sleep(1);
40
+ // restore git stash if necessary
41
+ if (state.is_dirty_check_stash) {
42
+ await cli("git stash pop");
43
+ actions.output(<Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>);
44
+ }
44
45
 
45
- // finally handle the actual app and process exit
46
- if (props.clear) {
47
- actions.clear();
48
- }
46
+ // ensure output has a chance to render
47
+ await sleep(1);
49
48
 
50
- actions.unmount();
49
+ // finally handle the actual app and process exit
50
+ if (props.clear) {
51
+ actions.clear();
52
+ }
51
53
 
52
- process.exitCode = exit_code;
53
- process.exit();
54
- }
55
- }, [props.clear, props.code]);
54
+ actions.unmount();
56
55
 
57
- return null;
58
- }
56
+ process.exitCode = exit_code;
57
+ process.exit();
58
+ };
@@ -3,6 +3,7 @@ import * as React from "react";
3
3
 
4
4
  import * as Ink from "ink-cjs";
5
5
 
6
+ import { Exit } from "~/app/Exit";
6
7
  import { FormatText } from "~/app/FormatText";
7
8
  import { Store } from "~/app/Store";
8
9
  import { colors } from "~/core/colors";
@@ -36,7 +37,9 @@ export class ErrorBoundary extends React.Component<Props, State> {
36
37
  if (component_stack) {
37
38
  // remove first line of component_stack
38
39
  component_stack = component_stack.split("\n").slice(1).join("\n");
39
- this.setState({ component_stack });
40
+ this.setState({ component_stack }, async () => {
41
+ await Exit.handle_exit({ code: 30, clear: true });
42
+ });
40
43
  }
41
44
  }
42
45
 
@@ -0,0 +1,30 @@
1
+ import { test, expect } from "bun:test";
2
+
3
+ import { short_id } from "~/core/short_id";
4
+
5
+ const GENERATE_COUNT = 1000;
6
+
7
+ test("short_id is 15 characters", () => {
8
+ const id = short_id();
9
+ expect(id.length).toBe(16);
10
+ });
11
+
12
+ test("unique enough in practice", () => {
13
+ const id_set = new Set();
14
+ for (let i = 0; i < GENERATE_COUNT; i++) {
15
+ const id = short_id();
16
+ id_set.add(id);
17
+ }
18
+
19
+ expect(id_set.size).toBe(GENERATE_COUNT);
20
+ });
21
+
22
+ test("sorts lexicographically", () => {
23
+ const id_list = [];
24
+ for (let i = 0; i < GENERATE_COUNT; i++) {
25
+ const id = short_id();
26
+ id_list.push(id);
27
+ }
28
+
29
+ expect(id_list.sort()).toEqual(id_list);
30
+ });
@@ -1,87 +1,70 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
3
  export function short_id() {
4
- const timestamp = Date.now();
4
+ // wall-clock milliseconds
5
+ const timestamp = BigInt(Date.now());
5
6
 
6
- // 9 223 372 036 854 775 808
7
- // 9 trillion possible values
8
- // (2^53) * (2^10) = 2^63 = 9,223,372,036,854,775,808
9
- const js_max_bits = 53;
7
+ // random int value between 0 and 2^RANDOM_BITS - 1
8
+ const random = BigInt(crypto.randomInt(0, 1 << RANDOM_BITS));
10
9
 
11
- const timestamp_bits = Math.floor(Math.log2(timestamp)) + 1;
10
+ // concatenate timestamp (high bits) and random value
11
+ const combined = (timestamp << BigInt(RANDOM_BITS)) | random;
12
12
 
13
- // padding needed to reach 53 bits
14
- const padding_bits = js_max_bits - timestamp_bits;
13
+ const id = encode(combined, ID_LENGTH);
15
14
 
16
- // random between 0 and 2^padding_bits - 1
17
- const random = crypto.randomInt(0, Math.pow(2, padding_bits));
18
-
19
- // combine timestamp and random value
20
- const combined = interleave_bits(timestamp, random);
21
-
22
- // console.debug({ combined, timestamp, random, padding_bits, timestamp_bits });
23
-
24
- return encode(combined);
25
- }
26
-
27
- function binary(value: number) {
28
- return BigInt(value).toString(2);
15
+ // console.debug({ id, timestamp, random, combined });
16
+ return id;
29
17
  }
30
18
 
31
- function rand_index(list: Array<any>) {
32
- return Math.floor(Math.random() * list.length);
33
- }
34
-
35
- function interleave_bits(a: number, b: number) {
36
- const a_binary = binary(a).split("");
37
-
38
- const b_binary = binary(b).split("");
39
-
40
- while (b_binary.length) {
41
- // pull random bit out of b_binary
42
-
43
- const b_index = rand_index(b_binary);
44
-
45
- const [selected] = b_binary.splice(b_index, 1);
46
-
47
- // insert random bit into a_binary
19
+ // converting value into base based on available chars
20
+ function encode(value: bigint, length?: number) {
21
+ let result = "";
48
22
 
49
- const a_index = rand_index(a_binary);
23
+ // avoid zero division
24
+ if (value > 0n) {
25
+ while (value > 0n) {
26
+ // convert least significant digit
27
+ const digit = to_number(value % BASE);
28
+ result = CHARS[digit] + result;
50
29
 
51
- a_binary.splice(a_index, 0, selected);
30
+ // drop the digit we just divided by
31
+ value /= BASE;
32
+ }
52
33
  }
53
34
 
54
- // convert binary list back to integer
55
-
56
- const a_value = parseInt(a_binary.join(""), 2);
35
+ // left pad with 0 to guarantee chars
36
+ if (length) {
37
+ result = result.padStart(length, PAD);
38
+ }
57
39
 
58
- return a_value;
40
+ return result;
59
41
  }
60
42
 
61
- function encode(value: number) {
62
- // base64 encode (64 characters)
63
- // max character necessary to encode is equal to maximum number
64
- // of bits in value divided by bits per character in encoding
65
- //
66
- // Example
67
- // in base64 each characters can represent 6 bits (2^6 = 64)
68
- // 53 bits / 6 bits = 8.833333333333334 characters (9 characters)
69
- //
70
- // prettier-ignore
71
- const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
72
-
73
- const bits_per_char = Math.log2(chars.length);
74
- const max_value_bits = 53;
75
- const max_char_size = Math.ceil(max_value_bits / bits_per_char);
76
-
77
- let result = "";
78
-
79
- while (value > 0) {
80
- result = chars[value % chars.length] + result;
81
-
82
- value = Math.floor(value / chars.length);
43
+ function to_number(value: bigint) {
44
+ if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
45
+ return Number(value);
83
46
  }
84
47
 
85
- // pad the result to necessary characters
86
- return result.padStart(max_char_size, "=");
48
+ throw new Error("BigInt value outside safe integer range");
87
49
  }
50
+
51
+ // valid characters to use for the short id
52
+ // use only lowercase letters and numbers to avoid
53
+ // confusing file system issues with git branch names
54
+ const CHARS = "-0123456789_abcdefghijklmnopqrstuvwxyz".split("").sort().join("");
55
+ const PAD = CHARS[0];
56
+ const BASE = BigInt(CHARS.length);
57
+ // bits carried by each char log2(BASE) ≈ 5.32
58
+ const CHAR_BITS = Math.log2(to_number(BASE));
59
+
60
+ // javascript Date.now returns a Number that will overflow eventually
61
+ // 2^53 max integer, overflows in 2^53 milliseconds (285_616 years)
62
+ // (1n<<53n) / 1_000n / 60n / 60n / 24n / 365n ≈ 285616n
63
+ const TIMESTAMP_BITS = 53;
64
+
65
+ // collision probability for identical timestamps (milliseconds)
66
+ // ≈ 1 / 2^30 ≈ 1 / 1_073_741_824 (1.1 billion)
67
+ const RANDOM_BITS = 30;
68
+
69
+ const ID_BITS = TIMESTAMP_BITS + RANDOM_BITS;
70
+ const ID_LENGTH = Math.ceil(ID_BITS / CHAR_BITS);