git-stack-cli 1.14.0 → 1.15.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.
Files changed (49) hide show
  1. package/README.md +2 -4
  2. package/dist/cjs/index.cjs +310 -180
  3. package/package.json +2 -1
  4. package/scripts/link.ts +14 -0
  5. package/src/app/App.tsx +41 -30
  6. package/src/app/AutoUpdate.tsx +9 -24
  7. package/src/app/CherryPickCheck.tsx +1 -2
  8. package/src/app/Debug.tsx +5 -6
  9. package/src/app/DependencyCheck.tsx +6 -6
  10. package/src/app/DetectInitialPR.tsx +2 -8
  11. package/src/app/DirtyCheck.tsx +51 -26
  12. package/src/app/Exit.tsx +41 -8
  13. package/src/app/FormatText.tsx +1 -5
  14. package/src/app/GatherMetadata.tsx +6 -13
  15. package/src/app/GithubApiError.tsx +1 -1
  16. package/src/app/HandleCtrlCSigint.tsx +36 -0
  17. package/src/app/LocalCommitStatus.tsx +1 -3
  18. package/src/app/LogTimestamp.tsx +1 -5
  19. package/src/app/ManualRebase.tsx +15 -37
  20. package/src/app/MultiSelect.tsx +2 -2
  21. package/src/app/PostRebaseStatus.tsx +2 -0
  22. package/src/app/PreManualRebase.tsx +3 -5
  23. package/src/app/RebaseCheck.tsx +1 -2
  24. package/src/app/SelectCommitRanges.tsx +6 -10
  25. package/src/app/Status.tsx +1 -1
  26. package/src/app/StatusTable.tsx +1 -4
  27. package/src/app/Store.tsx +29 -3
  28. package/src/app/SyncGithub.tsx +15 -45
  29. package/src/app/Table.tsx +4 -14
  30. package/src/app/TextInput.tsx +2 -7
  31. package/src/app/VerboseDebugInfo.tsx +1 -5
  32. package/src/app/YesNoPrompt.tsx +42 -31
  33. package/src/command.ts +8 -17
  34. package/src/commands/Fixup.tsx +17 -24
  35. package/src/commands/Log.tsx +3 -7
  36. package/src/commands/Rebase.tsx +18 -38
  37. package/src/components/ErrorBoundary.tsx +79 -0
  38. package/src/components/ExitingGate.tsx +27 -0
  39. package/src/core/CommitMetadata.ts +1 -1
  40. package/src/core/GitReviseTodo.test.ts +3 -3
  41. package/src/core/GitReviseTodo.ts +6 -8
  42. package/src/core/Metadata.test.ts +4 -4
  43. package/src/core/StackSummaryTable.ts +3 -3
  44. package/src/core/chalk.ts +1 -5
  45. package/src/core/cli.ts +2 -2
  46. package/src/core/github.tsx +15 -14
  47. package/src/core/pretty_json.ts +7 -0
  48. package/src/github/gh.auth_status.test.ts +2 -6
  49. package/src/index.tsx +42 -6
