git-stack-cli 2.7.1 → 2.7.2

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
@@ -37414,6 +37414,17 @@ async function fetch_json(url) {
37414
37414
  });
37415
37415
  }
37416
37416
 
37417
+ // src/core/get_timeout_fn.ts
37418
+ function get_timeout_fn(ms, message) {
37419
+ return function timeout(promise) {
37420
+ let id;
37421
+ const timeout = new Promise((_resolve, reject) => {
37422
+ id = setTimeout(() => reject(new Error(message)), ms);
37423
+ });
37424
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(id));
37425
+ };
37426
+ }
37427
+
37417
37428
  // src/core/is_finite_value.ts
37418
37429
  function is_finite_value(value) {
37419
37430
  return typeof value === "number" && Number.isFinite(value);
@@ -37522,19 +37533,15 @@ function AutoUpdate(props) {
37522
37533
  async function init_state() {
37523
37534
  if (state.latest_version !== null)
37524
37535
  return;
37525
- const local_version = "2.7.1";
37536
+ const local_version = "2.7.2";
37526
37537
  const latest_version = await get_latest_version();
37527
37538
  const is_brew_bun_standalone = get_is_brew_bun_standalone();
37528
37539
  patch({ local_version, latest_version, is_brew_bun_standalone });
37529
37540
  }
37530
37541
  async function get_latest_version() {
37531
37542
  const timeout_ms = is_finite_value(props.timeoutMs) ? props.timeoutMs : 2 * 1000;
37532
- const npm_json = await Promise.race([
37533
- fetch_json(`https://registry.npmjs.org/${props.name}`),
37534
- sleep(timeout_ms).then(() => {
37535
- abort(new Error("AutoUpdate timeout"));
37536
- })
37537
- ]);
37543
+ const timeout = get_timeout_fn(timeout_ms, "AutoUpdate timeout");
37544
+ const npm_json = await timeout(fetch_json(`https://registry.npmjs.org/${props.name}`));
37538
37545
  const maybe_version = npm_json?.["dist-tags"]?.latest;
37539
37546
  if (typeof maybe_version === "string") {
37540
37547
  return maybe_version;
@@ -38078,7 +38085,12 @@ function write(message, values) {
38078
38085
  return new_message;
38079
38086
  }
38080
38087
  function read(message) {
38081
- const values = { id: null, title: null };
38088
+ const values = { subject: null, id: null, title: null };
38089
+ const match_subject = message.match(RE2.subject_line);
38090
+ if (match_subject?.groups) {
38091
+ values.subject = match_subject.groups["subject"];
38092
+ invariant(values.subject, "subject must exist");
38093
+ }
38082
38094
  const match_id = message.match(RE2.stack_id);
38083
38095
  if (match_id?.groups) {
38084
38096
  values.id = match_id.groups["id"];
@@ -38106,10 +38118,46 @@ var TEMPLATE = {
38106
38118
  }
38107
38119
  };
38108
38120
  var RE2 = {
38121
+ subject_line: /^(?<subject>[^\n]*)/,
38109
38122
  stack_id: new RegExp(`${TEMPLATE.stack_id("(?<id>[^\\s]+)")}`, "i"),
38110
38123
  group_title: new RegExp(TEMPLATE.group_title("(?<title>[^\\n^\\r]+)"), "i")
38111
38124
  };
38112
38125
 
38126
+ // src/core/git.ts
38127
+ async function get_commits(dot_range) {
38128
+ const log_result = await cli(`git log ${dot_range} --format=${FORMAT} --color=never`);
38129
+ if (!log_result.stdout) {
38130
+ return [];
38131
+ }
38132
+ const commit_list = [];
38133
+ for (let record of log_result.stdout.split(SEP2.record)) {
38134
+ record = record.replace(/^\n/, "");
38135
+ record = record.replace(/\n$/, "");
38136
+ if (!record)
38137
+ continue;
38138
+ const [sha, full_message] = record.split(SEP2.field);
38139
+ const metadata = read(full_message);
38140
+ const branch_id = metadata.id;
38141
+ const subject_line = metadata.subject || "";
38142
+ const title = metadata.title;
38143
+ const commit = {
38144
+ sha,
38145
+ full_message,
38146
+ subject_line,
38147
+ branch_id,
38148
+ title
38149
+ };
38150
+ commit_list.push(commit);
38151
+ }
38152
+ commit_list.reverse();
38153
+ return commit_list;
38154
+ }
38155
+ var SEP2 = {
38156
+ record: "\x1E",
38157
+ field: "\x1F"
38158
+ };
38159
+ var FORMAT = `%H${SEP2.field}%B${SEP2.record}`;
38160
+
38113
38161
  // src/core/github.tsx
38114
38162
  var React24 = __toESM(require_react(), 1);
38115
38163
  import crypto from "node:crypto";
@@ -38144,6 +38192,8 @@ var RE3 = {
38144
38192
  async function pr_list() {
38145
38193
  const state = Store.getState();
38146
38194
  const actions = state.actions;
38195
+ const timer = Timer();
38196
+ actions.debug("start github.pr_list");
38147
38197
  const username = state.username;
38148
38198
  const repo_path = state.repo_path;
38149
38199
  invariant(username, "username must exist");
@@ -38165,6 +38215,8 @@ async function pr_list() {
38165
38215
  state2.pr[pr.headRefName] = pr;
38166
38216
  }
38167
38217
  });
38218
+ const duration = timer.duration();
38219
+ actions.debug(`end github.pr_list (duration=${duration})`);
38168
38220
  return result_pr_list;
38169
38221
  }
38170
38222
  async function pr_status(branch) {
@@ -38287,11 +38339,12 @@ var RE4 = {
38287
38339
 
38288
38340
  // src/core/CommitMetadata.ts
38289
38341
  async function range(commit_group_map) {
38290
- const master_branch = Store.getState().master_branch;
38291
38342
  await pr_list();
38292
- const commit_list = await get_commit_list();
38343
+ const master_branch = Store.getState().master_branch;
38344
+ const commit_list = await get_commits(`${master_branch}..HEAD`);
38293
38345
  const pr_lookup = {};
38294
38346
  let invalid = false;
38347
+ let last_group_id = null;
38295
38348
  const group_map = new Map;
38296
38349
  for (const commit of commit_list) {
38297
38350
  let id = commit.branch_id;
@@ -38307,9 +38360,7 @@ async function range(commit_group_map) {
38307
38360
  invalid = true;
38308
38361
  }
38309
38362
  if (id) {
38310
- const group_key_list = Array.from(group_map.keys());
38311
- const last_key = group_key_list[group_key_list.length - 1];
38312
- if (group_map.has(id) && last_key !== id) {
38363
+ if (group_map.has(id) && last_group_id !== id) {
38313
38364
  invalid = true;
38314
38365
  }
38315
38366
  } else {
@@ -38329,6 +38380,7 @@ async function range(commit_group_map) {
38329
38380
  };
38330
38381
  group.commits.push(commit);
38331
38382
  group_map.set(id, group);
38383
+ last_group_id = id;
38332
38384
  }
38333
38385
  const group_value_list = Array.from(group_map.values());
38334
38386
  const group_list = [];
@@ -38394,44 +38446,6 @@ async function range(commit_group_map) {
38394
38446
  }
38395
38447
  return { invalid, group_list, commit_list, pr_lookup, UNASSIGNED };
38396
38448
  }
38397
- async function get_commit_list() {
38398
- const master_branch = Store.getState().master_branch;
38399
- const log_result = await cli(`git log ${master_branch}..HEAD --oneline --format=%H --color=never`);
38400
- if (!log_result.stdout) {
38401
- return [];
38402
- }
38403
- const sha_list = lines(log_result.stdout).reverse();
38404
- const commit_metadata_list = [];
38405
- for (let i2 = 0;i2 < sha_list.length; i2++) {
38406
- const sha = sha_list[i2];
38407
- const commit_metadata = await commit(sha);
38408
- commit_metadata_list.push(commit_metadata);
38409
- }
38410
- return commit_metadata_list;
38411
- }
38412
- async function commit(sha) {
38413
- const full_message = (await cli(`git show -s --format=%B ${sha}`)).stdout;
38414
- const metadata = await read(full_message);
38415
- const branch_id = metadata?.id;
38416
- const subject_line = get_subject_line(full_message);
38417
- const title = metadata?.title;
38418
- return {
38419
- sha,
38420
- full_message,
38421
- subject_line,
38422
- branch_id,
38423
- title
38424
- };
38425
- }
38426
- function get_subject_line(message) {
38427
- const line_list = lines(message);
38428
- const first_line = line_list[0];
38429
- return remove(first_line);
38430
- }
38431
- function lines(value) {
38432
- return value.split(`
38433
- `);
38434
- }
38435
38449
  var UNASSIGNED = "unassigned";
38436
38450
 
38437
38451
  // src/core/GitReviseTodo.ts
@@ -38442,8 +38456,8 @@ function GitReviseTodo(args) {
38442
38456
  const group_list = args.commit_range.group_list;
38443
38457
  for (let i2 = args.rebase_group_index;i2 < group_list.length; i2++) {
38444
38458
  const group = group_list[i2];
38445
- for (const commit2 of group.commits) {
38446
- commit_list.push(commit2);
38459
+ for (const commit of group.commits) {
38460
+ commit_list.push(commit);
38447
38461
  }
38448
38462
  }
38449
38463
  const todo = GitReviseTodo.todo({ commit_list });
@@ -38451,17 +38465,17 @@ function GitReviseTodo(args) {
38451
38465
  }
38452
38466
  GitReviseTodo.todo = function todo(args) {
38453
38467
  const entry_list = [];
38454
- for (const commit2 of args.commit_list) {
38455
- const sha = commit2.sha.slice(0, 12);
38468
+ for (const commit of args.commit_list) {
38469
+ const sha = commit.sha.slice(0, 12);
38456
38470
  const entry_lines = [`++ pick ${sha}`];
38457
- const id = commit2.branch_id;
38471
+ const id = commit.branch_id;
38458
38472
  if (id == null || id === UNASSIGNED) {
38459
- entry_lines.push(commit2.full_message);
38473
+ entry_lines.push(commit.full_message);
38460
38474
  } else {
38461
- const title = commit2.title;
38475
+ const title = commit.title;
38462
38476
  invariant(title, "commit.title must exist");
38463
38477
  const metadata = { id, title };
38464
- const unsafe_message_with_id = write(commit2.full_message, metadata);
38478
+ const unsafe_message_with_id = write(commit.full_message, metadata);
38465
38479
  const message_with_id = unsafe_message_with_id;
38466
38480
  entry_lines.push(message_with_id);
38467
38481
  }
@@ -38629,8 +38643,8 @@ function DetectInitialPR(props) {
38629
38643
  invariant(commit_range, "commit_range must exist");
38630
38644
  try {
38631
38645
  let has_existing_metadata = false;
38632
- for (const commit2 of commit_range.commit_list) {
38633
- if (commit2.branch_id) {
38646
+ for (const commit of commit_range.commit_list) {
38647
+ if (commit.branch_id) {
38634
38648
  has_existing_metadata = true;
38635
38649
  break;
38636
38650
  }
@@ -38659,9 +38673,9 @@ function DetectInitialPR(props) {
38659
38673
  for (const group of commit_range.group_list) {
38660
38674
  group.id = branch_name2;
38661
38675
  group.title = state.pr?.title || "-";
38662
- for (const commit2 of commit_range.commit_list) {
38663
- commit2.branch_id = group.id;
38664
- commit2.title = group.title;
38676
+ for (const commit of commit_range.commit_list) {
38677
+ commit.branch_id = group.id;
38678
+ commit.title = group.title;
38665
38679
  }
38666
38680
  }
38667
38681
  const rebase_group_index = 0;
@@ -39290,7 +39304,9 @@ Rebase.run = async function run5(props) {
39290
39304
  }
39291
39305
  actions.exit(20);
39292
39306
  }
39307
+ actions.debug("start CommitMetadata.range");
39293
39308
  const next_commit_range = await range();
39309
+ actions.debug("end CommitMetadata.range");
39294
39310
  actions.output(/* @__PURE__ */ React34.createElement(FormatText, {
39295
39311
  wrapper: /* @__PURE__ */ React34.createElement(Text, {
39296
39312
  color: colors.green
@@ -39320,12 +39336,12 @@ Rebase.run = async function run5(props) {
39320
39336
  await cli(`git checkout -b ${temp_branch_name} ${rebase_merge_base}`);
39321
39337
  const picked_commit_list = [];
39322
39338
  for (let i2 = 0;i2 < commit_range.commit_list.length; i2++) {
39323
- const commit2 = commit_range.commit_list[i2];
39324
- const commit_pr = commit_range.pr_lookup[commit2.branch_id || ""];
39339
+ const commit = commit_range.commit_list[i2];
39340
+ const commit_pr = commit_range.pr_lookup[commit.branch_id || ""];
39325
39341
  const merged_pr = commit_pr?.state === "MERGED";
39326
39342
  const commit_message = /* @__PURE__ */ React34.createElement(Text, {
39327
39343
  color: colors.blue
39328
- }, commit2.subject_line);
39344
+ }, commit.subject_line);
39329
39345
  if (merged_pr) {
39330
39346
  actions.output(/* @__PURE__ */ React34.createElement(FormatText, {
39331
39347
  wrapper: /* @__PURE__ */ React34.createElement(Text, {
@@ -39352,15 +39368,17 @@ Rebase.run = async function run5(props) {
39352
39368
  commit_message
39353
39369
  }
39354
39370
  }));
39355
- picked_commit_list.push(commit2);
39371
+ picked_commit_list.push(commit);
39356
39372
  }
39357
39373
  if (picked_commit_list.length > 0) {
39358
39374
  await cli(`git clean -fd`);
39359
- const sha_list = picked_commit_list.map((commit2) => commit2.sha).join(" ");
39375
+ const sha_list = picked_commit_list.map((commit) => commit.sha).join(" ");
39360
39376
  await cli(`git cherry-pick --keep-redundant-commits ${sha_list}`);
39361
39377
  }
39362
39378
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
39379
+ actions.debug("start restore_git()");
39363
39380
  restore_git();
39381
+ actions.debug("end restore_git()");
39364
39382
  }
39365
39383
  function restore_git() {
39366
39384
  const spawn_options = { ignoreExitCode: true };
@@ -39431,10 +39449,10 @@ async function run6() {
39431
39449
  const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
39432
39450
  let commit_range = await range(commit_map);
39433
39451
  commit_range.group_list.reverse();
39434
- for (const commit2 of commit_range.commit_list) {
39435
- const group_from_map = commit_map[commit2.sha];
39436
- commit2.branch_id = group_from_map.id;
39437
- commit2.title = group_from_map.title;
39452
+ for (const commit of commit_range.commit_list) {
39453
+ const group_from_map = commit_map[commit.sha];
39454
+ commit.branch_id = group_from_map.id;
39455
+ commit.title = group_from_map.title;
39438
39456
  }
39439
39457
  await GitReviseTodo.execute({
39440
39458
  rebase_group_index: 0,
@@ -39898,8 +39916,8 @@ function SelectCommitRangesInternal(props) {
39898
39916
  map.set(args.key, args.value);
39899
39917
  return new Map(map);
39900
39918
  }, new Map, (map) => {
39901
- for (const commit2 of props.commit_range.commit_list) {
39902
- map.set(commit2.sha, commit2.branch_id);
39919
+ for (const commit of props.commit_range.commit_list) {
39920
+ map.set(commit.sha, commit.branch_id);
39903
39921
  }
39904
39922
  return new Map(map);
39905
39923
  });
@@ -39989,8 +40007,8 @@ function SelectCommitRangesInternal(props) {
39989
40007
  const max_width = 80;
39990
40008
  const [focused, set_focused] = React42.useState("");
39991
40009
  const has_groups = group.id !== props.commit_range.UNASSIGNED;
39992
- const items = props.commit_range.commit_list.map((commit2) => {
39993
- const commit_metadata_id = commit_map.get(commit2.sha);
40010
+ const items = props.commit_range.commit_list.map((commit) => {
40011
+ const commit_metadata_id = commit_map.get(commit.sha);
39994
40012
  const selected = commit_metadata_id !== null;
39995
40013
  let disabled;
39996
40014
  if (group_input) {
@@ -40001,8 +40019,8 @@ function SelectCommitRangesInternal(props) {
40001
40019
  disabled = Boolean(selected && commit_metadata_id !== group.id);
40002
40020
  }
40003
40021
  return {
40004
- label: commit2.subject_line,
40005
- value: commit2,
40022
+ label: commit.subject_line,
40023
+ value: commit,
40006
40024
  selected,
40007
40025
  disabled
40008
40026
  };
@@ -40133,7 +40151,7 @@ function SelectCommitRangesInternal(props) {
40133
40151
  wrapper: /* @__PURE__ */ React42.createElement(Text, {
40134
40152
  color: colors.gray
40135
40153
  }),
40136
- message: "Press {s} to {sync} the {count} assigned commits to Github",
40154
+ message: argv.sync ? "Press {s} to {sync} the {count} assigned commits to Github" : "Press {s} to {sync} the {count} assigned commits locally",
40137
40155
  values: {
40138
40156
  ...S_TO_SYNC_VALUES,
40139
40157
  count: /* @__PURE__ */ React42.createElement(Text, {
@@ -40141,15 +40159,11 @@ function SelectCommitRangesInternal(props) {
40141
40159
  bold: true
40142
40160
  }, assigned_count)
40143
40161
  }
40144
- })) : /* @__PURE__ */ React42.createElement(React42.Fragment, null, argv.sync ? /* @__PURE__ */ React42.createElement(FormatText, {
40145
- wrapper: /* @__PURE__ */ React42.createElement(Text, null),
40146
- message: "\uD83C\uDF89 Done! Press {s} to {sync} the commits to Github",
40147
- values: S_TO_SYNC_VALUES
40148
- }) : /* @__PURE__ */ React42.createElement(FormatText, {
40162
+ })) : /* @__PURE__ */ React42.createElement(FormatText, {
40149
40163
  wrapper: /* @__PURE__ */ React42.createElement(Text, null),
40150
- message: "\uD83C\uDF89 Done! Press {s} to {save} the commits locally",
40164
+ message: argv.sync ? "\uD83C\uDF89 Done! Press {s} to {sync} the PRs to Github" : "\uD83C\uDF89 Done! Press {s} to {sync} the PRs locally",
40151
40165
  values: S_TO_SYNC_VALUES
40152
- })), /* @__PURE__ */ React42.createElement(Box_default, null, /* @__PURE__ */ React42.createElement(FormatText, {
40166
+ }), /* @__PURE__ */ React42.createElement(Box_default, null, /* @__PURE__ */ React42.createElement(FormatText, {
40153
40167
  wrapper: /* @__PURE__ */ React42.createElement(Text, {
40154
40168
  color: colors.gray
40155
40169
  }),
@@ -40204,8 +40218,8 @@ function SelectCommitRangesInternal(props) {
40204
40218
  }
40205
40219
  let allow_unassigned_sync = null;
40206
40220
  for (let i2 = 0;i2 < props.commit_range.commit_list.length; i2++) {
40207
- const commit2 = props.commit_range.commit_list[i2];
40208
- const group_id = commit_map.get(commit2.sha);
40221
+ const commit = props.commit_range.commit_list[i2];
40222
+ const group_id = commit_map.get(commit.sha);
40209
40223
  if (allow_unassigned_sync === null) {
40210
40224
  if (group_id === null) {
40211
40225
  allow_unassigned_sync = true;
@@ -40770,14 +40784,14 @@ class UI {
40770
40784
  return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
40771
40785
  }
40772
40786
  toString() {
40773
- const lines2 = [];
40787
+ const lines = [];
40774
40788
  this.rows.forEach((row) => {
40775
- this.rowToString(row, lines2);
40789
+ this.rowToString(row, lines);
40776
40790
  });
40777
- return lines2.filter((line) => !line.hidden).map((line) => line.text).join(`
40791
+ return lines.filter((line) => !line.hidden).map((line) => line.text).join(`
40778
40792
  `);
40779
40793
  }
40780
- rowToString(row, lines2) {
40794
+ rowToString(row, lines) {
40781
40795
  this.rasterize(row).forEach((rrow, r3) => {
40782
40796
  let str = "";
40783
40797
  rrow.forEach((col, c2) => {
@@ -40804,16 +40818,16 @@ class UI {
40804
40818
  if (padding[right]) {
40805
40819
  str += " ".repeat(padding[right]);
40806
40820
  }
40807
- if (r3 === 0 && lines2.length > 0) {
40808
- str = this.renderInline(str, lines2[lines2.length - 1]);
40821
+ if (r3 === 0 && lines.length > 0) {
40822
+ str = this.renderInline(str, lines[lines.length - 1]);
40809
40823
  }
40810
40824
  });
40811
- lines2.push({
40825
+ lines.push({
40812
40826
  text: str.replace(/ +$/, ""),
40813
40827
  span: row.span
40814
40828
  });
40815
40829
  });
40816
- return lines2;
40830
+ return lines;
40817
40831
  }
40818
40832
  renderInline(source, previousLine) {
40819
40833
  const match2 = source.match(/^ */);
@@ -45562,7 +45576,7 @@ async function command2(argv, options = {}) {
45562
45576
  if (options.env_config) {
45563
45577
  builder = builder.config(options.env_config);
45564
45578
  }
45565
- const parsed = await builder.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).command(["update", "upgrade"], "Check and install the latest version of git stack", (yargs) => yargs).command("config", "Generate a one-time configuration json based on the passed arguments", (yargs) => yargs.options(DefaultOptions)).option("verbose", GlobalOptions.verbose).wrap(123).strict().version("2.7.1").showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`").help("help", "Show usage via `git stack help`");
45579
+ const parsed = await builder.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).command(["update", "upgrade"], "Check and install the latest version of git stack", (yargs) => yargs).command("config", "Generate a one-time configuration json based on the passed arguments", (yargs) => yargs.options(DefaultOptions)).option("verbose", GlobalOptions.verbose).wrap(123).strict().version("2.7.2").showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`").help("help", "Show usage via `git stack help`");
45566
45580
  const result = parsed.argv;
45567
45581
  return result;
45568
45582
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import * as util from "util";
4
4
 
5
5
  import * as file from "~/core/file";
6
+ import { get_local_iso } from "~/core/get_local_iso";
6
7
  import { spawn } from "~/core/spawn";
7
8
 
8
9
  const parsed_args = util.parseArgs({
@@ -25,7 +26,7 @@ const WATCH = parsed_args.values.watch;
25
26
  const VERBOSE = parsed_args.values.verbose;
26
27
 
27
28
  function log(...args: any[]) {
28
- const timestamp = new Date().toISOString();
29
+ const timestamp = get_local_iso(new Date());
29
30
  console.debug(`[${timestamp}]`, ...args);
30
31
  }
31
32
 
@@ -0,0 +1,20 @@
1
+ export function get_local_iso(date: Date) {
2
+ const d: Record<string, string> = {};
3
+ for (const part of FORMATTER.formatToParts(date)) {
4
+ d[part.type] = part.value;
5
+ }
6
+
7
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
8
+ const timestamp = `${d.year}-${d.month}-${d.day}T${d.hour}:${d.minute}:${d.second}.${ms}Z`;
9
+ return timestamp;
10
+ }
11
+
12
+ const FORMATTER = new Intl.DateTimeFormat("en-CA", {
13
+ year: "numeric",
14
+ month: "2-digit",
15
+ day: "2-digit",
16
+ hour: "2-digit",
17
+ minute: "2-digit",
18
+ second: "2-digit",
19
+ hour12: false,
20
+ });
@@ -11,9 +11,9 @@ import { assertNever } from "~/core/assertNever";
11
11
  import { cli } from "~/core/cli";
12
12
  import { colors } from "~/core/colors";
13
13
  import { fetch_json } from "~/core/fetch_json";
14
+ import { get_timeout_fn } from "~/core/get_timeout_fn";
14
15
  import { is_finite_value } from "~/core/is_finite_value";
15
16
  import { semver_compare } from "~/core/semver_compare";
16
- import { sleep } from "~/core/sleep";
17
17
 
18
18
  type Props = {
19
19
  name: string;
@@ -162,21 +162,12 @@ export function AutoUpdate(props: Props) {
162
162
 
163
163
  async function get_latest_version() {
164
164
  const timeout_ms = is_finite_value(props.timeoutMs) ? props.timeoutMs : 2 * 1000;
165
-
166
- const npm_json = await Promise.race([
167
- fetch_json(`https://registry.npmjs.org/${props.name}`),
168
-
169
- sleep(timeout_ms).then(() => {
170
- abort(new Error("AutoUpdate timeout"));
171
- }),
172
- ]);
173
-
165
+ const timeout = get_timeout_fn(timeout_ms, "AutoUpdate timeout");
166
+ const npm_json = await timeout(fetch_json(`https://registry.npmjs.org/${props.name}`));
174
167
  const maybe_version = npm_json?.["dist-tags"]?.latest;
175
-
176
168
  if (typeof maybe_version === "string") {
177
169
  return maybe_version;
178
170
  }
179
-
180
171
  throw new Error("Unable to retrieve latest version from npm");
181
172
  }
182
173
 
@@ -380,7 +380,11 @@ function SelectCommitRangesInternal(props: Props) {
380
380
  {sync_status !== "allow_unassigned" ? null : (
381
381
  <FormatText
382
382
  wrapper={<Ink.Text color={colors.gray} />}
383
- message="Press {s} to {sync} the {count} assigned commits to Github"
383
+ message={
384
+ argv.sync
385
+ ? "Press {s} to {sync} the {count} assigned commits to Github"
386
+ : "Press {s} to {sync} the {count} assigned commits locally"
387
+ }
384
388
  values={{
385
389
  ...S_TO_SYNC_VALUES,
386
390
  count: (
@@ -393,21 +397,15 @@ function SelectCommitRangesInternal(props: Props) {
393
397
  )}
394
398
  </React.Fragment>
395
399
  ) : (
396
- <React.Fragment>
397
- {argv.sync ? (
398
- <FormatText
399
- wrapper={<Ink.Text />}
400
- message="🎉 Done! Press {s} to {sync} the commits to Github"
401
- values={S_TO_SYNC_VALUES}
402
- />
403
- ) : (
404
- <FormatText
405
- wrapper={<Ink.Text />}
406
- message="🎉 Done! Press {s} to {save} the commits locally"
407
- values={S_TO_SYNC_VALUES}
408
- />
409
- )}
410
- </React.Fragment>
400
+ <FormatText
401
+ wrapper={<Ink.Text />}
402
+ message={
403
+ argv.sync
404
+ ? "🎉 Done! Press {s} to {sync} the PRs to Github"
405
+ : "🎉 Done! Press {s} to {sync} the PRs locally"
406
+ }
407
+ values={S_TO_SYNC_VALUES}
408
+ />
411
409
  )}
412
410
 
413
411
  <Ink.Box>
@@ -78,7 +78,9 @@ Rebase.run = async function run(props: Props) {
78
78
  actions.exit(20);
79
79
  }
80
80
 
81
+ actions.debug("start CommitMetadata.range");
81
82
  const next_commit_range = await CommitMetadata.range();
83
+ actions.debug("end CommitMetadata.range");
82
84
 
83
85
  actions.output(
84
86
  <FormatText
@@ -167,7 +169,9 @@ Rebase.run = async function run(props: Props) {
167
169
  // of original branch to the newly created temporary branch
168
170
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
169
171
 
172
+ actions.debug("start restore_git()");
170
173
  restore_git();
174
+ actions.debug("end restore_git()");
171
175
  }
172
176
 
173
177
  // cleanup git operations if cancelled during manual rebase
@@ -1,9 +1,7 @@
1
1
  import { Store } from "~/app/Store";
2
- import * as Metadata from "~/core/Metadata";
3
- import { cli } from "~/core/cli";
2
+ import * as git from "~/core/git";
4
3
  import * as github from "~/core/github";
5
4
 
6
- export type CommitMetadata = Awaited<ReturnType<typeof commit>>;
7
5
  export type CommitRange = Awaited<ReturnType<typeof range>>;
8
6
 
9
7
  type GithubPRStatus = ReturnType<typeof github.pr_status>;
@@ -15,24 +13,25 @@ type CommitGroup = {
15
13
  pr: null | PullRequest;
16
14
  base: null | string;
17
15
  dirty: boolean;
18
- commits: Array<CommitMetadata>;
16
+ commits: Array<git.Commit>;
19
17
  };
20
18
 
21
19
  export type SimpleGroup = { id: string; title: string };
22
20
  type CommitGroupMap = { [sha: string]: SimpleGroup };
23
21
 
24
22
  export async function range(commit_group_map?: CommitGroupMap) {
25
- const master_branch = Store.getState().master_branch;
26
-
27
23
  // gather all open prs in repo first
28
24
  // cheaper query to populate cache
29
25
  await github.pr_list();
30
26
 
31
- const commit_list = await get_commit_list();
27
+ const master_branch = Store.getState().master_branch;
28
+ const commit_list = await git.get_commits(`${master_branch}..HEAD`);
32
29
 
33
30
  const pr_lookup: Record<string, void | PullRequest> = {};
34
31
 
35
32
  let invalid = false;
33
+ let last_group_id: null | string = null;
34
+
36
35
  const group_map = new Map<string, CommitGroup>();
37
36
 
38
37
  for (const commit of commit_list) {
@@ -57,10 +56,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
57
56
  }
58
57
 
59
58
  if (id) {
60
- const group_key_list = Array.from(group_map.keys());
61
- const last_key = group_key_list[group_key_list.length - 1];
62
-
63
- if (group_map.has(id) && last_key !== id) {
59
+ if (group_map.has(id) && last_group_id !== id) {
64
60
  // if we've seen this id before and it's not
65
61
  // the last added key then we are out of order
66
62
  // console.debug("INVALID", "OUT OF ORDER");
@@ -87,6 +83,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
87
83
 
88
84
  group.commits.push(commit);
89
85
  group_map.set(id, group);
86
+ last_group_id = id;
90
87
  }
91
88
 
92
89
  // check each group for dirty state and base
@@ -183,53 +180,4 @@ export async function range(commit_group_map?: CommitGroupMap) {
183
180
  return { invalid, group_list, commit_list, pr_lookup, UNASSIGNED };
184
181
  }
185
182
 
186
- async function get_commit_list() {
187
- const master_branch = Store.getState().master_branch;
188
- const log_result = await cli(
189
- `git log ${master_branch}..HEAD --oneline --format=%H --color=never`,
190
- );
191
-
192
- if (!log_result.stdout) {
193
- return [];
194
- }
195
-
196
- const sha_list = lines(log_result.stdout).reverse();
197
-
198
- const commit_metadata_list = [];
199
-
200
- for (let i = 0; i < sha_list.length; i++) {
201
- const sha = sha_list[i];
202
- const commit_metadata = await commit(sha);
203
- commit_metadata_list.push(commit_metadata);
204
- }
205
-
206
- return commit_metadata_list;
207
- }
208
-
209
- export async function commit(sha: string) {
210
- const full_message = (await cli(`git show -s --format=%B ${sha}`)).stdout;
211
- const metadata = await Metadata.read(full_message);
212
- const branch_id = metadata?.id;
213
- const subject_line = get_subject_line(full_message);
214
- const title = metadata?.title;
215
-
216
- return {
217
- sha,
218
- full_message,
219
- subject_line,
220
- branch_id,
221
- title,
222
- };
223
- }
224
-
225
- function get_subject_line(message: string) {
226
- const line_list = lines(message);
227
- const first_line = line_list[0];
228
- return Metadata.remove(first_line);
229
- }
230
-
231
- function lines(value: string) {
232
- return value.split("\n");
233
- }
234
-
235
183
  export const UNASSIGNED = "unassigned";
@@ -17,6 +17,7 @@ test("read handles bulleted lists", () => {
17
17
  const metadata = Metadata.read(body);
18
18
 
19
19
  expect(metadata).toEqual({
20
+ subject: "[feat] implement various features",
20
21
  id: "DdKIFyufW",
21
22
  title: "saved group title",
22
23
  });
@@ -63,6 +64,7 @@ test("read handles slashes in branch name", () => {
63
64
  const metadata = Metadata.read(body);
64
65
 
65
66
  expect(metadata).toEqual({
67
+ subject: "[fix] slash in branch name",
66
68
  id: "dev/noah/fix-slash-branch",
67
69
  title: "fix slash branch",
68
70
  });
@@ -109,6 +111,7 @@ test("read handles double quotes", () => {
109
111
  const metadata = Metadata.read(body);
110
112
 
111
113
  expect(metadata).toEqual({
114
+ subject: 'Revert "[abc / 123] subject (#1234)"',
112
115
  id: "dev/noah/fix-slash-branch",
113
116
  title: 'Revert \\"[abc / 123] subject (#1234)\\"',
114
117
  });
@@ -6,6 +6,7 @@ type InputMetadataValues = {
6
6
  };
7
7
 
8
8
  type OutputMetadataValues = {
9
+ subject: null | string;
9
10
  id: null | string;
10
11
  title: null | string;
11
12
  };
@@ -28,7 +29,14 @@ export function write(message: string, values: InputMetadataValues) {
28
29
  }
29
30
 
30
31
  export function read(message: string): OutputMetadataValues {
31
- const values: OutputMetadataValues = { id: null, title: null };
32
+ const values: OutputMetadataValues = { subject: null, id: null, title: null };
33
+
34
+ const match_subject = message.match(RE.subject_line);
35
+
36
+ if (match_subject?.groups) {
37
+ values.subject = match_subject.groups["subject"];
38
+ invariant(values.subject, "subject must exist");
39
+ }
32
40
 
33
41
  const match_id = message.match(RE.stack_id);
34
42
 
@@ -69,6 +77,8 @@ const TEMPLATE = {
69
77
  };
70
78
 
71
79
  const RE = {
80
+ // https://regex101.com/r/pOrChS/1
81
+ subject_line: /^(?<subject>[^\n]*)/,
72
82
  // https://regex101.com/r/wLmGVq/1
73
83
  stack_id: new RegExp(`${TEMPLATE.stack_id("(?<id>[^\\s]+)")}`, "i"),
74
84
  group_title: new RegExp(TEMPLATE.group_title("(?<title>[^\\n^\\r]+)"), "i"),
@@ -0,0 +1,95 @@
1
+ // Bun Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`get_commits ABC..DEF:
4
+ [
5
+ {
6
+ "branch_id": null,
7
+ "full_message": "homebrew-git-stack 2.7.1",
8
+ "sha": "ba067f8ad641dda7e65e3c2acb2421c955843829",
9
+ "subject_line": "homebrew-git-stack 2.7.1",
10
+ "title": null,
11
+ },
12
+ {
13
+ "branch_id": "noah/paint-test---4gwpqhd033n6y5",
14
+ "full_message":
15
+ "Rebase: debug logs
16
+
17
+ git-stack-id: noah/paint-test---4gwpqhd033n6y5
18
+ git-stack-title: Rebase: debug logs"
19
+ ,
20
+ "sha": "7c8da9f5fe681fc7459a0e737241df631983cd3c",
21
+ "subject_line": "Rebase: debug logs",
22
+ "title": "Rebase: debug logs",
23
+ },
24
+ {
25
+ "branch_id": "noah/paint-test---4gwpqirewoudxa",
26
+ "full_message":
27
+ "CommitMetadata: track last group id
28
+
29
+ git-stack-id: noah/paint-test---4gwpqirewoudxa
30
+ git-stack-title: CommitMetadata: track last group id"
31
+ ,
32
+ "sha": "47a69d37f8c5cc796884a91fa9e93fc1db5297dd",
33
+ "subject_line": "CommitMetadata: track last group id",
34
+ "title": "CommitMetadata: track last group id",
35
+ },
36
+ {
37
+ "branch_id": "noah/paint-test---4gwpqjcpu7-isv",
38
+ "full_message":
39
+ "Github: pr_list duration timer
40
+
41
+ git-stack-id: noah/paint-test---4gwpqjcpu7-isv
42
+ git-stack-title: Github: pr_list duration timer"
43
+ ,
44
+ "sha": "8ccf42a7c72aa194fafc7c326f5874f5a0a009c6",
45
+ "subject_line": "Github: pr_list duration timer",
46
+ "title": "Github: pr_list duration timer",
47
+ },
48
+ ]
49
+ 1`] = `
50
+ [
51
+ {
52
+ "branch_id": null,
53
+ "full_message": "homebrew-git-stack 2.7.1",
54
+ "sha": "ba067f8ad641dda7e65e3c2acb2421c955843829",
55
+ "subject_line": "homebrew-git-stack 2.7.1",
56
+ "title": null,
57
+ },
58
+ {
59
+ "branch_id": "noah/paint-test---4gwpqhd033n6y5",
60
+ "full_message":
61
+ "Rebase: debug logs
62
+
63
+ git-stack-id: noah/paint-test---4gwpqhd033n6y5
64
+ git-stack-title: Rebase: debug logs"
65
+ ,
66
+ "sha": "7c8da9f5fe681fc7459a0e737241df631983cd3c",
67
+ "subject_line": "Rebase: debug logs",
68
+ "title": "Rebase: debug logs",
69
+ },
70
+ {
71
+ "branch_id": "noah/paint-test---4gwpqirewoudxa",
72
+ "full_message":
73
+ "CommitMetadata: track last group id
74
+
75
+ git-stack-id: noah/paint-test---4gwpqirewoudxa
76
+ git-stack-title: CommitMetadata: track last group id"
77
+ ,
78
+ "sha": "47a69d37f8c5cc796884a91fa9e93fc1db5297dd",
79
+ "subject_line": "CommitMetadata: track last group id",
80
+ "title": "CommitMetadata: track last group id",
81
+ },
82
+ {
83
+ "branch_id": "noah/paint-test---4gwpqjcpu7-isv",
84
+ "full_message":
85
+ "Github: pr_list duration timer
86
+
87
+ git-stack-id: noah/paint-test---4gwpqjcpu7-isv
88
+ git-stack-title: Github: pr_list duration timer"
89
+ ,
90
+ "sha": "8ccf42a7c72aa194fafc7c326f5874f5a0a009c6",
91
+ "subject_line": "Github: pr_list duration timer",
92
+ "title": "Github: pr_list duration timer",
93
+ },
94
+ ]
95
+ `;
@@ -0,0 +1,11 @@
1
+ export function get_timeout_fn(ms: number, message: string) {
2
+ return function timeout<T>(promise: Promise<T>) {
3
+ let id: ReturnType<typeof setTimeout>;
4
+
5
+ const timeout = new Promise<never>((_resolve, reject) => {
6
+ id = setTimeout(() => reject(new Error(message)), ms);
7
+ });
8
+
9
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(id));
10
+ };
11
+ }
@@ -0,0 +1,54 @@
1
+ import { test, expect } from "bun:test";
2
+
3
+ import * as git from "~/core/git";
4
+
5
+ test("get_commits ABC..DEF", async () => {
6
+ const commits = await git.get_commits("e781ede..8ccf42a");
7
+ expect(commits).toMatchSnapshot(`
8
+ [
9
+ {
10
+ "branch_id": null,
11
+ "full_message": "homebrew-git-stack 2.7.1",
12
+ "sha": "ba067f8ad641dda7e65e3c2acb2421c955843829",
13
+ "subject_line": "homebrew-git-stack 2.7.1",
14
+ "title": null,
15
+ },
16
+ {
17
+ "branch_id": "noah/paint-test---4gwpqhd033n6y5",
18
+ "full_message":
19
+ "Rebase: debug logs
20
+
21
+ git-stack-id: noah/paint-test---4gwpqhd033n6y5
22
+ git-stack-title: Rebase: debug logs"
23
+ ,
24
+ "sha": "7c8da9f5fe681fc7459a0e737241df631983cd3c",
25
+ "subject_line": "Rebase: debug logs",
26
+ "title": "Rebase: debug logs",
27
+ },
28
+ {
29
+ "branch_id": "noah/paint-test---4gwpqirewoudxa",
30
+ "full_message":
31
+ "CommitMetadata: track last group id
32
+
33
+ git-stack-id: noah/paint-test---4gwpqirewoudxa
34
+ git-stack-title: CommitMetadata: track last group id"
35
+ ,
36
+ "sha": "47a69d37f8c5cc796884a91fa9e93fc1db5297dd",
37
+ "subject_line": "CommitMetadata: track last group id",
38
+ "title": "CommitMetadata: track last group id",
39
+ },
40
+ {
41
+ "branch_id": "noah/paint-test---4gwpqjcpu7-isv",
42
+ "full_message":
43
+ "Github: pr_list duration timer
44
+
45
+ git-stack-id: noah/paint-test---4gwpqjcpu7-isv
46
+ git-stack-title: Github: pr_list duration timer"
47
+ ,
48
+ "sha": "8ccf42a7c72aa194fafc7c326f5874f5a0a009c6",
49
+ "subject_line": "Github: pr_list duration timer",
50
+ "title": "Github: pr_list duration timer",
51
+ },
52
+ ]
53
+ `);
54
+ });
@@ -0,0 +1,55 @@
1
+ import * as Metadata from "~/core/Metadata";
2
+ import { cli } from "~/core/cli";
3
+
4
+ export type Commit = Awaited<ReturnType<typeof get_commits>>[0];
5
+
6
+ export async function get_commits(dot_range: string) {
7
+ const log_result = await cli(`git log ${dot_range} --format=${FORMAT} --color=never`);
8
+
9
+ if (!log_result.stdout) {
10
+ return [];
11
+ }
12
+
13
+ const commit_list = [];
14
+
15
+ for (let record of log_result.stdout.split(SEP.record)) {
16
+ record = record.replace(/^\n/, "");
17
+ record = record.replace(/\n$/, "");
18
+
19
+ if (!record) continue;
20
+
21
+ const [sha, full_message] = record.split(SEP.field);
22
+
23
+ const metadata = Metadata.read(full_message);
24
+ const branch_id = metadata.id;
25
+ const subject_line = metadata.subject || "";
26
+ const title = metadata.title;
27
+
28
+ const commit = {
29
+ sha,
30
+ full_message,
31
+ subject_line,
32
+ branch_id,
33
+ title,
34
+ };
35
+
36
+ commit_list.push(commit);
37
+ }
38
+
39
+ commit_list.reverse();
40
+
41
+ return commit_list;
42
+ }
43
+
44
+ // Why these separators?
45
+ // - Rare in human written text
46
+ // - Supported in git %xNN to write bytes
47
+ // - Supported in javascript \xNN to write bytes
48
+ // - Used historically as separators in unicode
49
+ // https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators
50
+ const SEP = {
51
+ record: "\x1e",
52
+ field: "\x1f",
53
+ };
54
+
55
+ const FORMAT = `%H${SEP.field}%B${SEP.record}`;
@@ -8,6 +8,7 @@ import * as Ink from "ink-cjs";
8
8
 
9
9
  import { Brackets } from "~/app/Brackets";
10
10
  import { Store } from "~/app/Store";
11
+ import { Timer } from "~/core/Timer";
11
12
  import { cli } from "~/core/cli";
12
13
  import { colors } from "~/core/colors";
13
14
  import { get_tmp_dir } from "~/core/get_tmp_dir";
@@ -19,6 +20,9 @@ export async function pr_list(): Promise<Array<PullRequest>> {
19
20
  const state = Store.getState();
20
21
  const actions = state.actions;
21
22
 
23
+ const timer = Timer();
24
+ actions.debug("start github.pr_list");
25
+
22
26
  const username = state.username;
23
27
  const repo_path = state.repo_path;
24
28
  invariant(username, "username must exist");
@@ -53,6 +57,8 @@ export async function pr_list(): Promise<Array<PullRequest>> {
53
57
  }
54
58
  });
55
59
 
60
+ const duration = timer.duration();
61
+ actions.debug(`end github.pr_list (duration=${duration})`);
56
62
  return result_pr_list;
57
63
  }
58
64