git-stack-cli 1.13.0 → 1.13.1

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: {},
@@ -30322,9 +30323,9 @@ function DirtyCheck(props) {
30322
30323
 
30323
30324
  function GatherMetadata(props) {
30324
30325
  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));
30326
+ return (reactExports.createElement(Await, { fallback: fallback, function: run$a }, props.children));
30326
30327
  }
30327
- async function run$9() {
30328
+ async function run$a() {
30328
30329
  const actions = Store.getState().actions;
30329
30330
  const argv = Store.getState().argv;
30330
30331
  try {
@@ -30418,9 +30419,9 @@ function format_time(date) {
30418
30419
  }
30419
30420
 
30420
30421
  function GithubApiError() {
30421
- return reactExports.createElement(Await, { fallback: null, function: run$8 });
30422
+ return reactExports.createElement(Await, { fallback: null, function: run$9 });
30422
30423
  }
30423
- async function run$8() {
30424
+ async function run$9() {
30424
30425
  const actions = Store.getState().actions;
30425
30426
  const res = await cli(`gh api https://api.github.com/rate_limit`);
30426
30427
  const res_json = JSON.parse(res.stdout);
@@ -30462,7 +30463,7 @@ function LocalCommitStatus(props) {
30462
30463
  if (argv["mock-metadata"]) {
30463
30464
  return (reactExports.createElement(Await, { fallback: fallback, function: mock_metadata }, props.children));
30464
30465
  }
30465
- return (reactExports.createElement(Await, { fallback: fallback, function: run$7 }, props.children));
30466
+ return (reactExports.createElement(Await, { fallback: fallback, function: run$8 }, props.children));
30466
30467
  }
30467
30468
  async function mock_metadata() {
30468
30469
  const module = await Promise.resolve().then(function () { return metadata; });
@@ -30472,7 +30473,7 @@ async function mock_metadata() {
30472
30473
  state.step = "status";
30473
30474
  });
30474
30475
  }
30475
- async function run$7() {
30476
+ async function run$8() {
30476
30477
  const actions = Store.getState().actions;
30477
30478
  try {
30478
30479
  const commit_range = await range();
@@ -30552,9 +30553,21 @@ function encode(value) {
30552
30553
  }
30553
30554
 
30554
30555
  function Rebase() {
30555
- return (reactExports.createElement(Await, { function: Rebase.run, fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026") }));
30556
+ const abort_handler = reactExports.useRef(() => { });
30557
+ reactExports.useEffect(function listen_sigint() {
30558
+ process.once("SIGINT", sigint_handler);
30559
+ return function cleanup() {
30560
+ process.removeListener("SIGINT", sigint_handler);
30561
+ };
30562
+ function sigint_handler() {
30563
+ abort_handler.current();
30564
+ }
30565
+ }, []);
30566
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: async function () {
30567
+ await Rebase.run({ abort_handler });
30568
+ } }));
30556
30569
  }
30557
- Rebase.run = async function run() {
30570
+ Rebase.run = async function run(args) {
30558
30571
  const state = Store.getState();
30559
30572
  const actions = state.actions;
30560
30573
  const branch_name = state.branch_name;
@@ -30566,7 +30579,10 @@ Rebase.run = async function run() {
30566
30579
  invariant(commit_range, "commit_range must exist");
30567
30580
  invariant(repo_root, "repo_root must exist");
30568
30581
  // always listen for SIGINT event and restore git state
30569
- process.once("SIGINT", handle_exit);
30582
+ args.abort_handler.current = async function sigint_handler() {
30583
+ actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
30584
+ handle_exit(19);
30585
+ };
30570
30586
  const temp_branch_name = `${branch_name}_${short_id()}`;
30571
30587
  try {
30572
30588
  // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
@@ -30629,7 +30645,7 @@ Rebase.run = async function run() {
30629
30645
  actions.error(err.message);
30630
30646
  }
30631
30647
  }
30632
- handle_exit();
30648
+ handle_exit(20);
30633
30649
  }
30634
30650
  // cleanup git operations if cancelled during manual rebase
30635
30651
  function restore_git() {
@@ -30651,7 +30667,7 @@ Rebase.run = async function run() {
30651
30667
  }
30652
30668
  cli.sync(`pwd`, spawn_options);
30653
30669
  }
30654
- function handle_exit() {
30670
+ function handle_exit(code) {
30655
30671
  actions.output(reactExports.createElement(Text, { color: colors.yellow },
30656
30672
  "Restoring ",
30657
30673
  reactExports.createElement(Brackets, null, branch_name),
@@ -30661,7 +30677,7 @@ Rebase.run = async function run() {
30661
30677
  "Restored ",
30662
30678
  reactExports.createElement(Brackets, null, branch_name),
30663
30679
  "."));
30664
- actions.exit(6);
30680
+ actions.exit(code);
30665
30681
  }
30666
30682
  };
30667
30683
 
@@ -30669,117 +30685,22 @@ function LocalMergeRebase() {
30669
30685
  return reactExports.createElement(Rebase, null);
30670
30686
  }
30671
30687
 
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
30688
  function ManualRebase() {
30780
- return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: run$6 }));
30689
+ const abort_handler = reactExports.useRef(() => { });
30690
+ reactExports.useEffect(function listen_sigint() {
30691
+ process.once("SIGINT", sigint_handler);
30692
+ return function cleanup() {
30693
+ process.removeListener("SIGINT", sigint_handler);
30694
+ };
30695
+ async function sigint_handler() {
30696
+ abort_handler.current();
30697
+ }
30698
+ }, []);
30699
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Rebasing commits\u2026"), function: async function () {
30700
+ await run$7({ abort_handler });
30701
+ } }));
30781
30702
  }
30782
- async function run$6() {
30703
+ async function run$7(args) {
30783
30704
  const state = Store.getState();
30784
30705
  const actions = state.actions;
30785
30706
  const argv = state.argv;
@@ -30792,54 +30713,53 @@ async function run$6() {
30792
30713
  invariant(commit_map, "commit_map must exist");
30793
30714
  invariant(repo_root, "repo_root must exist");
30794
30715
  // 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
- }
30716
+ args.abort_handler.current = function sigint_handler() {
30717
+ actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
30718
+ handle_exit(15);
30719
+ };
30816
30720
  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;
30721
+ try {
30722
+ // get latest merge_base relative to local master
30723
+ const merge_base = (await cli(`git merge-base HEAD ${master_branch}`))
30724
+ .stdout;
30725
+ // immediately paint all commit to preserve selected commit ranges
30726
+ let commit_range = await range(commit_map);
30727
+ // reverse group list to ensure we create git revise in correct order
30728
+ commit_range.group_list.reverse();
30729
+ for (const commit of commit_range.commit_list) {
30730
+ const group_from_map = commit_map[commit.sha];
30731
+ commit.branch_id = group_from_map.id;
30732
+ commit.title = group_from_map.title;
30826
30733
  }
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;
30734
+ await GitReviseTodo.execute({
30735
+ rebase_group_index: 0,
30736
+ rebase_merge_base: merge_base,
30737
+ commit_range,
30738
+ });
30739
+ commit_range = await range(commit_map);
30740
+ // reverse commit list so that we can cherry-pick in order
30741
+ commit_range.group_list.reverse();
30742
+ let rebase_merge_base = merge_base;
30743
+ let rebase_group_index = 0;
30744
+ for (let i = 0; i < commit_range.group_list.length; i++) {
30745
+ const group = commit_range.group_list[i];
30746
+ if (!group.dirty) {
30747
+ continue;
30748
+ }
30749
+ if (i > 0) {
30750
+ const prev_group = commit_range.group_list[i - 1];
30751
+ const prev_commit = prev_group.commits[prev_group.commits.length - 1];
30752
+ rebase_merge_base = prev_commit.sha;
30753
+ rebase_group_index = i;
30754
+ }
30755
+ break;
30832
30756
  }
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 {
30757
+ actions.debug(`rebase_merge_base = ${rebase_merge_base}`);
30758
+ actions.debug(`rebase_group_index = ${rebase_group_index}`);
30759
+ // actions.debug(`commit_range=${JSON.stringify(commit_range, null, 2)}`);
30839
30760
  // must perform rebase from repo root for applying git patch
30840
30761
  process.chdir(repo_root);
30841
30762
  await cli(`pwd`);
30842
- actions.output(reactExports.createElement(Text, { color: colors.yellow, wrap: "truncate-end" }, "Rebasing\u2026"));
30843
30763
  // create temporary branch
30844
30764
  await cli(`git checkout -b ${temp_branch_name}`);
30845
30765
  await GitReviseTodo.execute({
@@ -30850,13 +30770,18 @@ async function run$6() {
30850
30770
  // after all commits have been modified move the pointer
30851
30771
  // of original branch to the newly created temporary branch
30852
30772
  await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
30773
+ restore_git();
30853
30774
  if (argv.sync) {
30854
- await sync_github();
30775
+ actions.set((state) => {
30776
+ state.step = "sync-github";
30777
+ state.sync_github = { commit_range, rebase_group_index };
30778
+ });
30779
+ }
30780
+ else {
30781
+ actions.set((state) => {
30782
+ state.step = "post-rebase-status";
30783
+ });
30855
30784
  }
30856
- restore_git();
30857
- actions.set((state) => {
30858
- state.step = "post-rebase-status";
30859
- });
30860
30785
  }
30861
30786
  catch (err) {
30862
30787
  if (err instanceof Error) {
@@ -30866,164 +30791,7 @@ async function run$6() {
30866
30791
  if (!argv.verbose) {
30867
30792
  actions.error("Try again with `--verbose` to see more information.");
30868
30793
  }
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
- }
30794
+ handle_exit(16);
31027
30795
  }
31028
30796
  // cleanup git operations if cancelled during manual rebase
31029
30797
  function restore_git() {
@@ -31045,7 +30813,7 @@ async function run$6() {
31045
30813
  }
31046
30814
  cli.sync(`pwd`, spawn_options);
31047
30815
  }
31048
- function handle_exit() {
30816
+ function handle_exit(code) {
31049
30817
  actions.output(reactExports.createElement(Text, { color: colors.yellow },
31050
30818
  "Restoring ",
31051
30819
  reactExports.createElement(Brackets, null, branch_name),
@@ -31055,10 +30823,9 @@ async function run$6() {
31055
30823
  "Restored ",
31056
30824
  reactExports.createElement(Brackets, null, branch_name),
31057
30825
  "."));
31058
- actions.exit(5);
30826
+ actions.exit(code);
31059
30827
  }
31060
30828
  }
31061
- const get_group_url = (group) => group.pr?.url || group.id;
31062
30829
 
31063
30830
  function Table(props) {
31064
30831
  if (!props.data.length) {
@@ -31237,9 +31004,9 @@ function get_status_bold(row) {
31237
31004
  }
31238
31005
 
31239
31006
  function PostRebaseStatus() {
31240
- return reactExports.createElement(Await, { fallback: null, function: run$5 });
31007
+ return reactExports.createElement(Await, { fallback: null, function: run$6 });
31241
31008
  }
31242
- async function run$5() {
31009
+ async function run$6() {
31243
31010
  const actions = Store.getState().actions;
31244
31011
  // reset github pr cache before refreshing via commit range below
31245
31012
  actions.reset_pr();
@@ -31269,9 +31036,9 @@ function PreLocalMergeRebase() {
31269
31036
  }
31270
31037
 
31271
31038
  function PreManualRebase() {
31272
- return reactExports.createElement(Await, { fallback: null, function: run$4 });
31039
+ return reactExports.createElement(Await, { fallback: null, function: run$5 });
31273
31040
  }
31274
- async function run$4() {
31041
+ async function run$5() {
31275
31042
  const state = Store.getState();
31276
31043
  const actions = state.actions;
31277
31044
  const repo_root = state.repo_root;
@@ -31773,59 +31540,419 @@ function SelectCommitRangesInternal(props) {
31773
31540
  set_group_input(false);
31774
31541
  }
31775
31542
  }
31776
- const SYMBOL = {
31777
- left: "←",
31778
- right: "→",
31779
- enter: "Enter",
31543
+ const SYMBOL = {
31544
+ left: "←",
31545
+ right: "→",
31546
+ enter: "Enter",
31547
+ };
31548
+
31549
+ function Status() {
31550
+ return reactExports.createElement(Await, { fallback: null, function: run$4 });
31551
+ }
31552
+ async function run$4() {
31553
+ const state = Store.getState();
31554
+ const actions = state.actions;
31555
+ const argv = state.argv;
31556
+ const commit_range = Store.getState().commit_range;
31557
+ invariant(commit_range, "commit_range must exist");
31558
+ actions.output(reactExports.createElement(StatusTable, null));
31559
+ let needs_rebase = false;
31560
+ let needs_update = false;
31561
+ for (const group of commit_range.group_list) {
31562
+ if (group.dirty) {
31563
+ needs_update = true;
31564
+ }
31565
+ if (group.pr?.state === "MERGED") {
31566
+ needs_rebase = true;
31567
+ }
31568
+ }
31569
+ if (argv.check) {
31570
+ actions.exit(0);
31571
+ }
31572
+ else if (needs_rebase) {
31573
+ Store.setState((state) => {
31574
+ state.step = "pre-local-merge-rebase";
31575
+ });
31576
+ }
31577
+ else if (needs_update) {
31578
+ Store.setState((state) => {
31579
+ state.step = "pre-select-commit-ranges";
31580
+ });
31581
+ }
31582
+ else if (argv.force) {
31583
+ Store.setState((state) => {
31584
+ state.step = "select-commit-ranges";
31585
+ });
31586
+ }
31587
+ else {
31588
+ actions.output(reactExports.createElement(Text, null, "\u2705 Everything up to date."));
31589
+ actions.output(reactExports.createElement(Text, { color: colors.gray },
31590
+ reactExports.createElement(Text, null, "Run with"),
31591
+ reactExports.createElement(Text, { bold: true, color: colors.yellow }, ` --force `),
31592
+ reactExports.createElement(Text, null, "to force update all pull requests.")));
31593
+ actions.exit(0);
31594
+ }
31595
+ }
31596
+
31597
+ /**
31598
+ * Gets the last element of `array`.
31599
+ *
31600
+ * @static
31601
+ * @memberOf _
31602
+ * @since 0.1.0
31603
+ * @category Array
31604
+ * @param {Array} array The array to query.
31605
+ * @returns {*} Returns the last element of `array`.
31606
+ * @example
31607
+ *
31608
+ * _.last([1, 2, 3]);
31609
+ * // => 3
31610
+ */
31611
+
31612
+ function last(array) {
31613
+ var length = array == null ? 0 : array.length;
31614
+ return length ? array[length - 1] : undefined;
31615
+ }
31616
+
31617
+ var last_1 = last;
31618
+
31619
+ var last$1 = /*@__PURE__*/getDefaultExportFromCjs(last_1);
31620
+
31621
+ function write(args) {
31622
+ const stack_table = table(args);
31623
+ let result = args.body;
31624
+ if (RE.stack_table_link.test(result)) {
31625
+ // replace stack table
31626
+ result = result.replace(RE.stack_table_link, stack_table);
31627
+ }
31628
+ else if (RE.stack_table_legacy.test(result)) {
31629
+ // replace stack table
31630
+ result = result.replace(RE.stack_table_legacy, stack_table);
31631
+ }
31632
+ else {
31633
+ // append stack table
31634
+ result = `${result}\n\n${stack_table}`;
31635
+ }
31636
+ result = result.trimEnd();
31637
+ return result;
31638
+ }
31639
+ function table(args) {
31640
+ const stack_pr_url_list = [...args.pr_url_list];
31641
+ const old_stack = parse(args.body);
31642
+ // remove existing stack pr urls from the old stack pr urls
31643
+ for (const pr_url of stack_pr_url_list) {
31644
+ old_stack.delete(pr_url);
31645
+ }
31646
+ // add remaining old stack pr urls to the front of stack pr url list
31647
+ const old_pr_list = Array.from(old_stack.keys());
31648
+ old_pr_list.reverse();
31649
+ for (const pr_url of old_pr_list) {
31650
+ stack_pr_url_list.unshift(pr_url);
31651
+ }
31652
+ const stack_list = [];
31653
+ const num_digits = String(stack_pr_url_list.length).length;
31654
+ for (let i = 0; i < stack_pr_url_list.length; i++) {
31655
+ const pr_url = stack_pr_url_list[i];
31656
+ const selected = args.selected_url === pr_url;
31657
+ let icon;
31658
+ if (old_stack.has(pr_url)) {
31659
+ icon = "✅";
31660
+ }
31661
+ else if (selected) {
31662
+ icon = "👉";
31663
+ }
31664
+ else {
31665
+ icon = "⏳";
31666
+ }
31667
+ const num = String(i + 1).padStart(num_digits, "0");
31668
+ stack_list.push(TEMPLATE.row({ icon, num, pr_url }));
31669
+ }
31670
+ if (!stack_list.length) {
31671
+ return "";
31672
+ }
31673
+ return TEMPLATE.stack_table_link(["", ...stack_list, "", ""].join("\n"));
31674
+ }
31675
+ function parse(body) {
31676
+ let stack_table_match = body.match(RE.stack_table_link);
31677
+ if (!stack_table_match?.groups) {
31678
+ stack_table_match = body.match(RE.stack_table_legacy);
31679
+ }
31680
+ if (!stack_table_match?.groups) {
31681
+ return new Map();
31682
+ }
31683
+ const rows_string = stack_table_match.groups["rows"];
31684
+ const row_list = rows_string.split("\n");
31685
+ const result = new Map();
31686
+ for (const row of row_list) {
31687
+ const row_match = row.match(RE.row);
31688
+ const parsed_row = row_match?.groups;
31689
+ if (!parsed_row) {
31690
+ // skip invalid row
31691
+ continue;
31692
+ }
31693
+ if (!RE.pr_url.test(parsed_row.pr_url)) {
31694
+ continue;
31695
+ }
31696
+ result.set(parsed_row.pr_url, parsed_row);
31697
+ }
31698
+ return result;
31699
+ }
31700
+ const TEMPLATE = {
31701
+ stack_table_legacy(rows) {
31702
+ return `#### git stack${rows}`;
31703
+ },
31704
+ stack_table_link(rows) {
31705
+ return `#### [git stack](https://github.com/magus/git-stack-cli)${rows}`;
31706
+ },
31707
+ row(args) {
31708
+ return `- ${args.icon} \`${args.num}\` ${args.pr_url}`;
31709
+ },
31710
+ };
31711
+ const RE = {
31712
+ // https://regex101.com/r/kqB9Ft/1
31713
+ stack_table_legacy: new RegExp(TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
31714
+ stack_table_link: new RegExp(TEMPLATE.stack_table_link("ROWS")
31715
+ .replace("[", "\\[")
31716
+ .replace("]", "\\]")
31717
+ .replace("(", "\\(")
31718
+ .replace(")", "\\)")
31719
+ .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
31720
+ row: new RegExp(TEMPLATE.row({
31721
+ icon: "(?<icon>.+)",
31722
+ num: "(?<num>\\d+)",
31723
+ pr_url: "(?<pr_url>.+)",
31724
+ })),
31725
+ pr_url: /^https:\/\/.*$/,
31780
31726
  };
31781
31727
 
31782
- function Status() {
31783
- return reactExports.createElement(Await, { fallback: null, function: run$3 });
31728
+ function SyncGithub() {
31729
+ const abort_handler = reactExports.useRef(() => { });
31730
+ reactExports.useEffect(function listen_sigint() {
31731
+ process.once("SIGINT", sigint_handler);
31732
+ return function cleanup() {
31733
+ process.removeListener("SIGINT", sigint_handler);
31734
+ };
31735
+ function sigint_handler() {
31736
+ abort_handler.current();
31737
+ }
31738
+ }, []);
31739
+ return (reactExports.createElement(Await, { fallback: reactExports.createElement(Text, { color: colors.yellow }, "Syncing\u2026"), function: async function () {
31740
+ await run$3({ abort_handler });
31741
+ } }));
31784
31742
  }
31785
- async function run$3() {
31743
+ async function run$3(args) {
31786
31744
  const state = Store.getState();
31787
31745
  const actions = state.actions;
31788
31746
  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;
31747
+ const branch_name = state.branch_name;
31748
+ const commit_map = state.commit_map;
31749
+ const master_branch = state.master_branch;
31750
+ const repo_root = state.repo_root;
31751
+ const sync_github = state.sync_github;
31752
+ invariant(branch_name, "branch_name must exist");
31753
+ invariant(commit_map, "commit_map must exist");
31754
+ invariant(repo_root, "repo_root must exist");
31755
+ invariant(sync_github, "sync_github must exist");
31756
+ const commit_range = sync_github.commit_range;
31757
+ const rebase_group_index = sync_github.rebase_group_index;
31758
+ // always listen for SIGINT event and restore pr state
31759
+ args.abort_handler.current = function sigint_handler() {
31760
+ actions.output(reactExports.createElement(Text, { color: colors.red }, "\uD83D\uDEA8 Abort"));
31761
+ handle_exit(17);
31762
+ };
31763
+ let DEFAULT_PR_BODY = "";
31764
+ if (state.pr_template_body) {
31765
+ DEFAULT_PR_BODY = state.pr_template_body;
31766
+ }
31767
+ const push_group_list = get_push_group_list();
31768
+ // for all push targets in push_group_list
31769
+ // things that can be done in parallel are grouped by numbers
31770
+ //
31771
+ // -----------------------------------
31772
+ // 1 (before_push) temp mark draft
31773
+ // --------------------------------------
31774
+ // 2 push simultaneously to github
31775
+ // --------------------------------------
31776
+ // 2 create PR / edit PR
31777
+ // 2 (after_push) undo temp mark draft
31778
+ // --------------------------------------
31779
+ try {
31780
+ const before_push_tasks = [];
31781
+ for (const group of push_group_list) {
31782
+ before_push_tasks.push(before_push({ group }));
31797
31783
  }
31798
- if (group.pr?.state === "MERGED") {
31799
- needs_rebase = true;
31784
+ await Promise.all(before_push_tasks);
31785
+ // git push -f origin HEAD~6:OtVX7Qvrw HEAD~3:E63ytp5dj HEAD~2:gs-NBabNSjXA HEAD~1:gs-UGVJdKNoD HEAD~0:gs-6LAx-On4
31786
+ const git_push_command = [`git push -f origin`];
31787
+ if (argv.verify === false) {
31788
+ git_push_command.push("--no-verify");
31789
+ }
31790
+ for (const group of push_group_list) {
31791
+ const last_commit = last$1(group.commits);
31792
+ invariant(last_commit, "last_commit must exist");
31793
+ const target = `${last_commit.sha}:${group.id}`;
31794
+ git_push_command.push(target);
31795
+ }
31796
+ await cli(git_push_command);
31797
+ const pr_url_list = commit_range.group_list.map(get_group_url);
31798
+ const after_push_tasks = [];
31799
+ for (const group of push_group_list) {
31800
+ after_push_tasks.push(after_push({ group, pr_url_list }));
31801
+ }
31802
+ await Promise.all(after_push_tasks);
31803
+ // finally, ensure all prs have the updated stack table from updated pr_url_list
31804
+ // this step must come after the after_push since that step may create new PRs
31805
+ // we need the urls for all prs at this step so we run it after the after_push
31806
+ const update_pr_body_tasks = [];
31807
+ for (let i = 0; i < commit_range.group_list.length; i++) {
31808
+ const group = commit_range.group_list[i];
31809
+ // use the updated pr_url_list to get the actual selected_url
31810
+ const selected_url = pr_url_list[i];
31811
+ const task = update_pr_body({ group, selected_url, pr_url_list });
31812
+ update_pr_body_tasks.push(task);
31813
+ }
31814
+ await Promise.all(update_pr_body_tasks);
31815
+ actions.set((state) => {
31816
+ state.step = "post-rebase-status";
31817
+ });
31818
+ }
31819
+ catch (err) {
31820
+ if (err instanceof Error) {
31821
+ actions.error(err.message);
31822
+ }
31823
+ actions.error("Unable to sync.");
31824
+ if (!argv.verbose) {
31825
+ actions.error("Try again with `--verbose` to see more information.");
31800
31826
  }
31827
+ await handle_exit(18);
31801
31828
  }
31802
- if (argv.check) {
31803
- actions.exit(0);
31829
+ function get_push_group_list() {
31830
+ // start from HEAD and work backward to rebase_group_index
31831
+ const push_group_list = [];
31832
+ for (let i = 0; i < commit_range.group_list.length; i++) {
31833
+ const index = commit_range.group_list.length - 1 - i;
31834
+ // do not go past rebase_group_index
31835
+ if (index < rebase_group_index) {
31836
+ break;
31837
+ }
31838
+ const group = commit_range.group_list[index];
31839
+ push_group_list.unshift(group);
31840
+ }
31841
+ return push_group_list;
31804
31842
  }
31805
- else if (needs_rebase) {
31806
- Store.setState((state) => {
31807
- state.step = "pre-local-merge-rebase";
31808
- });
31843
+ async function before_push(args) {
31844
+ const { group } = args;
31845
+ invariant(group.base, "group.base must exist");
31846
+ // we may temporarily mark PR as a draft before editing it
31847
+ // if it is not already a draft PR, to avoid notification spam
31848
+ let is_temp_draft = !group.pr?.isDraft;
31849
+ // before pushing reset base to master temporarily
31850
+ // avoid accidentally pointing to orphaned parent commit
31851
+ // should hopefully fix issues where a PR includes a bunch of commits after pushing
31852
+ if (group.pr) {
31853
+ if (!group.pr.isDraft) {
31854
+ is_temp_draft = true;
31855
+ }
31856
+ if (is_temp_draft) {
31857
+ await pr_draft({
31858
+ branch: group.id,
31859
+ draft: true,
31860
+ });
31861
+ }
31862
+ await pr_edit({
31863
+ branch: group.id,
31864
+ base: master_branch,
31865
+ });
31866
+ }
31809
31867
  }
31810
- else if (needs_update) {
31811
- Store.setState((state) => {
31812
- state.step = "pre-select-commit-ranges";
31813
- });
31868
+ async function after_push(args) {
31869
+ const { group, pr_url_list } = args;
31870
+ invariant(group.base, "group.base must exist");
31871
+ const selected_url = get_group_url(group);
31872
+ if (group.pr) {
31873
+ // ensure base matches pr in github
31874
+ await pr_edit({
31875
+ branch: group.id,
31876
+ base: group.base,
31877
+ body: write({
31878
+ body: group.pr.body,
31879
+ pr_url_list,
31880
+ selected_url,
31881
+ }),
31882
+ });
31883
+ // we may temporarily mark PR as a draft before editing it
31884
+ // if it is not already a draft PR, to avoid notification spam
31885
+ let is_temp_draft = !group.pr?.isDraft;
31886
+ if (is_temp_draft) {
31887
+ // mark pr as ready for review again
31888
+ await pr_draft({
31889
+ branch: group.id,
31890
+ draft: false,
31891
+ });
31892
+ }
31893
+ }
31894
+ else {
31895
+ // create pr in github
31896
+ const pr_url = await pr_create({
31897
+ branch: group.id,
31898
+ base: group.base,
31899
+ title: group.title,
31900
+ body: DEFAULT_PR_BODY,
31901
+ draft: argv.draft,
31902
+ });
31903
+ if (!pr_url) {
31904
+ throw new Error("unable to create pr");
31905
+ }
31906
+ // update pr_url_list with created pr_url
31907
+ for (let i = 0; i < pr_url_list.length; i++) {
31908
+ const url = pr_url_list[i];
31909
+ if (url === selected_url) {
31910
+ pr_url_list[i] = pr_url;
31911
+ }
31912
+ }
31913
+ }
31814
31914
  }
31815
- else if (argv.force) {
31816
- Store.setState((state) => {
31817
- state.step = "select-commit-ranges";
31915
+ async function update_pr_body(args) {
31916
+ const { group, selected_url, pr_url_list } = args;
31917
+ invariant(group.base, "group.base must exist");
31918
+ const body = group.pr?.body || DEFAULT_PR_BODY;
31919
+ const update_body = write({
31920
+ body,
31921
+ pr_url_list,
31922
+ selected_url,
31818
31923
  });
31924
+ if (update_body === body) {
31925
+ actions.debug(`Skipping body update for ${selected_url}`);
31926
+ }
31927
+ else {
31928
+ actions.debug(`Update body for ${selected_url}`);
31929
+ await pr_edit({
31930
+ branch: group.id,
31931
+ base: group.base,
31932
+ body: update_body,
31933
+ });
31934
+ }
31819
31935
  }
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);
31936
+ function handle_exit(code) {
31937
+ actions.output(reactExports.createElement(Text, { color: colors.yellow }, "Restoring PR state\u2026"));
31938
+ for (const group of push_group_list) {
31939
+ // we may temporarily mark PR as a draft before editing it
31940
+ // if it is not already a draft PR, to avoid notification spam
31941
+ let is_temp_draft = !group.pr?.isDraft;
31942
+ // restore PR to non-draft state
31943
+ if (is_temp_draft) {
31944
+ pr_draft({
31945
+ branch: group.id,
31946
+ draft: false,
31947
+ })
31948
+ .catch(actions.error);
31949
+ }
31950
+ }
31951
+ actions.output(reactExports.createElement(Text, { color: colors.yellow }, "Restored PR state."));
31952
+ actions.exit(code);
31827
31953
  }
31828
31954
  }
31955
+ const get_group_url = (group) => group.pr?.url || group.id;
31829
31956
 
31830
31957
  function Main() {
31831
31958
  const step = Store.useState((state) => state.step);
@@ -31848,6 +31975,8 @@ function Main() {
31848
31975
  return reactExports.createElement(PreManualRebase, null);
31849
31976
  case "manual-rebase":
31850
31977
  return reactExports.createElement(ManualRebase, null);
31978
+ case "sync-github":
31979
+ return reactExports.createElement(SyncGithub, null);
31851
31980
  case "post-rebase-status":
31852
31981
  return reactExports.createElement(PostRebaseStatus, null);
31853
31982
  default:
@@ -37444,7 +37573,7 @@ async function command() {
37444
37573
  .wrap(123)
37445
37574
  // disallow unknown options
37446
37575
  .strict()
37447
- .version("1.13.0" )
37576
+ .version("1.13.1" )
37448
37577
  .showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
37449
37578
  .help("help", "Show usage via `git stack help`")
37450
37579
  .argv;
@@ -37529,7 +37658,10 @@ const FixupOptions = {
37529
37658
 
37530
37659
  command()
37531
37660
  .then((argv) => {
37532
- const ink = render(reactExports.createElement(App, null));
37661
+ const ink = render(reactExports.createElement(App, null), {
37662
+ // If true, each update will be rendered as a separate output, without replacing the previous one.
37663
+ // debug: true,
37664
+ });
37533
37665
  Store.setState((state) => {
37534
37666
  state.ink = ink;
37535
37667
  state.process_argv = process.argv;