@@ -22,31 +22,23 @@ async function run() {
22
22
 
23
23
  if (!relative_number) {
24
24
  actions.output(
25
- <Ink.Text color={colors.red}>
26
- ❗️ Usage: git fixup {"<relative-commit-number>"}
27
- </Ink.Text>
25
+ <Ink.Text color={colors.red}>❗️ Usage: git fixup {"<relative-commit-number>"}</Ink.Text>,
28
26
  );
29
27
  actions.output("");
28
+ actions.output("This script automates the process of adding staged changes as a fixup commit");
30
29
  actions.output(
31
- "This script automates the process of adding staged changes as a fixup commit"
32
- );
33
- actions.output(
34
- "and the subsequent git rebase to flatten the commits based on relative commit number"
35
- );
36
- actions.output(
37
- "You can use a `git log` like below to get the relative commit number"
30
+ "and the subsequent git rebase to flatten the commits based on relative commit number",
38
31
  );
32
+ actions.output("You can use a `git log` like below to get the relative commit number");
39
33
  actions.output("");
40
34
  actions.output(" ❯ git stack log");
41
35
  actions.output(
42
- " 1\te329794d5f881cbf0fc3f26d2108cf6f3fdebabe enable drop_error_subtask test param"
36
+ " 1\te329794d5f881cbf0fc3f26d2108cf6f3fdebabe enable drop_error_subtask test param",
43
37
  );
44
38
  actions.output(
45
- " 2\t57f43b596e5c6b97bc47e2a591f82ccc81651156 test drop_error_subtask baseline"
46
- );
47
- actions.output(
48
- " 3\t838e878d483c6a2d5393063fc59baf2407225c6d ErrorSubtask test baseline"
39
+ " 2\t57f43b596e5c6b97bc47e2a591f82ccc81651156 test drop_error_subtask baseline",
49
40
  );
41
+ actions.output(" 3\t838e878d483c6a2d5393063fc59baf2407225c6d ErrorSubtask test baseline");
50
42
  actions.output("");
51
43
  actions.output("To target `838e87` above, you would call `fixup 3`");
52
44
 
@@ -71,8 +63,7 @@ async function run() {
71
63
  const adjusted_number = Number(relative_number) - 1;
72
64
 
73
65
  // get the commit SHA of the target commit
74
- const commit_sha = (await cli(`git rev-parse HEAD~${adjusted_number}`))
75
- .stdout;
66
+ const commit_sha = (await cli(`git rev-parse HEAD~${adjusted_number}`)).stdout;
76
67
 
77
68
  actions.output(
78
69
  <FormatText
@@ -82,7 +73,7 @@ async function run() {
82
73
  commit_sha: <Parens>{commit_sha}</Parens>,
83
74
  relative_number: relative_number,
84
75
  }}
85
- />
76
+ />,
86
77
  );
87
78
 
88
79
  await cli(`git commit --fixup ${commit_sha}`);
@@ -97,9 +88,13 @@ async function run() {
97
88
  if (diff_cmd.code) {
98
89
  save_stash = true;
99
90
 
100
- await cli("git stash -q");
91
+ await cli("git stash --include-untracked");
101
92
 
102
- actions.output(<Ink.Text>📦 Changes saved to stash</Ink.Text>);
93
+ actions.output(
94
+ <Ink.Text color={colors.yellow}>
95
+ <FormatText message="📦 Changes saved to stash" />
96
+ </Ink.Text>,
97
+ );
103
98
  }
104
99
 
105
100
  try {
@@ -118,11 +113,9 @@ async function run() {
118
113
  await cli("git reset --soft HEAD~1");
119
114
  } finally {
120
115
  if (save_stash) {
121
- await cli("git stash pop -q");
116
+ await cli("git stash pop");
122
117
 
123
- actions.output(
124
- <Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>
125
- );
118
+ actions.output(<Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>);
126
119
  }
127
120
  }
128
121
  }
@@ -50,13 +50,9 @@ async function run(args: Args) {
50
50
  const subject_format = `%<(60,trunc)%s`;
51
51
 
52
52
  // combine all the above formats into one
53
- const format = [
54
- sha_format,
55
- date_format,
56
- author_format,
57
- decoration_format,
58
- subject_format,
59
- ].join(" ");
53
+ const format = [sha_format, date_format, author_format, decoration_format, subject_format].join(
54
+ " ",
55
+ );
60
56
 
61
57
  // view the SHA, description and history graph of last 20 commits
62
58
  const rest_args = process_argv.slice(3).join(" ");
@@ -16,35 +16,15 @@ import { invariant } from "~/core/invariant";
16
16
  import { short_id } from "~/core/short_id";
17
17
 
18
18
  export function Rebase() {
19
- const abort_handler = React.useRef(() => {});
20
-
21
- React.useEffect(function listen_sigint() {
22
- process.once("SIGINT", sigint_handler);
23
-
24
- return function cleanup() {
25
- process.removeListener("SIGINT", sigint_handler);
26
- };
27
-
28
- function sigint_handler() {
29
- abort_handler.current();
30
- }
31
- }, []);
32
-
33
19
  return (
34
20
  <Await
35
21
  fallback={<Ink.Text color={colors.yellow}>Rebasing commits…</Ink.Text>}
36
- function={async function () {
37
- await Rebase.run({ abort_handler });
38
- }}
22
+ function={Rebase.run}
39
23
  />
40
24
  );
41
25
  }
42
26
 
43
- type Args = {
44
- abort_handler: React.MutableRefObject<() => void>;
45
- };
46
-
47
- Rebase.run = async function run(args: Args) {
27
+ Rebase.run = async function run() {
48
28
  const state = Store.getState();
49
29
  const actions = state.actions;
50
30
  const branch_name = state.branch_name;
@@ -57,11 +37,12 @@ Rebase.run = async function run(args: Args) {
57
37
  invariant(commit_range, "commit_range must exist");
58
38
  invariant(repo_root, "repo_root must exist");
59
39
 
60
- // always listen for SIGINT event and restore git state
61
- args.abort_handler.current = async function sigint_handler() {
40
+ // immediately register abort_handler in case of ctrl+c exit
41
+ actions.register_abort_handler(async function abort_rebase() {
62
42
  actions.output(<Ink.Text color={colors.red}>🚨 Abort</Ink.Text>);
63
- handle_exit(19);
64
- };
43
+ handle_exit();
44
+ return 19;
45
+ });
65
46
 
66
47
  const temp_branch_name = `${branch_name}_${short_id()}`;
67
48
 
@@ -73,9 +54,7 @@ Rebase.run = async function run(args: Args) {
73
54
  await cli(`pwd`);
74
55
 
75
56
  // update local master to match remote
76
- await cli(
77
- `git fetch --no-tags -v origin ${master_branch}:${master_branch}`
78
- );
57
+ await cli(`git fetch --no-tags -v origin ${master_branch}:${master_branch}`);
79
58
 
80
59
  const master_sha = (await cli(`git rev-parse ${master_branch}`)).stdout;
81
60
  const rebase_merge_base = master_sha;
@@ -102,7 +81,7 @@ Rebase.run = async function run(args: Args) {
102
81
  commit_message: <Brackets>{commit.subject_line}</Brackets>,
103
82
  pr_status: <Parens>MERGED</Parens>,
104
83
  }}
105
- />
84
+ />,
106
85
  );
107
86
  }
108
87
 
@@ -117,7 +96,7 @@ Rebase.run = async function run(args: Args) {
117
96
  values={{
118
97
  commit_message: <Brackets>{commit.subject_line}</Brackets>,
119
98
  }}
120
- />
99
+ />,
121
100
  );
122
101
  }
123
102
 
@@ -150,9 +129,11 @@ Rebase.run = async function run(args: Args) {
150
129
  branch_name: <Brackets>{branch_name}</Brackets>,
151
130
  origin_branch: <Brackets>{`origin/${master_branch}`}</Brackets>,
152
131
  }}
153
- />
132
+ />,
154
133
  );
155
134
 
135
+ actions.unregister_abort_handler();
136
+
156
137
  actions.set((state) => {
157
138
  state.commit_range = next_commit_range;
158
139
  state.step = "status";
@@ -166,7 +147,8 @@ Rebase.run = async function run(args: Args) {
166
147
  }
167
148
  }
168
149
 
169
- handle_exit(20);
150
+ handle_exit();
151
+ actions.exit(20);
170
152
  }
171
153
 
172
154
  // cleanup git operations if cancelled during manual rebase
@@ -194,11 +176,11 @@ Rebase.run = async function run(args: Args) {
194
176
  cli.sync(`pwd`, spawn_options);
195
177
  }
196
178
 
197
- function handle_exit(code: number) {
179
+ function handle_exit() {
198
180
  actions.output(
199
181
  <Ink.Text color={colors.yellow}>
200
182
  Restoring <Brackets>{branch_name}</Brackets>…
201
- </Ink.Text>
183
+ </Ink.Text>,
202
184
  );
203
185
 
204
186
  restore_git();
@@ -206,9 +188,7 @@ Rebase.run = async function run(args: Args) {
206
188
  actions.output(
207
189
  <Ink.Text color={colors.yellow}>
208
190
  Restored <Brackets>{branch_name}</Brackets>.
209
- </Ink.Text>
191
+ </Ink.Text>,
210
192
  );
211
-
212
- actions.exit(code);
213
193
  }
214
194
  };
@@ -0,0 +1,79 @@
1
+ /* eslint-disable no-console */
2
+ import * as React from "react";
3
+
4
+ import * as Ink from "ink-cjs";
5
+
6
+ import { FormatText } from "~/app/FormatText";
7
+ import { Store } from "~/app/Store";
8
+ import { colors } from "~/core/colors";
9
+
10
+ type Props = {
11
+ children: React.ReactNode;
12
+ };
13
+
14
+ type State = {
15
+ error: null | Error;
16
+ component_stack: string;
17
+ };
18
+
19
+ export class ErrorBoundary extends React.Component<Props, State> {
20
+ constructor(props: Props) {
21
+ super(props);
22
+
23
+ this.state = {
24
+ error: null,
25
+ component_stack: "",
26
+ };
27
+ }
28
+
29
+ static getDerivedStateFromError(error: Error) {
30
+ return { error };
31
+ }
32
+
33
+ override componentDidCatch(_error: Error, error_info: React.ErrorInfo) {
34
+ let component_stack = error_info.componentStack;
35
+
36
+ if (component_stack) {
37
+ // remove first line of component_stack
38
+ component_stack = component_stack.split("\n").slice(1).join("\n");
39
+ this.setState({ component_stack });
40
+ }
41
+ }
42
+
43
+ override render() {
44
+ if (!this.state.error) {
45
+ return this.props.children;
46
+ }
47
+
48
+ const message = this.state.error.message;
49
+
50
+ return (
51
+ <Ink.Box flexDirection="column" gap={0}>
52
+ <Ink.Text color={colors.red}>
53
+ <FormatText
54
+ message="🚨 Unhandled error {message}"
55
+ values={{
56
+ message: <Ink.Text color={colors.gray}>{message}</Ink.Text>,
57
+ }}
58
+ />
59
+ </Ink.Text>
60
+
61
+ {this._render_verbose()}
62
+ </Ink.Box>
63
+ );
64
+ }
65
+
66
+ _render_verbose() {
67
+ const store_state = Store.getState();
68
+
69
+ if (store_state.argv.verbose) {
70
+ return <Ink.Text color={colors.gray}>{this.state.component_stack}</Ink.Text>;
71
+ }
72
+
73
+ return (
74
+ <Ink.Text color={colors.gray}>
75
+ <FormatText message="Try again with `--verbose` to see more information." />
76
+ </Ink.Text>
77
+ );
78
+ }
79
+ }
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+
3
+ import * as Ink from "ink-cjs";
4
+
5
+ import { FormatText } from "~/app/FormatText";
6
+ import { Store } from "~/app/Store";
7
+ import { colors } from "~/core/colors";
8
+
9
+ type Props = {
10
+ children: React.ReactNode;
11
+ };
12
+
13
+ export function ExitingGate(props: Props) {
14
+ const is_exiting = Store.useState((state) => state.is_exiting);
15
+
16
+ if (!is_exiting) {
17
+ return props.children;
18
+ }
19
+
20
+ return (
21
+ <Ink.Box flexDirection="column">
22
+ <Ink.Text color={colors.red}>
23
+ <FormatText message="🚨 Exiting…" />
24
+ </Ink.Text>
25
+ </Ink.Box>
26
+ );
27
+ }
@@ -167,7 +167,7 @@ export async function range(commit_group_map?: CommitGroupMap) {
167
167
  async function get_commit_list() {
168
168
  const master_branch = Store.getState().master_branch;
169
169
  const log_result = await cli(
170
- `git log ${master_branch}..HEAD --oneline --format=%H --color=never`
170
+ `git log ${master_branch}..HEAD --oneline --format=%H --color=never`,
171
171
  );
172
172
 
173
173
  if (!log_result.stdout) {
@@ -35,7 +35,7 @@ test("git-revise-todo from commit range with single new commit", () => {
35
35
  "",
36
36
  "git-stack-id: E63ytp5dj",
37
37
  "git-stack-title: lemon color",
38
- ].join("\n")
38
+ ].join("\n"),
39
39
  );
40
40
  });
41
41
 
@@ -53,7 +53,7 @@ test("git-revise-todo from commit range with single new commit in new group", ()
53
53
  "",
54
54
  "git-stack-id: 6Ak-qn+5Z",
55
55
  "git-stack-title: new group",
56
- ].join("\n")
56
+ ].join("\n"),
57
57
  );
58
58
  });
