git-stack-cli 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,15 +20,18 @@
20
20
  > [!TIP]
21
21
  >
22
22
  > Install via **[Homebrew](https://brew.sh/)** to ensure the **[official Github CLI](https://cli.github.com/)** and **[git revise](https://github.com/mystor/git-revise)** dependencies are installed automatically
23
+ >
24
+ > ```bash
25
+ > brew install magus/git-stack/git-stack
26
+ > ```
23
27
 
24
- ```bash
25
- brew tap magus/git-stack
26
- brew install git-stack
27
- ```
28
+ <details>
28
29
 
29
- ### `npm`
30
+ <summary>
31
+ npm alternative
32
+ </summary>
30
33
 
31
- Installing via **[npm](https://www.npmjs.com/)** requires installing the **[official Github CLI](https://cli.github.com/)** and **[git revise](https://github.com/mystor/git-revise)** dependencies separarely
34
+ If you prefer to use **[npm](https://www.npmjs.com/)** you will need to install the **[official Github CLI](https://cli.github.com/)** and **[git revise](https://github.com/mystor/git-revise)** dependencies separarely
32
35
 
33
36
  ```bash
34
37
  brew install gh
@@ -37,6 +40,8 @@ brew install git-revise
37
40
  npm i -g git-stack-cli
38
41
  ```
39
42
 
43
+ </details>
44
+
40
45
  ## Usage
41
46
 
42
47
  ```bash
@@ -76,6 +81,8 @@ Managing even a few stacked diffs requires a relatively strong knowledge of `git
76
81
 
77
82
  ## Development
78
83
 
84
+ Ensure `node --version` is the same across both projects you are using to test the `git-stack` cli
85
+
79
86
  ```bash
80
87
  git submodule update --init --recursive
81
88
  npm i
@@ -27800,7 +27800,8 @@ function GitReviseTodo(args) {
27800
27800
  for (const commit of group.commits) {
27801
27801
  // update git commit message with stack id
27802
27802
  const metadata = { id: group.id, title: group.title };
27803
- const message_with_id = write$1(commit.full_message, metadata);
27803
+ const unsafe_message_with_id = write$1(commit.full_message, metadata);
27804
+ const message_with_id = unsafe_message_with_id.replace(/"/g, '\\"');
27804
27805
  // get first 12 characters of commit sha
27805
27806
  const sha = commit.sha.slice(0, 12);
27806
27807
  // generate git revise entry
@@ -27816,9 +27817,13 @@ function GitReviseTodo(args) {
27816
27817
  function write(args) {
27817
27818
  const stack_table = table(args);
27818
27819
  let result = args.body;
27819
- if (RE.stack_table.test(result)) {
27820
+ if (RE.stack_table_link.test(result)) {
27820
27821
  // replace stack table
27821
- result = result.replace(RE.stack_table, stack_table);
27822
+ result = result.replace(RE.stack_table_link, stack_table);
27823
+ }
27824
+ else if (RE.stack_table_legacy.test(result)) {
27825
+ // replace stack table
27826
+ result = result.replace(RE.stack_table_legacy, stack_table);
27822
27827
  }
27823
27828
  else {
27824
27829
  // append stack table
@@ -27861,10 +27866,13 @@ function table(args) {
27861
27866
  if (!stack_list.length) {
27862
27867
  return "";
27863
27868
  }
27864
- return TEMPLATE.stack_table(["", ...stack_list, "", ""].join("\n"));
27869
+ return TEMPLATE.stack_table_link(["", ...stack_list, "", ""].join("\n"));
27865
27870
  }
27866
27871
  function parse(body) {
27867
- const stack_table_match = body.match(RE.stack_table);
27872
+ let stack_table_match = body.match(RE.stack_table_link);
27873
+ if (!stack_table_match?.groups) {
27874
+ stack_table_match = body.match(RE.stack_table_legacy);
27875
+ }
27868
27876
  if (!stack_table_match?.groups) {
27869
27877
  return new Map();
27870
27878
  }
@@ -27886,16 +27894,25 @@ function parse(body) {
27886
27894
  return result;
27887
27895
  }
27888
27896
  const TEMPLATE = {
27889
- stack_table(rows) {
27897
+ stack_table_legacy(rows) {
27890
27898
  return `#### git stack${rows}`;
27891
27899
  },
27900
+ stack_table_link(rows) {
27901
+ return `#### [git stack](https://github.com/magus/git-stack-cli)${rows}`;
27902
+ },
27892
27903
  row(args) {
27893
27904
  return `- ${args.icon} \`${args.num}\` ${args.pr_url}`;
27894
27905
  },
27895
27906
  };
27896
27907
  const RE = {
27897
27908
  // https://regex101.com/r/kqB9Ft/1
27898
- stack_table: new RegExp(TEMPLATE.stack_table("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
27909
+ stack_table_legacy: new RegExp(TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
27910
+ stack_table_link: new RegExp(TEMPLATE.stack_table_link("ROWS")
27911
+ .replace("[", "\\[")
27912
+ .replace("]", "\\]")
27913
+ .replace("(", "\\(")
27914
+ .replace(")", "\\)")
27915
+ .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")),
27899
27916
  row: new RegExp(TEMPLATE.row({
27900
27917
  icon: "(?<icon>.+)",
27901
27918
  num: "(?<num>\\d+)",
@@ -28499,7 +28516,8 @@ function MultiSelect(props) {
28499
28516
  });
28500
28517
  // clamp index to keep in item range
28501
28518
  const [index, set_index] = reactExports.useReducer((_, value) => {
28502
- return clamp(value, 0, props.items.length - 1);
28519
+ const next_index = clamp(value, 0, props.items.length - 1);
28520
+ return next_index;
28503
28521
  }, 0, function find_initial_index() {
28504
28522
  let last_enabled;
28505
28523
  for (let i = props.items.length - 1; i >= 0; i--) {
@@ -28526,9 +28544,17 @@ function MultiSelect(props) {
28526
28544
  const selected_list = Array.from(selected_set);
28527
28545
  const selected = selected_set.has(index);
28528
28546
  const state = selected_list.map((index) => props.items[index].value);
28529
- // console.debug({ item, selected, state });
28547
+ // console.debug("onSelect", { item, selected, state });
28530
28548
  props.onSelect({ item, selected, state });
28531
28549
  }, [selected_set]);
28550
+ reactExports.useEffect(() => {
28551
+ const item = props.items[index].value;
28552
+ const selected_list = Array.from(selected_set);
28553
+ const selected = selected_set.has(index);
28554
+ const state = selected_list.map((index) => props.items[index].value);
28555
+ // console.debug("onFocus", { item, selected, state });
28556
+ props.onFocus?.({ item, selected, state });
28557
+ }, [index]);
28532
28558
  useInput((input, key) => {
28533
28559
  if (props.disabled) {
28534
28560
  // console.debug("[MultiSelect] disabled, ignoring input");
@@ -28663,7 +28689,7 @@ function TextInput(props) {
28663
28689
  reactExports.createElement(Text, { color: colors.yellow, dimColor: true, inverse: caret_visible }, " ")));
28664
28690
  }
28665
28691
  function get_value(props) {
28666
- return props.value || "";
28692
+ return props.value || props.defaultValue || "";
28667
28693
  }
28668
28694
 
28669
28695
  function SelectCommitRanges() {
@@ -28794,9 +28820,13 @@ function SelectCommitRangesInternal(props) {
28794
28820
  group_title_width = Math.min(group.title.length, group_title_width);
28795
28821
  let max_item_width = max_group_label_width;
28796
28822
  max_item_width -= left_arrow.length + right_arrow.length;
28823
+ const [focused, set_focused] = reactExports.useState("");
28797
28824
  return (reactExports.createElement(Box, { flexDirection: "column" },
28798
28825
  reactExports.createElement(Box, { height: 1 }),
28799
- reactExports.createElement(MultiSelect, { key: group.id, items: items, maxWidth: max_item_width, disabled: group_input, onSelect: (args) => {
28826
+ reactExports.createElement(MultiSelect, { key: group.id, items: items, maxWidth: max_item_width, disabled: group_input, onFocus: (args) => {
28827
+ // console.debug("onFocus", args);
28828
+ set_focused(args.item.subject_line);
28829
+ }, onSelect: (args) => {
28800
28830
  // console.debug("onSelect", args);
28801
28831
  const key = args.item.sha;
28802
28832
  let value;
@@ -28841,7 +28871,7 @@ function SelectCommitRangesInternal(props) {
28841
28871
  enter: (reactExports.createElement(Text, { bold: true, color: colors.green }, SYMBOL.enter)),
28842
28872
  } }))),
28843
28873
  } }),
28844
- reactExports.createElement(TextInput, { onSubmit: submit_group_input }),
28874
+ reactExports.createElement(TextInput, { defaultValue: focused, onSubmit: submit_group_input }),
28845
28875
  reactExports.createElement(Box, { height: 1 }))),
28846
28876
  reactExports.createElement(Box, null,
28847
28877
  reactExports.createElement(FormatText, { wrapper: reactExports.createElement(Text, { color: colors.gray }), message: "Press {left} and {right} to view PR groups", values: {
@@ -34422,7 +34452,7 @@ async function command() {
34422
34452
  .wrap(123)
34423
34453
  // disallow unknown options
34424
34454
  .strict()
34425
- .version("1.5.1" )
34455
+ .version("1.6.0" )
34426
34456
  .showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
34427
34457
  .help("help", "Show usage via `git stack help`").argv);
34428
34458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-stack-cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "author": "magus",
6
6
  "license": "MIT",
@@ -9,7 +9,8 @@ import { wrap_index } from "~/core/wrap_index";
9
9
 
10
10
  type Props<T> = {
11
11
  items: Array<Item<T>>;
12
- onSelect(args: SelectArgs<T>): void;
12
+ onSelect: (args: CallbackArgs<T>) => void;
13
+ onFocus?: (args: CallbackArgs<T>) => void;
13
14
  disabled?: boolean;
14
15
  maxWidth?: number;
15
16
  };
@@ -21,7 +22,7 @@ type Item<T> = {
21
22
  disabled?: ItemRowProps["disabled"];
22
23
  };
23
24
 
24
- type SelectArgs<T> = {
25
+ type CallbackArgs<T> = {
25
26
  item: T;
26
27
  selected: boolean;
27
28
  state: Array<T>;
@@ -55,7 +56,8 @@ export function MultiSelect<T>(props: Props<T>) {
55
56
  // clamp index to keep in item range
56
57
  const [index, set_index] = React.useReducer(
57
58
  (_: unknown, value: number) => {
58
- return clamp(value, 0, props.items.length - 1);
59
+ const next_index = clamp(value, 0, props.items.length - 1);
60
+ return next_index;
59
61
  },
60
62
  0,
61
63
  function find_initial_index() {
@@ -94,10 +96,20 @@ export function MultiSelect<T>(props: Props<T>) {
94
96
  const selected = selected_set.has(index);
95
97
  const state = selected_list.map((index) => props.items[index].value);
96
98
 
97
- // console.debug({ item, selected, state });
99
+ // console.debug("onSelect", { item, selected, state });
98
100
  props.onSelect({ item, selected, state });
99
101
  }, [selected_set]);
100
102
 
103
+ React.useEffect(() => {
104
+ const item = props.items[index].value;
105
+ const selected_list = Array.from(selected_set);
106
+ const selected = selected_set.has(index);
107
+ const state = selected_list.map((index) => props.items[index].value);
108
+
109
+ // console.debug("onFocus", { item, selected, state });
110
+ props.onFocus?.({ item, selected, state });
111
+ }, [index]);
112
+
101
113
  Ink.useInput((input, key) => {
102
114
  if (props.disabled) {
103
115
  // console.debug("[MultiSelect] disabled, ignoring input");
@@ -199,6 +199,8 @@ function SelectCommitRangesInternal(props: Props) {
199
199
  let max_item_width = max_group_label_width;
200
200
  max_item_width -= left_arrow.length + right_arrow.length;
201
201
 
202
+ const [focused, set_focused] = React.useState("");
203
+
202
204
  return (
203
205
  <Ink.Box flexDirection="column">
204
206
  <Ink.Box height={1} />
@@ -208,6 +210,11 @@ function SelectCommitRangesInternal(props: Props) {
208
210
  items={items}
209
211
  maxWidth={max_item_width}
210
212
  disabled={group_input}
213
+ onFocus={(args) => {
214
+ // console.debug("onFocus", args);
215
+
216
+ set_focused(args.item.subject_line);
217
+ }}
211
218
  onSelect={(args) => {
212
219
  // console.debug("onSelect", args);
213
220
 
@@ -326,7 +333,7 @@ function SelectCommitRangesInternal(props: Props) {
326
333
  }}
327
334
  />
328
335
 
329
- <TextInput onSubmit={submit_group_input} />
336
+ <TextInput defaultValue={focused} onSubmit={submit_group_input} />
330
337
 
331
338
  <Ink.Box height={1} />
332
339
  </React.Fragment>
@@ -6,6 +6,7 @@ import { colors } from "~/core/colors";
6
6
 
7
7
  type Props = {
8
8
  multiline?: boolean;
9
+ defaultValue?: string;
9
10
  value?: string;
10
11
  onChange?: (value: string) => void;
11
12
  onSubmit?: (value: string) => void;
@@ -84,5 +85,5 @@ export function TextInput(props: Props) {
84
85
  }
85
86
 
86
87
  function get_value(props: Props) {
87
- return props.value || "";
88
+ return props.value || props.defaultValue || "";
88
89
  }
@@ -57,6 +57,24 @@ test("git-revise-todo from commit range with single new commit in new group", ()
57
57
  );
58
58
  });
59
59
 
60
+ test("git-revise-todo handles double quotes in commit message", () => {
61
+ const rebase_group_index = 0;
62
+ const commit_range = COMMIT_MESSAGE_WITH_QUOTES;
63
+
64
+ const git_revise_todo = GitReviseTodo({ rebase_group_index, commit_range });
65
+
66
+ expect(git_revise_todo).toBe(
67
+ [
68
+ //force line break
69
+ "++ pick f143d03c723c",
70
+ '[new] invalid \\\\"by me\\\\" quotes',
71
+ "",
72
+ "git-stack-id: 6Ak-qn+5Z",
73
+ 'git-stack-title: [new] invalid \\"by me\\" quotes',
74
+ ].join("\n")
75
+ );
76
+ });
77
+
60
78
  const SINGLE_COMMIT_EXISTING_GROUP: CommitMetadata.CommitRange = {
61
79
  "invalid": false,
62
80
  "group_list": [
@@ -595,3 +613,36 @@ const SINGLE_COMMIT_NEW_GROUP: CommitMetadata.CommitRange = {
595
613
  },
596
614
  "UNASSIGNED": "unassigned",
597
615
  };
616
+
617
+ const COMMIT_MESSAGE_WITH_QUOTES: CommitMetadata.CommitRange = {
618
+ "invalid": false,
619
+ "group_list": [
620
+ {
621
+ "id": "6Ak-qn+5Z",
622
+ "title": '[new] invalid "by me" quotes',
623
+ "pr": null,
624
+ "base": "E63ytp5dj",
625
+ "dirty": true,
626
+ "commits": [
627
+ {
628
+ "sha": "f143d03c723c9f5231a81c1e12098511611898cb",
629
+ "full_message": '[new] invalid "by me" quotes',
630
+ "subject_line": '[new] invalid "by me" quotes',
631
+ "branch_id": null,
632
+ "title": null,
633
+ },
634
+ ],
635
+ },
636
+ ],
637
+ "commit_list": [
638
+ {
639
+ "sha": "f143d03c723c9f5231a81c1e12098511611898cb",
640
+ "full_message": '[new] invalid "by me" quotes',
641
+ "subject_line": '[new] invalid "by me" quotes',
642
+ "branch_id": null,
643
+ "title": null,
644
+ },
645
+ ],
646
+ "pr_lookup": {},
647
+ "UNASSIGNED": "unassigned",
648
+ };
@@ -57,7 +57,11 @@ export function GitReviseTodo(args: Args): string {
57
57
  for (const commit of group.commits) {
58
58
  // update git commit message with stack id
59
59
  const metadata = { id: group.id, title: group.title };
60
- const message_with_id = Metadata.write(commit.full_message, metadata);
60
+ const unsafe_message_with_id = Metadata.write(
61
+ commit.full_message,
62
+ metadata
63
+ );
64
+ const message_with_id = unsafe_message_with_id.replace(/"/g, '\\"');
61
65
 
62
66
  // get first 12 characters of commit sha
63
67
  const sha = commit.sha.slice(0, 12);
@@ -79,7 +79,7 @@ test("builds list of prs with selected emoji", () => {
79
79
  expect(output.split("\n")).toEqual([
80
80
  ...args.body.split("\n"),
81
81
  "",
82
- "#### git stack",
82
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
83
83
  "- 👉 `1` https://github.com/magus/git-multi-diff-playground/pull/43",
84
84
  "- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/47",
85
85
  ]);
@@ -89,7 +89,7 @@ test("can parse stack table from body", () => {
89
89
  const body_line_list = [
90
90
  "",
91
91
  "",
92
- "#### git stack",
92
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
93
93
  "- invalid line that will be dropped",
94
94
  "- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/47",
95
95
  "- 👉 `1` https://github.com/magus/git-multi-diff-playground/pull/43",
@@ -122,7 +122,7 @@ test("persists removed pr urls from previous stack table", () => {
122
122
  body: [
123
123
  "Summary of problem",
124
124
  "",
125
- "#### git stack",
125
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
126
126
  "- ⏳ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
127
127
  "- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/44",
128
128
  "- 👉 `3` https://github.com/magus/git-multi-diff-playground/pull/47",
@@ -142,7 +142,7 @@ test("persists removed pr urls from previous stack table", () => {
142
142
  expect(output.split("\n")).toEqual([
143
143
  "Summary of problem",
144
144
  "",
145
- "#### git stack",
145
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
146
146
  "- ✅ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
147
147
  "- ✅ `2` https://github.com/magus/git-multi-diff-playground/pull/44",
148
148
  "- 👉 `3` https://github.com/magus/git-multi-diff-playground/pull/47",
@@ -161,7 +161,7 @@ test("persist only valid urls, removed broken branch ids from interrupted sync",
161
161
  body: [
162
162
  "Summary of problem",
163
163
  "",
164
- "#### git stack",
164
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
165
165
  "- ✅ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
166
166
  "- ✅ `2` gs-P4EBkJm+q",
167
167
  "- 👉 `3` https://github.com/magus/git-multi-diff-playground/pull/47",
@@ -181,7 +181,7 @@ test("persist only valid urls, removed broken branch ids from interrupted sync",
181
181
  expect(output.split("\n")).toEqual([
182
182
  "Summary of problem",
183
183
  "",
184
- "#### git stack",
184
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
185
185
  "- ✅ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
186
186
  "- 👉 `2` https://github.com/magus/git-multi-diff-playground/pull/47",
187
187
  "- ⏳ `3` https://github.com/magus/git-multi-diff-playground/pull/54",
@@ -193,3 +193,68 @@ test("persist only valid urls, removed broken branch ids from interrupted sync",
193
193
 
194
194
  expect(rerun_output).toBe(output);
195
195
  });
196
+
197
+ test("can parse legacy git stack", () => {
198
+ const body_line_list = [
199
+ "",
200
+ "",
201
+ "#### git stack",
202
+ "- invalid line that will be dropped",
203
+ "- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/47",
204
+ "- 👉 `1` https://github.com/magus/git-multi-diff-playground/pull/43",
205
+ ];
206
+
207
+ const parsed = StackSummaryTable.parse(body_line_list.join("\n"));
208
+
209
+ expect(Array.from(parsed.entries())).toEqual([
210
+ [
211
+ "https://github.com/magus/git-multi-diff-playground/pull/47",
212
+ {
213
+ icon: "⏳",
214
+ num: "2",
215
+ pr_url: "https://github.com/magus/git-multi-diff-playground/pull/47",
216
+ },
217
+ ],
218
+ [
219
+ "https://github.com/magus/git-multi-diff-playground/pull/43",
220
+ {
221
+ icon: "👉",
222
+ num: "1",
223
+ pr_url: "https://github.com/magus/git-multi-diff-playground/pull/43",
224
+ },
225
+ ],
226
+ ]);
227
+ });
228
+
229
+ test("converts legacy git stack to link version", () => {
230
+ const args = {
231
+ body: [
232
+ "Summary of problem",
233
+ "",
234
+ "#### git stack",
235
+ "- ✅ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
236
+ "- ✅ `2` gs-P4EBkJm+q",
237
+ "- 👉 `3` https://github.com/magus/git-multi-diff-playground/pull/47",
238
+ ].join("\n"),
239
+
240
+ pr_url_list: [
241
+ "https://github.com/magus/git-multi-diff-playground/pull/47",
242
+ "https://github.com/magus/git-multi-diff-playground/pull/54",
243
+ "https://github.com/magus/git-multi-diff-playground/pull/61",
244
+ ],
245
+
246
+ selected_url: "https://github.com/magus/git-multi-diff-playground/pull/47",
247
+ };
248
+
249
+ const output = StackSummaryTable.write(args);
250
+
251
+ expect(output.split("\n")).toEqual([
252
+ "Summary of problem",
253
+ "",
254
+ "#### [git stack](https://github.com/magus/git-stack-cli)",
255
+ "- ✅ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
256
+ "- 👉 `2` https://github.com/magus/git-multi-diff-playground/pull/47",
257
+ "- ⏳ `3` https://github.com/magus/git-multi-diff-playground/pull/54",
258
+ "- ⏳ `4` https://github.com/magus/git-multi-diff-playground/pull/61",
259
+ ]);
260
+ });
@@ -9,9 +9,12 @@ export function write(args: WriteArgs) {
9
9
 
10
10
  let result = args.body;
11
11
 
12
- if (RE.stack_table.test(result)) {
12
+ if (RE.stack_table_link.test(result)) {
13
13
  // replace stack table
14
- result = result.replace(RE.stack_table, stack_table);
14
+ result = result.replace(RE.stack_table_link, stack_table);
15
+ } else if (RE.stack_table_legacy.test(result)) {
16
+ // replace stack table
17
+ result = result.replace(RE.stack_table_legacy, stack_table);
15
18
  } else {
16
19
  // append stack table
17
20
  result = `${result}\n\n${stack_table}`;
@@ -64,11 +67,15 @@ export function table(args: WriteArgs) {
64
67
  return "";
65
68
  }
66
69
 
67
- return TEMPLATE.stack_table(["", ...stack_list, "", ""].join("\n"));
70
+ return TEMPLATE.stack_table_link(["", ...stack_list, "", ""].join("\n"));
68
71
  }
69
72
 
70
73
  export function parse(body: string): Map<string, StackTableRow> {
71
- const stack_table_match = body.match(RE.stack_table);
74
+ let stack_table_match = body.match(RE.stack_table_link);
75
+
76
+ if (!stack_table_match?.groups) {
77
+ stack_table_match = body.match(RE.stack_table_legacy);
78
+ }
72
79
 
73
80
  if (!stack_table_match?.groups) {
74
81
  return new Map();
@@ -99,10 +106,14 @@ export function parse(body: string): Map<string, StackTableRow> {
99
106
  }
100
107
 
101
108
  const TEMPLATE = {
102
- stack_table(rows: string) {
109
+ stack_table_legacy(rows: string) {
103
110
  return `#### git stack${rows}`;
104
111
  },
105
112
 
113
+ stack_table_link(rows: string) {
114
+ return `#### [git stack](https://github.com/magus/git-stack-cli)${rows}`;
115
+ },
116
+
106
117
  row(args: StackTableRow) {
107
118
  return `- ${args.icon} \`${args.num}\` ${args.pr_url}`;
108
119
  },
@@ -110,8 +121,17 @@ const TEMPLATE = {
110
121
 
111
122
  const RE = {
112
123
  // https://regex101.com/r/kqB9Ft/1
113
- stack_table: new RegExp(
114
- TEMPLATE.stack_table("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")
124
+ stack_table_legacy: new RegExp(
125
+ TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")
126
+ ),
127
+
128
+ stack_table_link: new RegExp(
129
+ TEMPLATE.stack_table_link("ROWS")
130
+ .replace("[", "\\[")
131
+ .replace("]", "\\]")
132
+ .replace("(", "\\(")
133
+ .replace(")", "\\)")
134
+ .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")
115
135
  ),
116
136
 
117
137
  row: new RegExp(