git-stack-cli 1.13.0 → 1.13.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.
@@ -26213,6 +26213,7 @@ const BaseStore = createStore()(immer((set, get) => ({
26213
26213
  commit_map: null,
26214
26214
  pr_templates: [],
26215
26215
  pr_template_body: null,
26216
+ sync_github: null,
26216
26217
  step: "loading",
26217
26218
  output: [],
26218
26219
  pending_output: {},
@@ -29781,7 +29782,13 @@ async function pr_status(branch) {
29781
29782
  }
29782
29783
  async function pr_create(args) {
29783
29784
  const title = safe_quote(args.title);
29784
- let command = `gh pr create --fill --head ${args.branch} --base ${args.base} --title="${title}" --body="${args.body}"`;
29785
+ // explicit refs/heads head branch to avoid creation failing
29786
+ //
29787
+ // ❯ gh pr create --head origin/gs-ED2etrzv2 --base gs-6LAx-On45 --title="2024-01-05 test" --body=""
29788
+ // pull request create failed: GraphQL: Head sha can't be blank, Base sha can't be blank, No commits between gs-6LAx-On45 and origin/gs-ED2etrzv2, Head ref must be a branch (createPullRequest)
29789
+ //
29790
+ // https://github.com/cli/cli/issues/5465
29791
+ let command = `gh pr create --head refs/heads/${args.branch} --base ${args.base} --title="${title}" --body="${args.body}"`;
29785
29792
  if (args.draft) {
29786
29793
  command += " --draft";
29787
29794
  }
@@ -30322,9 +30329,9 @@ function DirtyCheck(props) {
30322
30329
 
30323
30330
  function GatherMetadata(props) {
30324
30331
  const fallback = (reactExports.createElement(Text, { color: colors.yellow }, "Gathering local git information\u2026"));
30325
- return (reactExports.createElement(Await, { fallback: fallback, function: run$9 }, props.children));
30332
+ return (reactExports.createElement(Await, { fallback: fallback, function: run$a }, props.children));
30326
30333
  }
30327
- async function run$9() {
30334
+ async function run$a() {
30328
30335
  const actions = Store.getState().actions;
30329
30336
  const argv = Store.getState().argv;
30330
30337
  try {
@@ -30418,9 +30425,9 @@ function format_time(date) {
30418
30425
  }
30419
30426
 
30420
30427
  function GithubApiError() {
30421
- return reactExports.createElement(Await, { fallback: null, function: run$8 });
30428
+ return reactExports.createElement(Await, { fallback: null, function: run$9 });
30422
30429
  }
30423
- async function run$8() {
30430
+ async function run$9() {
30424
30431
  const actions = Store.getState().actions;
30425
30432
  const res = await cli(`gh api https://api.github.com/rate_limit`);
30426
30433
  const res_json = JSON.parse(res.stdout);
@@ -30462,7 +30469,7 @@ function LocalCommitStatus(props) {
30462
30469
  if (argv["mock-metadata"]) {
30463
30470
  return (reactExports.createElement(Await, { fallback: fallback, function: mock_metadata }, props.children));
30464
30471
  }
30465
- return (reactExports.createElement(Await, { fallback: fallback, function: run$7 }, props.children));
30472
+ return (reactExports.createElement(Await, { fallback: fallback, function: run$8 }, props.children));
30466
30473
  }
30467
30474
  async function mock_metadata() {
30468
30475
  const module = await Promise.resolve().then(function () { return metadata; });
@@ -30472,7 +30479,7 @@ async function mock_metadata() {
30472
30479
  state.step = "status";
30473
30480
  });
30474
30481
  }
30475
- async function run$7() {
30482
+ async function run$8() {
30476
30483
  const actions = Store.getState().actions;
30477
30484
  try {
30478
30485
  const commit_range = await range();
@@ -30552,9 +30559,21 @@ function encode(value) {
30552
30559
  }
30553
30560
 
30554
30561
  function Rebase() {
30555
- return (reactExports.createElement(Await, { function: Rebase.run, fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026") }));
30562
+ const abort_handler = reactExports.useRef(() => { });
30563
+ reactExports.useEffect(function listen_sigint() {
30564
+ process.once("SIGINT", sigint_handler);
30565
+ return function cleanup() {
30566
+ process.removeListener("SIGINT", sigint_handler);
30567
+ };
30568
+ function sigint_handler() {
30569
+ abort_handler.current();
30570
+ }
30571
+ }, []);
30572
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: async function () {
30573
+ await Rebase.run({ abort_handler });
30574
+ } }));
30556
30575
  }
30557
- Rebase.run = async function run() {
30576
+ Rebase.run = async function run(args) {
30558
30577
  const state = Store.getState();
30559
30578
  const actions = state.actions;
30560
30579
  const branch_name = state.branch_name;
@@ -30566,7 +30585,10 @@ Rebase.run = async function run() {
30566
30585
  invariant(commit_range, "commit_range must exist");
30567
30586
  invariant(repo_root, "repo_root must exist");
30568
30587
  // always listen for SIGINT event and restore git state
30569
- process.once("SIGINT", handle_exit);
30588
+ args.abort_handler.current = async function sigint_handler() {
30589
+ actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
30590
+ handle_exit(19);
30591
+ };
30570
30592
  const temp_branch_name = `${branch_name}_${short_id()}`;
30571
30593
  try {
30572
30594
  // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
@@ -30629,7 +30651,7 @@ Rebase.run = async function run() {
30629
30651
  actions.error(err.message);
30630
30652
  }
30631
30653
  }
30632
- handle_exit();
30654
+ handle_exit(20);
30633
30655
  }
30634
30656
  // cleanup git operations if cancelled during manual rebase
30635
30657
  function restore_git() {
@@ -30651,7 +30673,7 @@ Rebase.run = async function run() {
30651
30673
  }
30652
30674
  cli.sync(`pwd`, spawn_options);
30653
30675
  }
30654
- function handle_exit() {
30676
+ function handle_exit(code) {
30655
30677
  actions.output(reactExports.createElement(Text, { color: colors.yellow },
30656
30678
  "Restoring ",
30657
30679
  reactExports.createElement(Brackets, null, branch_name),
@@ -30661,7 +30683,7 @@ Rebase.run = async function run() {
30661
30683
  "Restored ",
30662
30684
  reactExports.createElement(Brackets, null, branch_name),
30663
30685
  "."));
30664
- actions.exit(6);
30686
+ actions.exit(code);
30665
30687
  }
30666
30688
  };
30667
30689
 
@@ -30669,117 +30691,22 @@ function LocalMergeRebase() {
30669
30691
  return reactExports.createElement(Rebase, null);
30670
30692
  }
30671
30693
 
30672
- function write(args) {
30673
- const stack_table = table(args);
30674
- let result = args.body;
30675
- if (RE.stack_table_link.test(result)) {
30676
- // replace stack table
30677
- result = result.replace(RE.stack_table_link, stack_table);
30678
- }
30679
- else if (RE.stack_table_legacy.test(result)) {
30680
- // replace stack table
30681
- result = result.replace(RE.stack_table_legacy, stack_table);
30682
- }
30683
- else {
30684
- // append stack table
30685
- result = `${result}\n\n${stack_table}`;
30686
- }
30687
- result = result.trimEnd();
30688
- return result;
30689
- }
30690
- function table(args) {
30691
- const stack_pr_url_list = [...args.pr_url_list];
30692
- const old_stack = parse(args.body);
30693
- // remove existing stack pr urls from the old stack pr urls
30694
- for (const pr_url of stack_pr_url_list) {
30695
- old_stack.delete(pr_url);
30696
- }
30697
- // add remaining old stack pr urls to the front of stack pr url list
30698
- const old_pr_list = Array.from(old_stack.keys());
30699
- old_pr_list.reverse();
30700
- for (const pr_url of old_pr_list) {
30701
- stack_pr_url_list.unshift(pr_url);
30702
- }
30703
- const stack_list = [];
30704
- const num_digits = String(stack_pr_url_list.length).length;
30705
- for (let i = 0; i < stack_pr_url_list.length; i++) {
30706
- const pr_url = stack_pr_url_list[i];
30707
- const selected = args.selected_url === pr_url;
30708
- let icon;
30709
- if (old_stack.has(pr_url)) {
30710
- icon = "✅";
30711
- }
30712
- else if (selected) {
30713
- icon = "👉";
30714
- }
30715
- else {
30716
- icon = "⏳";
30717
- }
30718
- const num = String(i + 1).padStart(num_digits, "0");
30719
- stack_list.push(TEMPLATE.row({ icon, num, pr_url }));
30720
- }
30721
- if (!stack_list.length) {
30722
- return "";
30723
- }
30724
- return TEMPLATE.stack_table_link(["", ...stack_list, "", ""].join("\n"));
30725
- }
30726
- function parse(body) {
30727
- let stack_table_match = body.match(RE.stack_table_link);
30728
- if (!stack_table_match?.groups) {
30729
- stack_table_match = body.match(RE.stack_table_legacy);
30730
- }
30731
- if (!stack_table_match?.groups) {
30732
- return new Map();
30733
- }
30734
- const rows_string = stack_table_match.groups["rows"];
30735
- const row_list = rows_string.split("\n");
30736
- const result = new Map();
30737
- for (const row of row_list) {
30738
- const row_match = row.match(RE.row);
30739
- const parsed_row = row_match?.groups;
30740
- if (!parsed_row) {
30741
- // skip invalid row
30742
- continue;
30743
- }
30744
- if (!RE.pr_url.test(parsed_row.pr_url)) {
30745
- continue;
30746
- }
30747
- result.set(parsed_row.pr_url, parsed_row);
30748
- }
30749
- return result;
30750
- }
30751
- const TEMPLATE = {
30752
- stack_table_legacy(rows) {
30753
- return `#### git stack${rows}`;
30754
- },
30755
- stack_table_link(rows) {
30756
- return `#### [git stack](https://github.com/magus/git-stack-cli)${rows}`;
30757
- },
30758
- row(args) {
30759
- return `- ${args.icon} \`${args.num}\` ${args.pr_url}`;
30760
- },
30761
- };
30762
- const RE = {
30763
- // https://regex101.com/r/kqB9Ft/1
30764
- stack_table_legacy: new RegExp(TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
30765
- stack_table_link: new RegExp(TEMPLATE.stack_table_link("ROWS")
30766
- .replace("[", "\\[")
30767
- .replace("]", "\\]")
30768
- .replace("(", "\\(")
30769
- .replace(")", "\\)")
30770
- .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
30771
- row: new RegExp(TEMPLATE.row({
30772
- icon: "(?<icon>.+)",
30773
- num: "(?<num>\\d+)",
30774
- pr_url: "(?<pr_url>.+)",
30775
- })),
30776
- pr_url: /^https:\/\/.*$/,
30777
- };
30778
-
30779
30694
  function ManualRebase() {
30780
- return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: run$6 }));
30695
+ const abort_handler = reactExports.useRef(() => { });
30696
+ reactExports.useEffect(function listen_sigint() {
30697
+ process.once("SIGINT", sigint_handler);
30698
+ return function cleanup() {
30699
+ process.removeListener("SIGINT", sigint_handler);
30700
+ };
30701
+ async function sigint_handler() {
30702
+ abort_handler.current();
30703
+ }
30704
+ }, []);
30705
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: async function () {
30706
+ await run$7({ abort_handler });
30707
+ } }));
30781
30708
  }
30782
- async function run$6() {
30709
+ async function run$7(args) {
30783
30710
  const state = Store.getState();
30784
30711
  const actions = state.actions;
30785
30712
  const argv = state.argv;
@@ -30792,54 +30719,53 @@ async function run$6() {
30792
30719
  invariant(commit_map, "commit_map must exist");
30793
30720
  invariant(repo_root, "repo_root must exist");
30794
30721
  // always listen for SIGINT event and restore git state
30795
- process.once("SIGINT", handle_exit);
30796
- // get latest merge_base relative to local master
30797
- const merge_base = (await cli(`git merge-base HEAD ${master_branch}`)).stdout;
30798
- // immediately paint all commit to preserve selected commit ranges
30799
- let commit_range = await range(commit_map);
30800
- // reverse group list to ensure we create git revise in correct order
30801
- commit_range.group_list.reverse();
30802
- for (const commit of commit_range.commit_list) {
30803
- const group_from_map = commit_map[commit.sha];
30804
- commit.branch_id = group_from_map.id;
30805
- commit.title = group_from_map.title;
30806
- }
30807
- await GitReviseTodo.execute({
30808
- rebase_group_index: 0,
30809
- rebase_merge_base: merge_base,
30810
- commit_range,
30811
- });
30812
- let DEFAULT_PR_BODY = "";
30813
- if (state.pr_template_body) {
30814
- DEFAULT_PR_BODY = state.pr_template_body;
30815
- }
30722
+ args.abort_handler.current = function sigint_handler() {
30723
+ actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
30724
+ handle_exit(15);
30725
+ };
30816
30726
  const temp_branch_name = `${branch_name}_${short_id()}`;
30817
- commit_range = await range(commit_map);
30818
- // reverse commit list so that we can cherry-pick in order
30819
- commit_range.group_list.reverse();
30820
- let rebase_merge_base = merge_base;
30821
- let rebase_group_index = 0;
30822
- for (let i = 0; i < commit_range.group_list.length; i++) {
30823
- const group = commit_range.group_list[i];
30824
- if (!group.dirty) {
30825
- continue;
30727
+ try {
30728
+ // get latest merge_base relative to local master
30729
+ const merge_base = (await cli(`git merge-base HEAD ${master_branch}`))
30730
+ .stdout;
30731
+ // immediately paint all commit to preserve selected commit ranges
30732
+ let commit_range = await range(commit_map);
30733
+ // reverse group list to ensure we create git revise in correct order
30734
+ commit_range.group_list.reverse();
30735
+ for (const commit of commit_range.commit_list) {
30736
+ const group_from_map = commit_map[commit.sha];
30737
+ commit.branch_id = group_from_map.id;
30738
+ commit.title = group_from_map.title;
30826
30739
  }
30827
- if (i > 0) {
30828
- const prev_group = commit_range.group_list[i - 1];
30829
- const prev_commit = prev_group.commits[prev_group.commits.length - 1];
30830
- rebase_merge_base = prev_commit.sha;
30831
- rebase_group_index = i;
30740
+ await GitReviseTodo.execute({
30741
+ rebase_group_index: 0,
30742
+ rebase_merge_base: merge_base,
30743
+ commit_range,
30744
+ });
30745
+ commit_range = await range(commit_map);
30746
+ // reverse commit list so that we can cherry-pick in order
30747
+ commit_range.group_list.reverse();
30748
+ let rebase_merge_base = merge_base;
30749
+ let rebase_group_index = 0;
30750
+ for (let i = 0; i < commit_range.group_list.length; i++) {
30751
+ const group = commit_range.group_list[i];
30752
+ if (!group.dirty) {
30753
+ continue;
30754
+ }
30755
+ if (i > 0) {
30756
+ const prev_group = commit_range.group_list[i - 1];
30757
+ const prev_commit = prev_group.commits[prev_group.commits.length - 1];
30758
+ rebase_merge_base = prev_commit.sha;
30759
+ rebase_group_index = i;
30760
+ }
30761
+ break;
30832
30762
  }
30833
- break;
30834
- }
30835
- actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
30836
- actions.debug(`rebase_group_index = ${rebase_group_index}`);
30837
- // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
30838
- try {
30763
+ actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
30764
+ actions.debug(`rebase_group_index = ${rebase_group_index}`);
30765
+ // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
30839
30766
  // must perform rebase from repo root for applying git patch
30840
30767
  process.chdir(repo_root);
30841
30768
  await cli(`pwd`);
30842
- actions.output(reactExports.createElement(Text, { color: colors.yellow, wrap: "truncate-end" }, "Rebasing\u2026"));
30843
30769
  // create temporary branch
30844
30770
  await cli(`git checkout -b ${temp_branch_name}`);
30845
30771
  await GitReviseTodo.execute({
@@ -30850,13 +30776,18 @@ async function run$6() {
30850
30776
  // after all commits have been modified move the pointer
30851
30777
  // of original branch to the newly created temporary branch
30852
30778
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
30779
+ restore_git();
30853
30780
  if (argv.sync) {
30854
- await sync_github();
30781
+ actions.set((state) => {
30782
+ state.step = "sync-github";
30783
+ state.sync_github = { commit_range, rebase_group_index };
30784
+ });
30785
+ }
30786
+ else {
30787
+ actions.set((state) => {
30788
+ state.step = "post-rebase-status";
30789
+ });
30855
30790
  }
30856
- restore_git();
30857
- actions.set((state) => {
30858
- state.step = "post-rebase-status";
30859
- });
30860
30791
  }
30861
30792
  catch (err) {
30862
30793
  if (err instanceof Error) {
@@ -30866,164 +30797,7 @@ async function run$6() {
30866
30797
  if (!argv.verbose) {
30867
30798
  actions.error("Try again with `--verbose` to see more information.");
30868
30799
  }
30869
- handle_exit();
30870
- }
30871
- async function sync_github() {
30872
- // in order to sync we walk from rebase_group_index to HEAD
30873
- // checking out each group and syncing to github
30874
- // start from HEAD and work backward to rebase_group_index
30875
- const push_group_list = [];
30876
- let lookback_index = 0;
30877
- for (let i = 0; i < commit_range.group_list.length; i++) {
30878
- const index = commit_range.group_list.length - 1 - i;
30879
- // do not go past rebase_group_index
30880
- if (index < rebase_group_index) {
30881
- break;
30882
- }
30883
- const group = commit_range.group_list[index];
30884
- // console.debug({ i, index, group });
30885
- if (i > 0) {
30886
- const prev_group = commit_range.group_list[index + 1];
30887
- lookback_index += prev_group.commits.length;
30888
- }
30889
- // console.debug(`git show head~${lookback_index}`);
30890
- // push group and lookback_index onto front of push_group_list
30891
- push_group_list.unshift({ group, lookback_index });
30892
- }
30893
- actions.output(reactExports.createElement(FormatText, { wrapper: reactExports.createElement(Text, { color: colors.yellow, wrap: "truncate-end" }), message: "Syncing {group_list}\u2026", values: {
30894
- group_list: (reactExports.createElement(reactExports.Fragment, null, push_group_list.map((push_group) => {
30895
- const group = push_group.group;
30896
- return (reactExports.createElement(Brackets, { key: group.id }, group.pr?.title || group.title || group.id));
30897
- }))),
30898
- } }));
30899
- // for all push targets in push_group_list
30900
- // things that can be done in parallel are grouped by numbers
30901
- //
30902
- // -----------------------------------
30903
- // 1 (before_push) temp mark draft
30904
- // --------------------------------------
30905
- // 2 push simultaneously to github
30906
- // --------------------------------------
30907
- // 2 create PR / edit PR
30908
- // 2 (after_push) undo temp mark draft
30909
- // --------------------------------------
30910
- const before_push_tasks = [];
30911
- for (const push_group of push_group_list) {
30912
- before_push_tasks.push(before_push(push_group));
30913
- }
30914
- await Promise.all(before_push_tasks);
30915
- const push_target_list = push_group_list.map((push_group) => {
30916
- return `HEAD~${push_group.lookback_index}:${push_group.group.id}`;
30917
- });
30918
- const push_target_args = push_target_list.join(" ");
30919
- const git_push_command = [`git push -f origin ${push_target_args}`];
30920
- if (argv.verify === false) {
30921
- git_push_command.push("--no-verify");
30922
- }
30923
- await cli(git_push_command);
30924
- const pr_url_list = commit_range.group_list.map(get_group_url);
30925
- const after_push_tasks = [];
30926
- for (const push_group of push_group_list) {
30927
- const group = push_group.group;
30928
- after_push_tasks.push(after_push({ group, pr_url_list }));
30929
- }
30930
- await Promise.all(after_push_tasks);
30931
- // finally, ensure all prs have the updated stack table from updated pr_url_list
30932
- for (let i = 0; i < commit_range.group_list.length; i++) {
30933
- const group = commit_range.group_list[i];
30934
- // use the updated pr_url_list to get the actual selected_url
30935
- const selected_url = pr_url_list[i];
30936
- invariant(group.base, "group.base must exist");
30937
- const body = group.pr?.body || DEFAULT_PR_BODY;
30938
- const update_body = write({
30939
- body,
30940
- pr_url_list,
30941
- selected_url,
30942
- });
30943
- if (update_body === body) {
30944
- actions.debug(`Skipping body update for ${selected_url}`);
30945
- }
30946
- else {
30947
- actions.debug(`Update body for ${selected_url}`);
30948
- await pr_edit({
30949
- branch: group.id,
30950
- base: group.base,
30951
- body: update_body,
30952
- });
30953
- }
30954
- }
30955
- }
30956
- async function before_push(args) {
30957
- const { group } = args;
30958
- invariant(group.base, "group.base must exist");
30959
- // we may temporarily mark PR as a draft before editing it
30960
- // if it is not already a draft PR, to avoid notification spam
30961
- let is_temp_draft = !group.pr?.isDraft;
30962
- // before pushing reset base to master temporarily
30963
- // avoid accidentally pointing to orphaned parent commit
30964
- // should hopefully fix issues where a PR includes a bunch of commits after pushing
30965
- if (group.pr) {
30966
- if (!group.pr.isDraft) {
30967
- is_temp_draft = true;
30968
- }
30969
- if (is_temp_draft) {
30970
- await pr_draft({
30971
- branch: group.id,
30972
- draft: true,
30973
- });
30974
- }
30975
- await pr_edit({
30976
- branch: group.id,
30977
- base: master_branch,
30978
- });
30979
- }
30980
- }
30981
- async function after_push(args) {
30982
- const { group, pr_url_list } = args;
30983
- invariant(group.base, "group.base must exist");
30984
- const selected_url = get_group_url(group);
30985
- if (group.pr) {
30986
- // ensure base matches pr in github
30987
- await pr_edit({
30988
- branch: group.id,
30989
- base: group.base,
30990
- body: write({
30991
- body: group.pr.body,
30992
- pr_url_list,
30993
- selected_url,
30994
- }),
30995
- });
30996
- // we may temporarily mark PR as a draft before editing it
30997
- // if it is not already a draft PR, to avoid notification spam
30998
- let is_temp_draft = !group.pr?.isDraft;
30999
- if (is_temp_draft) {
31000
- // mark pr as ready for review again
31001
- await pr_draft({
31002
- branch: group.id,
31003
- draft: false,
31004
- });
31005
- }
31006
- }
31007
- else {
31008
- // create pr in github
31009
- const pr_url = await pr_create({
31010
- branch: group.id,
31011
- base: group.base,
31012
- title: group.title,
31013
- body: DEFAULT_PR_BODY,
31014
- draft: argv.draft,
31015
- });
31016
- if (!pr_url) {
31017
- throw new Error("unable to create pr");
31018
- }
31019
- // update pr_url_list with created pr_url
31020
- for (let i = 0; i < pr_url_list.length; i++) {
31021
- const url = pr_url_list[i];
31022
- if (url === selected_url) {
31023
- pr_url_list[i] = pr_url;
31024
- }
31025
- }
31026
- }
30800
+ handle_exit(16);
31027
30801
  }
31028
30802
  // cleanup git operations if cancelled during manual rebase
31029
30803
  function restore_git() {
@@ -31045,7 +30819,7 @@ async function run$6() {
31045
30819
  }
31046
30820
  cli.sync(`pwd`, spawn_options);
31047
30821
  }
31048
- function handle_exit() {
30822
+ function handle_exit(code) {
31049
30823
  actions.output(reactExports.createElement(Text, { color: colors.yellow },
31050
30824
  "Restoring ",
31051
30825
  reactExports.createElement(Brackets, null, branch_name),
@@ -31055,10 +30829,9 @@ async function run$6() {
31055
30829
  "Restored ",
31056
30830
  reactExports.createElement(Brackets, null, branch_name),
31057
30831
  "."));
31058
- actions.exit(5);
30832
+ actions.exit(code);
31059
30833
  }
31060
30834
  }
31061
- const get_group_url = (group) => group.pr?.url || group.id;
31062
30835
 
31063
30836
  function Table(props) {
31064
30837
  if (!props.data.length) {
@@ -31237,9 +31010,9 @@ function get_status_bold(row) {
31237
31010
  }
31238
31011
 
31239
31012
  function PostRebaseStatus() {
31240
- return reactExports.createElement(Await, { fallback: null, function: run$5 });
31013
+ return reactExports.createElement(Await, { fallback: null, function: run$6 });
31241
31014
  }
31242
- async function run$5() {
31015
+ async function run$6() {
31243
31016
  const actions = Store.getState().actions;
31244
31017
  // reset github pr cache before refreshing via commit range below
31245
31018
  actions.reset_pr();
@@ -31269,9 +31042,9 @@ function PreLocalMergeRebase() {
31269
31042
  }
31270
31043
 
31271
31044
  function PreManualRebase() {
31272
- return reactExports.createElement(Await, { fallback: null, function: run$4 });
31045
+ return reactExports.createElement(Await, { fallback: null, function: run$5 });
31273
31046
  }
31274
- async function run$4() {
31047
+ async function run$5() {
31275
31048
  const state = Store.getState();
31276
31049
  const actions = state.actions;
31277
31050
  const repo_root = state.repo_root;
@@ -31773,59 +31546,435 @@ function SelectCommitRangesInternal(props) {
31773
31546
  set_group_input(false);
31774
31547
  }
31775
31548
  }
31776
- const SYMBOL = {
31777
- left: "←",
31778
- right: "→",
31779
- enter: "Enter",
31549
+ const SYMBOL = {
31550
+ left: "←",
31551
+ right: "→",
31552
+ enter: "Enter",
31553
+ };
31554
+
31555
+ function Status() {
31556
+ return reactExports.createElement(Await, { fallback: null, function: run$4 });
31557
+ }
31558
+ async function run$4() {
31559
+ const state = Store.getState();
31560
+ const actions = state.actions;
31561
+ const argv = state.argv;
31562
+ const commit_range = Store.getState().commit_range;
31563
+ invariant(commit_range, "commit_range must exist");
31564
+ actions.output(reactExports.createElement(StatusTable, null));
31565
+ let needs_rebase = false;
31566
+ let needs_update = false;
31567
+ for (const group of commit_range.group_list) {
31568
+ if (group.dirty) {
31569
+ needs_update = true;
31570
+ }
31571
+ if (group.pr?.state === "MERGED") {
31572
+ needs_rebase = true;
31573
+ }
31574
+ }
31575
+ if (argv.check) {
31576
+ actions.exit(0);
31577
+ }
31578
+ else if (needs_rebase) {
31579
+ Store.setState((state) => {
31580
+ state.step = "pre-local-merge-rebase";
31581
+ });
31582
+ }
31583
+ else if (needs_update) {
31584
+ Store.setState((state) => {
31585
+ state.step = "pre-select-commit-ranges";
31586
+ });
31587
+ }
31588
+ else if (argv.force) {
31589
+ Store.setState((state) => {
31590
+ state.step = "select-commit-ranges";
31591
+ });
31592
+ }
31593
+ else {
31594
+ actions.output(reactExports.createElement(Text, null, "\u2705 Everything up to date."));
31595
+ actions.output(reactExports.createElement(Text, { color: colors.gray },
31596
+ reactExports.createElement(Text, null, "Run with"),
31597
+ reactExports.createElement(Text, { bold: true, color: colors.yellow }, ` --force `),
31598
+ reactExports.createElement(Text, null, "to force update all pull requests.")));
31599
+ actions.exit(0);
31600
+ }
31601
+ }
31602
+
31603
+ /**
31604
+ * Gets the last element of `array`.
31605
+ *
31606
+ * @static
31607
+ * @memberOf _
31608
+ * @since 0.1.0
31609
+ * @category Array
31610
+ * @param {Array} array The array to query.
31611
+ * @returns {*} Returns the last element of `array`.
31612
+ * @example
31613
+ *
31614
+ * _.last([1, 2, 3]);
31615
+ * // => 3
31616
+ */
31617
+
31618
+ function last(array) {
31619
+ var length = array == null ? 0 : array.length;
31620
+ return length ? array[length - 1] : undefined;
31621
+ }
31622
+
31623
+ var last_1 = last;
31624
+
31625
+ var last$1 = /*@__PURE__*/getDefaultExportFromCjs(last_1);
31626
+
31627
+ function write(args) {
31628
+ const stack_table = table(args);
31629
+ let result = args.body;
31630
+ if (RE.stack_table_link.test(result)) {
31631
+ // replace stack table
31632
+ result = result.replace(RE.stack_table_link, stack_table);
31633
+ }
31634
+ else if (RE.stack_table_legacy.test(result)) {
31635
+ // replace stack table
31636
+ result = result.replace(RE.stack_table_legacy, stack_table);
31637
+ }
31638
+ else {
31639
+ // append stack table
31640
+ result = `${result}\n\n${stack_table}`;
31641
+ }
31642
+ result = result.trimEnd();
31643
+ return result;
31644
+ }
31645
+ function table(args) {
31646
+ const stack_pr_url_list = [...args.pr_url_list];
31647
+ const old_stack = parse(args.body);
31648
+ // remove existing stack pr urls from the old stack pr urls
31649
+ for (const pr_url of stack_pr_url_list) {
31650
+ old_stack.delete(pr_url);
31651
+ }
31652
+ // add remaining old stack pr urls to the front of stack pr url list
31653
+ const old_pr_list = Array.from(old_stack.keys());
31654
+ old_pr_list.reverse();
31655
+ for (const pr_url of old_pr_list) {
31656
+ stack_pr_url_list.unshift(pr_url);
31657
+ }
31658
+ const stack_list = [];
31659
+ const num_digits = String(stack_pr_url_list.length).length;
31660
+ for (let i = 0; i < stack_pr_url_list.length; i++) {
31661
+ const pr_url = stack_pr_url_list[i];
31662
+ const selected = args.selected_url === pr_url;
31663
+ let icon;
31664
+ if (old_stack.has(pr_url)) {
31665
+ icon = "✅";
31666
+ }
31667
+ else if (selected) {
31668
+ icon = "👉";
31669
+ }
31670
+ else {
31671
+ icon = "⏳";
31672
+ }
31673
+ const num = String(i + 1).padStart(num_digits, "0");
31674
+ stack_list.push(TEMPLATE.row({ icon, num, pr_url }));
31675
+ }
31676
+ if (!stack_list.length) {
31677
+ return "";
31678
+ }
31679
+ return TEMPLATE.stack_table_link(["", ...stack_list, "", ""].join("\n"));
31680
+ }
31681
+ function parse(body) {
31682
+ let stack_table_match = body.match(RE.stack_table_link);
31683
+ if (!stack_table_match?.groups) {
31684
+ stack_table_match = body.match(RE.stack_table_legacy);
31685
+ }
31686
+ if (!stack_table_match?.groups) {
31687
+ return new Map();
31688
+ }
31689
+ const rows_string = stack_table_match.groups["rows"];
31690
+ const row_list = rows_string.split("\n");
31691
+ const result = new Map();
31692
+ for (const row of row_list) {
31693
+ const row_match = row.match(RE.row);
31694
+ const parsed_row = row_match?.groups;
31695
+ if (!parsed_row) {
31696
+ // skip invalid row
31697
+ continue;
31698
+ }
31699
+ if (!RE.pr_url.test(parsed_row.pr_url)) {
31700
+ continue;
31701
+ }
31702
+ result.set(parsed_row.pr_url, parsed_row);
31703
+ }
31704
+ return result;
31705
+ }
31706
+ const TEMPLATE = {
31707
+ stack_table_legacy(rows) {
31708
+ return `#### git stack${rows}`;
31709
+ },
31710
+ stack_table_link(rows) {
31711
+ return `#### [git stack](https://github.com/magus/git-stack-cli)${rows}`;
31712
+ },
31713
+ row(args) {
31714
+ return `- ${args.icon} \`${args.num}\` ${args.pr_url}`;
31715
+ },
31716
+ };
31717
+ const RE = {
31718
+ // https://regex101.com/r/kqB9Ft/1
31719
+ stack_table_legacy: new RegExp(TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
31720
+ stack_table_link: new RegExp(TEMPLATE.stack_table_link("ROWS")
31721
+ .replace("[", "\\[")
31722
+ .replace("]", "\\]")
31723
+ .replace("(", "\\(")
31724
+ .replace(")", "\\)")
31725
+ .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
31726
+ row: new RegExp(TEMPLATE.row({
31727
+ icon: "(?<icon>.+)",
31728
+ num: "(?<num>\\d+)",
31729
+ pr_url: "(?<pr_url>.+)",
31730
+ })),
31731
+ pr_url: /^https:\/\/.*$/,
31780
31732
  };
31781
31733
 
31782
- function Status() {
31783
- return reactExports.createElement(Await, { fallback: null, function: run$3 });
31734
+ function SyncGithub() {
31735
+ const abort_handler = reactExports.useRef(() => { });
31736
+ reactExports.useEffect(function listen_sigint() {
31737
+ process.once("SIGINT", sigint_handler);
31738
+ return function cleanup() {
31739
+ process.removeListener("SIGINT", sigint_handler);
31740
+ };
31741
+ function sigint_handler() {
31742
+ abort_handler.current();
31743
+ }
31744
+ }, []);
31745
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Syncing\u2026"), function: async function () {
31746
+ await run$3({ abort_handler });
31747
+ } }));
31784
31748
  }
31785
- async function run$3() {
31749
+ async function run$3(args) {
31786
31750
  const state = Store.getState();
31787
31751
  const actions = state.actions;
31788
31752
  const argv = state.argv;
31789
- const commit_range = Store.getState().commit_range;
31790
- invariant(commit_range, "commit_range must exist");
31791
- actions.output(reactExports.createElement(StatusTable, null));
31792
- let needs_rebase = false;
31793
- let needs_update = false;
31794
- for (const group of commit_range.group_list) {
31795
- if (group.dirty) {
31796
- needs_update = true;
31753
+ const branch_name = state.branch_name;
31754
+ const commit_map = state.commit_map;
31755
+ const master_branch = state.master_branch;
31756
+ const repo_root = state.repo_root;
31757
+ const sync_github = state.sync_github;
31758
+ invariant(branch_name, "branch_name must exist");
31759
+ invariant(commit_map, "commit_map must exist");
31760
+ invariant(repo_root, "repo_root must exist");
31761
+ invariant(sync_github, "sync_github must exist");
31762
+ const commit_range = sync_github.commit_range;
31763
+ const rebase_group_index = sync_github.rebase_group_index;
31764
+ // always listen for SIGINT event and restore pr state
31765
+ args.abort_handler.current = function sigint_handler() {
31766
+ actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
31767
+ handle_exit(17);
31768
+ };
31769
+ let DEFAULT_PR_BODY = "";
31770
+ if (state.pr_template_body) {
31771
+ DEFAULT_PR_BODY = state.pr_template_body;
31772
+ }
31773
+ const push_group_list = get_push_group_list();
31774
+ // for all push targets in push_group_list
31775
+ // things that can be done in parallel are grouped by numbers
31776
+ //
31777
+ // -----------------------------------
31778
+ // 1 (before_push) temp mark draft
31779
+ // --------------------------------------
31780
+ // 2 push simultaneously to github
31781
+ // --------------------------------------
31782
+ // 2 create PR / edit PR
31783
+ // 2 (after_push) undo temp mark draft
31784
+ // --------------------------------------
31785
+ try {
31786
+ const before_push_tasks = [];
31787
+ for (const group of push_group_list) {
31788
+ before_push_tasks.push(before_push({ group }));
31797
31789
  }
31798
- if (group.pr?.state === "MERGED") {
31799
- needs_rebase = true;
31790
+ await Promise.all(before_push_tasks);
31791
+ const git_push_command = [`git push -f origin`];
31792
+ if (argv.verify === false) {
31793
+ git_push_command.push("--no-verify");
31794
+ }
31795
+ for (const group of push_group_list) {
31796
+ const last_commit = last$1(group.commits);
31797
+ invariant(last_commit, "last_commit must exist");
31798
+ // explicit refs/heads head branch to avoid push failing
31799
+ //
31800
+ // ❯ git push -f origin --no-verify f6e249051b4820a03deb957ddebc19acfd7dfd7c:gs-ED2etrzv2
31801
+ // error: The destination you provided is not a full refname (i.e.,
31802
+ // starting with "refs/"). We tried to guess what you meant by:
31803
+ //
31804
+ // - Looking for a ref that matches 'gs-ED2etrzv2' on the remote side.
31805
+ // - Checking if the <src> being pushed ('f6e249051b4820a03deb957ddebc19acfd7dfd7c')
31806
+ // is a ref in "refs/{heads,tags}/". If so we add a corresponding
31807
+ // refs/{heads,tags}/ prefix on the remote side.
31808
+ //
31809
+ // Neither worked, so we gave up. You must fully qualify the ref.
31810
+ // hint: The <src> part of the refspec is a commit object.
31811
+ // hint: Did you mean to create a new branch by pushing to
31812
+ // hint: 'f6e249051b4820a03deb957ddebc19acfd7dfd7c:refs/heads/gs-ED2etrzv2'?
31813
+ // error: failed to push some refs to 'github.com:magus/git-multi-diff-playground.git'
31814
+ //
31815
+ const target = `${last_commit.sha}:refs/heads/${group.id}`;
31816
+ git_push_command.push(target);
31817
+ }
31818
+ await cli(git_push_command);
31819
+ const pr_url_list = commit_range.group_list.map(get_group_url);
31820
+ const after_push_tasks = [];
31821
+ for (const group of push_group_list) {
31822
+ after_push_tasks.push(after_push({ group, pr_url_list }));
31823
+ }
31824
+ await Promise.all(after_push_tasks);
31825
+ // finally, ensure all prs have the updated stack table from updated pr_url_list
31826
+ // this step must come after the after_push since that step may create new PRs
31827
+ // we need the urls for all prs at this step so we run it after the after_push
31828
+ const update_pr_body_tasks = [];
31829
+ for (let i = 0; i < commit_range.group_list.length; i++) {
31830
+ const group = commit_range.group_list[i];
31831
+ // use the updated pr_url_list to get the actual selected_url
31832
+ const selected_url = pr_url_list[i];
31833
+ const task = update_pr_body({ group, selected_url, pr_url_list });
31834
+ update_pr_body_tasks.push(task);
31835
+ }
31836
+ await Promise.all(update_pr_body_tasks);
31837
+ actions.set((state) => {
31838
+ state.step = "post-rebase-status";
31839
+ });
31840
+ }
31841
+ catch (err) {
31842
+ if (err instanceof Error) {
31843
+ actions.error(err.message);
31844
+ }
31845
+ actions.error("Unable to sync.");
31846
+ if (!argv.verbose) {
31847
+ actions.error("Try again with `--verbose` to see more information.");
31800
31848
  }
31849
+ await handle_exit(18);
31801
31850
  }
31802
- if (argv.check) {
31803
- actions.exit(0);
31851
+ function get_push_group_list() {
31852
+ // start from HEAD and work backward to rebase_group_index
31853
+ const push_group_list = [];
31854
+ for (let i = 0; i < commit_range.group_list.length; i++) {
31855
+ const index = commit_range.group_list.length - 1 - i;
31856
+ // do not go past rebase_group_index
31857
+ if (index < rebase_group_index) {
31858
+ break;
31859
+ }
31860
+ const group = commit_range.group_list[index];
31861
+ push_group_list.unshift(group);
31862
+ }
31863
+ return push_group_list;
31804
31864
  }
31805
- else if (needs_rebase) {
31806
- Store.setState((state) => {
31807
- state.step = "pre-local-merge-rebase";
31808
- });
31865
+ async function before_push(args) {
31866
+ const { group } = args;
31867
+ invariant(group.base, "group.base must exist");
31868
+ // we may temporarily mark PR as a draft before editing it
31869
+ // if it is not already a draft PR, to avoid notification spam
31870
+ let is_temp_draft = !group.pr?.isDraft;
31871
+ // before pushing reset base to master temporarily
31872
+ // avoid accidentally pointing to orphaned parent commit
31873
+ // should hopefully fix issues where a PR includes a bunch of commits after pushing
31874
+ if (group.pr) {
31875
+ if (!group.pr.isDraft) {
31876
+ is_temp_draft = true;
31877
+ }
31878
+ if (is_temp_draft) {
31879
+ await pr_draft({
31880
+ branch: group.id,
31881
+ draft: true,
31882
+ });
31883
+ }
31884
+ await pr_edit({
31885
+ branch: group.id,
31886
+ base: master_branch,
31887
+ });
31888
+ }
31809
31889
  }
31810
- else if (needs_update) {
31811
- Store.setState((state) => {
31812
- state.step = "pre-select-commit-ranges";
31813
- });
31890
+ async function after_push(args) {
31891
+ const { group, pr_url_list } = args;
31892
+ invariant(group.base, "group.base must exist");
31893
+ const selected_url = get_group_url(group);
31894
+ if (group.pr) {
31895
+ // ensure base matches pr in github
31896
+ await pr_edit({
31897
+ branch: group.id,
31898
+ base: group.base,
31899
+ body: write({
31900
+ body: group.pr.body,
31901
+ pr_url_list,
31902
+ selected_url,
31903
+ }),
31904
+ });
31905
+ // we may temporarily mark PR as a draft before editing it
31906
+ // if it is not already a draft PR, to avoid notification spam
31907
+ let is_temp_draft = !group.pr?.isDraft;
31908
+ if (is_temp_draft) {
31909
+ // mark pr as ready for review again
31910
+ await pr_draft({
31911
+ branch: group.id,
31912
+ draft: false,
31913
+ });
31914
+ }
31915
+ }
31916
+ else {
31917
+ // create pr in github
31918
+ const pr_url = await pr_create({
31919
+ branch: group.id,
31920
+ base: group.base,
31921
+ title: group.title,
31922
+ body: DEFAULT_PR_BODY,
31923
+ draft: argv.draft,
31924
+ });
31925
+ if (!pr_url) {
31926
+ throw new Error("unable to create pr");
31927
+ }
31928
+ // update pr_url_list with created pr_url
31929
+ for (let i = 0; i < pr_url_list.length; i++) {
31930
+ const url = pr_url_list[i];
31931
+ if (url === selected_url) {
31932
+ pr_url_list[i] = pr_url;
31933
+ }
31934
+ }
31935
+ }
31814
31936
  }
31815
- else if (argv.force) {
31816
- Store.setState((state) => {
31817
- state.step = "select-commit-ranges";
31937
+ async function update_pr_body(args) {
31938
+ const { group, selected_url, pr_url_list } = args;
31939
+ invariant(group.base, "group.base must exist");
31940
+ const body = group.pr?.body || DEFAULT_PR_BODY;
31941
+ const update_body = write({
31942
+ body,
31943
+ pr_url_list,
31944
+ selected_url,
31818
31945
  });
31946
+ if (update_body === body) {
31947
+ actions.debug(`Skipping body update for ${selected_url}`);
31948
+ }
31949
+ else {
31950
+ actions.debug(`Update body for ${selected_url}`);
31951
+ await pr_edit({
31952
+ branch: group.id,
31953
+ base: group.base,
31954
+ body: update_body,
31955
+ });
31956
+ }
31819
31957
  }
31820
- else {
31821
- actions.output(reactExports.createElement(Text, null, "\u2705 Everything up to date."));
31822
- actions.output(reactExports.createElement(Text, { color: colors.gray },
31823
- reactExports.createElement(Text, null, "Run with"),
31824
- reactExports.createElement(Text, { bold: true, color: colors.yellow }, ` --force `),
31825
- reactExports.createElement(Text, null, "to force update all pull requests.")));
31826
- actions.exit(0);
31958
+ function handle_exit(code) {
31959
+ actions.output(reactExports.createElement(Text, { color: colors.yellow }, "Restoring PR state\u2026"));
31960
+ for (const group of push_group_list) {
31961
+ // we may temporarily mark PR as a draft before editing it
31962
+ // if it is not already a draft PR, to avoid notification spam
31963
+ let is_temp_draft = !group.pr?.isDraft;
31964
+ // restore PR to non-draft state
31965
+ if (is_temp_draft) {
31966
+ pr_draft({
31967
+ branch: group.id,
31968
+ draft: false,
31969
+ })
31970
+ .catch(actions.error);
31971
+ }
31972
+ }
31973
+ actions.output(reactExports.createElement(Text, { color: colors.yellow }, "Restored PR state."));
31974
+ actions.exit(code);
31827
31975
  }
31828
31976
  }
31977
+ const get_group_url = (group) => group.pr?.url || group.id;
31829
31978
 
31830
31979
  function Main() {
31831
31980
  const step = Store.useState((state) => state.step);
@@ -31848,6 +31997,8 @@ function Main() {
31848
31997
  return reactExports.createElement(PreManualRebase, null);
31849
31998
  case "manual-rebase":
31850
31999
  return reactExports.createElement(ManualRebase, null);
32000
+ case "sync-github":
32001
+ return reactExports.createElement(SyncGithub, null);
31851
32002
  case "post-rebase-status":
31852
32003
  return reactExports.createElement(PostRebaseStatus, null);
31853
32004
  default:
@@ -37444,7 +37595,7 @@ async function command() {
37444
37595
  .wrap(123)
37445
37596
  // disallow unknown options
37446
37597
  .strict()
37447
- .version("1.13.0" )
37598
+ .version("1.13.2" )
37448
37599
  .showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
37449
37600
  .help("help", "Show usage via `git stack help`")
37450
37601
  .argv;
@@ -37529,7 +37680,10 @@ const FixupOptions = {
37529
37680
 
37530
37681
  command()
37531
37682
  .then((argv) => {
37532
- const ink = render(reactExports.createElement(App, null));
37683
+ const ink = render(reactExports.createElement(App, null), {
37684
+ // If true, each update will be rendered as a separate output, without replacing the previous one.
37685
+ // debug: true,
37686
+ });
37533
37687
  Store.setState((state) => {
37534
37688
  state.ink = ink;
37535
37689
  state.process_argv = process.argv;