59
59
 
@@ -71,7 +71,7 @@ test("git-revise-todo handles double quotes in commit message", () => {
71
71
  "",
72
72
  "git-stack-id: 6Ak-qn+5Z",
73
73
  'git-stack-title: [new] invalid \\"by me\\" quotes',
74
- ].join("\n")
74
+ ].join("\n"),
75
75
  );
76
76
  });
77
77
 
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import * as Metadata from "~/core/Metadata";
6
6
  import { cli } from "~/core/cli";
7
7
  import { invariant } from "~/core/invariant";
8
+ import { safe_rm } from "~/core/safe_rm";
8
9
 
9
10
  import type * as CommitMetadata from "~/core/CommitMetadata";
10
11
 
@@ -86,10 +87,7 @@ GitReviseTodo.todo = function todo(args: CommitListArgs) {
86
87
 
87
88
  const metadata = { id, title };
88
89
 
89
- const unsafe_message_with_id = Metadata.write(
90
- commit.full_message,
91
- metadata
92
- );
90
+ const unsafe_message_with_id = Metadata.write(commit.full_message, metadata);
93
91
 
94
92
  let message_with_id = unsafe_message_with_id;
95
93
 
@@ -111,10 +109,7 @@ GitReviseTodo.todo = function todo(args: CommitListArgs) {
111
109
 
112
110
  GitReviseTodo.execute = async function grt_execute(args: ExecuteArgs) {
113
111
  // generate temporary directory and drop sequence editor script
114
- const tmp_git_sequence_editor_path = path.join(
115
- os.tmpdir(),
116
- "git-sequence-editor.sh"
117
- );
112
+ const tmp_git_sequence_editor_path = path.join(os.tmpdir(), "git-sequence-editor.sh");
118
113
 
119
114
  // replaced at build time with literal contents of `scripts/git-sequence-editor.sh`
120
115
  const GIT_SEQUENCE_EDITOR_SCRIPT = `process.env.GIT_SEQUENCE_EDITOR_SCRIPT`;
@@ -140,6 +135,9 @@ GitReviseTodo.execute = async function grt_execute(args: ExecuteArgs) {
140
135
  // change to pipe to see output temporarily
141
136
  // https://github.com/magus/git-stack-cli/commit/f9f10e3ac3cd9a35ee75d3e0851a48391967a23f
142
137
  await cli(command, { stdio: ["ignore", "ignore", "ignore"] });
138
+
139
+ // cleanup tmp_git_sequence_editor_path
140
+ await safe_rm(tmp_git_sequence_editor_path);
143
141
  };
144
142
 
145
143
  type ExecuteArgs = {
@@ -48,7 +48,7 @@ test("write handles bulleted lists", () => {
48
48
  "",
49
49
  "git-stack-id: abcd1234",
50
50
  "git-stack-title: banana",
51
- ].join("\n")
51
+ ].join("\n"),
52
52
  );
53
53
  });
54
54
 
@@ -94,7 +94,7 @@ test("write handles bulleted lists", () => {
94
94
  "",
95
95
  "git-stack-id: fix-slash-branch",
96
96
  "git-stack-title: fix slash branch",
97
- ].join("\n")
97
+ ].join("\n"),
98
98
  );
99
99
  });
100
100
 
@@ -133,7 +133,7 @@ test("write handles double quotes", () => {
133
133
  "",
134
134
  "git-stack-id: abc123",
135
135
  'git-stack-title: Revert \\"[abc / 123] subject (#1234)\\"',
136
- ].join("\n")
136
+ ].join("\n"),
137
137
  );
138
138
  });
139
139
 
@@ -156,6 +156,6 @@ test("removes metadata", () => {
156
156
  "- keyboard modality escape key",
157
157
  "- centralize settings",
158
158
  "- move logic inside if branch",
159
- ].join("\n")
159
+ ].join("\n"),
160
160
  );
161
161
  });
@@ -122,7 +122,7 @@ const TEMPLATE = {
122
122
  const RE = {
123
123
  // https://regex101.com/r/kqB9Ft/1
124
124
  stack_table_legacy: new RegExp(
125
- TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")
125
+ TEMPLATE.stack_table_legacy("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)"),
126
126
  ),
127
127
 
128
128
  stack_table_link: new RegExp(
@@ -131,7 +131,7 @@ const RE = {
131
131
  .replace("]", "\\]")
132
132
  .replace("(", "\\(")
133
133
  .replace(")", "\\)")
134
- .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")
134
+ .replace("ROWS", "\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)"),
135
135
  ),
136
136
 
137
137
  row: new RegExp(
@@ -139,7 +139,7 @@ const RE = {
139
139
  icon: "(?<icon>.+)",
140
140
  num: "(?<num>\\d+)",
141
141
  pr_url: "(?<pr_url>.+)",
142
- })
142
+ }),
143
143
  ),
144
144
 
145
145
  pr_url: /^https:\/\/.*$/,
package/src/core/chalk.ts CHANGED
@@ -19,11 +19,7 @@ function create_color_proxy(base: typeof chalk): ColorProxy {
19
19
 
20
20
  case "bracket":
21
21
  return (str: string) =>
22
- [
23
- target.bold.whiteBright("["),
24
- str,
25
- target.bold.whiteBright("]"),
26
- ].join("");
22
+ [target.bold.whiteBright("["), str, target.bold.whiteBright("]")].join("");
27
23
 
28
24
  case "url":
29
25
  return target.bold.underline.blueBright;
package/src/core/cli.ts CHANGED
@@ -22,7 +22,7 @@ let i = 0;
22
22
 
23
23
  export async function cli(
24
24
  unsafe_command: string | Array<string | number>,
25
- unsafe_options?: Options
25
+ unsafe_options?: Options,
26
26
  ): Promise<Return> {
27
27
  const options = Object.assign({}, unsafe_options);
28
28
 
@@ -97,7 +97,7 @@ export async function cli(
97
97
 
98
98
  cli.sync = function cli_sync(
99
99
  unsafe_command: string | Array<string | number>,
100
- unsafe_options?: Options
100
+ unsafe_options?: Options,
101
101
  ): Return {
102
102
  const options = Object.assign({}, unsafe_options);
103
103
 
@@ -24,7 +24,7 @@ export async function pr_list(): Promise<Array<PullRequest>> {
24
24
  invariant(repo_path, "repo_path must exist");
25
25
 
26
26
  const result_pr_list = await gh_json<Array<PullRequest>>(
27
- `pr list --repo ${repo_path} --author ${username} --state open ${JSON_FIELDS}`
27
+ `pr list --repo ${repo_path} --author ${username} --state open ${JSON_FIELDS}`,
28
28
  );
29
29
 
30
30
  if (result_pr_list instanceof Error) {
@@ -42,7 +42,7 @@ export async function pr_list(): Promise<Array<PullRequest>> {
42
42
  <Brackets>{repo_path}</Brackets>
43
43
  <Ink.Text>{" authored by "}</Ink.Text>
44
44
  <Brackets>{username}</Brackets>
45
- </Ink.Text>
45
+ </Ink.Text>,
46
46
  );
47
47
  }
48
48
 
@@ -77,7 +77,7 @@ export async function pr_status(branch: string): Promise<null | PullRequest> {
77
77
  </Ink.Text>
78
78
  <Ink.Text> </Ink.Text>
79
79
  <Ink.Text dimColor>{branch}</Ink.Text>
80
- </Ink.Text>
80
+ </Ink.Text>,
81
81
  );
82
82
  }
83
83
 
@@ -94,13 +94,11 @@ export async function pr_status(branch: string): Promise<null | PullRequest> {
94
94
  </Ink.Text>
95
95
  <Ink.Text> </Ink.Text>
96
96
  <Ink.Text dimColor>{branch}</Ink.Text>
97
- </Ink.Text>
97
+ </Ink.Text>,
98
98
  );
99
99
  }
100
100
 
101
- const pr = await gh_json<PullRequest>(
102
- `pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`
103
- );
101
+ const pr = await gh_json<PullRequest>(`pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`);
104
102
 
105
103
  if (pr instanceof Error) {
106
104
  return null;
@@ -161,8 +159,10 @@ type EditPullRequestArgs = {
161
159
  export async function pr_edit(args: EditPullRequestArgs) {
162
160
  const command_parts = [`gh pr edit ${args.branch} --base ${args.base}`];
163
161
 
162
+ let body_file: string | undefined;
163
+
164
164
  if (args.body) {
165
- const body_file = await write_body_file(args);
165
+ body_file = await write_body_file(args);
166
166
  command_parts.push(`--body-file="${body_file}"`);
167
167
  }
168
168
 
@@ -171,6 +171,11 @@ export async function pr_edit(args: EditPullRequestArgs) {
171
171
  if (cli_result.code !== 0) {
172
172
  handle_error(cli_result.output);
173
173
  }
174
+
175
+ // cleanup body_file
176
+ if (body_file) {
177
+ await safe_rm(body_file);
178
+ }
174
179
  }
175
180
 
176
181
  type DraftPullRequestArgs = {
@@ -183,9 +188,7 @@ export async function pr_draft(args: DraftPullRequestArgs) {
183
188
  // https://docs.github.com/en/graphql/reference/mutations#convertpullrequesttodraft
184
189
  // https://docs.github.com/en/graphql/reference/mutations#markpullrequestreadyforreview
185
190
 
186
- const mutation_name = args.draft
187
- ? "convertPullRequestToDraft"
188
- : "markPullRequestReadyForReview";
191
+ const mutation_name = args.draft ? "convertPullRequestToDraft" : "markPullRequestReadyForReview";
189
192
 
190
193
  let query = `
191
194
  mutation($id: ID!) {
@@ -208,9 +211,7 @@ export async function pr_draft(args: DraftPullRequestArgs) {
208
211
  const cache_pr = state.pr[args.branch];
209
212
  invariant(cache_pr, "cache_pr must exist");
210
213
 
211
- const command_parts = [
212
- `gh api graphql -F id="${cache_pr.id}" -f query='${query}'`,
213
- ];
214
+ const command_parts = [`gh api graphql -F id="${cache_pr.id}" -f query='${query}'`];
214
215
 
215
216
  const command = command_parts.join(" ");
216
217
 
@@ -0,0 +1,7 @@
1
+ export namespace pretty_json {
2
+ export type JSONValue = null | number | string | boolean | { [key: string]: JSONValue };
3
+ }
4
+
5
+ export function pretty_json<T extends pretty_json.JSONValue>(input: T): string {
6
+ return JSON.stringify(input, null, 2);
7
+ }
@@ -3,17 +3,13 @@ import { test, expect } from "bun:test";
3
3
  import * as gh from "./gh";
4
4
 
5
5
  test("logged in as", () => {
6
- const username = gh.auth_status(
7
- " ✓ Logged in to github.com as magus (keyring)\n"
8
- );
6
+ const username = gh.auth_status(" ✓ Logged in to github.com as magus (keyring)\n");
9
7
 
10
8
  expect(username).toBe("magus");
11
9
  });
12
10
 
13
11
  test("logged in without as", () => {
14
- const username = gh.auth_status(
15
- "✓ Logged in to github.com account xoxohorses (keyring)"
16
- );
12
+ const username = gh.auth_status("✓ Logged in to github.com account xoxohorses (keyring)");
17
13
 
18
14
  expect(username).toBe("xoxohorses");
19
15
  });