git-stack-cli 1.0.2 → 1.0.4
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 +1 -1
- package/dist/cjs/index.cjs +3 -3
- package/package.json +15 -8
- package/rollup.config.mjs +46 -0
- package/scripts/.eslintrc.cjs +61 -0
- package/scripts/build-standalone.ts +73 -0
- package/scripts/core/create_asset.ts +21 -0
- package/scripts/core/file.ts +36 -0
- package/scripts/core/spawn.ts +62 -0
- package/scripts/npm-prepublishOnly.ts +8 -0
- package/scripts/release-brew.ts +69 -0
- package/scripts/release-github.ts +34 -0
- package/scripts/release-npm.ts +109 -0
- package/scripts/tsconfig.json +35 -0
- package/src/__fixtures__/metadata.ts +666 -0
- package/src/app/App.tsx +65 -0
- package/src/app/AutoUpdate.tsx +229 -0
- package/src/app/Await.tsx +82 -0
- package/src/app/Brackets.tsx +22 -0
- package/src/app/Command.tsx +19 -0
- package/src/app/Debug.tsx +52 -0
- package/src/app/DependencyCheck.tsx +155 -0
- package/src/app/Exit.tsx +25 -0
- package/src/app/FormatText.tsx +26 -0
- package/src/app/GatherMetadata.tsx +145 -0
- package/src/app/GithubApiError.tsx +78 -0
- package/src/app/LocalCommitStatus.tsx +70 -0
- package/src/app/LocalMergeRebase.tsx +230 -0
- package/src/app/LogTimestamp.tsx +12 -0
- package/src/app/Main.tsx +52 -0
- package/src/app/ManualRebase.tsx +308 -0
- package/src/app/MultiSelect.tsx +246 -0
- package/src/app/Output.tsx +37 -0
- package/src/app/Parens.tsx +21 -0
- package/src/app/PostRebaseStatus.tsx +33 -0
- package/src/app/PreLocalMergeRebase.tsx +31 -0
- package/src/app/PreSelectCommitRanges.tsx +31 -0
- package/src/app/Providers.tsx +11 -0
- package/src/app/RebaseCheck.tsx +96 -0
- package/src/app/SelectCommitRanges.tsx +372 -0
- package/src/app/Status.tsx +82 -0
- package/src/app/StatusTable.tsx +155 -0
- package/src/app/Store.tsx +252 -0
- package/src/app/Table.tsx +137 -0
- package/src/app/TextInput.tsx +88 -0
- package/src/app/Url.tsx +19 -0
- package/src/app/Waterfall.tsx +37 -0
- package/src/app/YesNoPrompt.tsx +73 -0
- package/src/command.ts +78 -0
- package/src/core/CommitMetadata.ts +212 -0
- package/src/core/Metadata.test.ts +41 -0
- package/src/core/Metadata.ts +51 -0
- package/src/core/StackSummaryTable.test.ts +157 -0
- package/src/core/StackSummaryTable.ts +127 -0
- package/src/core/Timer.ts +44 -0
- package/src/core/assertNever.ts +4 -0
- package/src/core/cache.ts +49 -0
- package/src/core/capitalize.ts +5 -0
- package/src/core/chalk.ts +103 -0
- package/src/core/clamp.ts +6 -0
- package/src/core/cli.ts +161 -0
- package/src/core/colors.ts +23 -0
- package/src/core/date.ts +25 -0
- package/src/core/fetch_json.ts +26 -0
- package/src/core/github.tsx +215 -0
- package/src/core/invariant.ts +5 -0
- package/src/core/is_command_available.ts +21 -0
- package/src/core/is_finite_value.ts +3 -0
- package/src/core/json.ts +32 -0
- package/src/core/match_group.ts +10 -0
- package/src/core/read_json.ts +12 -0
- package/src/core/safe_quote.ts +10 -0
- package/src/core/semver_compare.ts +27 -0
- package/src/core/short_id.ts +87 -0
- package/src/core/sleep.ts +3 -0
- package/src/core/wrap_index.ts +11 -0
- package/src/index.tsx +22 -0
- package/src/types/global.d.ts +7 -0
- package/tsconfig.json +53 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
type ChalkInstance = typeof chalk;
|
|
4
|
+
|
|
5
|
+
interface ColorProxy extends ChalkInstance {
|
|
6
|
+
test(): void;
|
|
7
|
+
bracket(str: string): string;
|
|
8
|
+
url(str: string): this;
|
|
9
|
+
cmd(str: string): this;
|
|
10
|
+
branch(str: string): this;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function create_color_proxy(base: typeof chalk): ColorProxy {
|
|
14
|
+
return new Proxy(base as ColorProxy, {
|
|
15
|
+
get(target, prop: keyof ColorProxy) {
|
|
16
|
+
switch (prop) {
|
|
17
|
+
case "test":
|
|
18
|
+
return test;
|
|
19
|
+
|
|
20
|
+
case "bracket":
|
|
21
|
+
return (str: string) =>
|
|
22
|
+
[
|
|
23
|
+
target.bold.whiteBright("["),
|
|
24
|
+
str,
|
|
25
|
+
target.bold.whiteBright("]"),
|
|
26
|
+
].join("");
|
|
27
|
+
|
|
28
|
+
case "url":
|
|
29
|
+
return target.bold.underline.blueBright;
|
|
30
|
+
case "cmd":
|
|
31
|
+
return target.bold.yellow;
|
|
32
|
+
case "branch":
|
|
33
|
+
return target.bold.green;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const target_prop = target[prop];
|
|
37
|
+
|
|
38
|
+
return target_prop;
|
|
39
|
+
},
|
|
40
|
+
apply(target, _this_arg, arguments_list) {
|
|
41
|
+
return target(...arguments_list);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export const color = create_color_proxy(chalk);
|
|
46
|
+
|
|
47
|
+
function test() {
|
|
48
|
+
const PROP_LIST = [
|
|
49
|
+
"reset", // Reset the current style.
|
|
50
|
+
"bold", // Make the text bold.
|
|
51
|
+
"dim", // Make the text have lower opacity.
|
|
52
|
+
"italic", // Make the text italic. (Not widely supported)
|
|
53
|
+
"underline", // Put a horizontal line below the text. (Not widely supported)
|
|
54
|
+
"overline", // Put a horizontal line above the text. (Not widely supported)
|
|
55
|
+
"inverse", // Invert background and foreground colors.
|
|
56
|
+
"hidden", // Print the text but make it invisible.
|
|
57
|
+
"strikethrough", // Puts a horizontal line through the center of the text. (Not widely supported)
|
|
58
|
+
"visible", // Print the text only when Chalk has a color level above zero. Can be useful for things that are purely cosmetic.
|
|
59
|
+
|
|
60
|
+
"black",
|
|
61
|
+
"red",
|
|
62
|
+
"green",
|
|
63
|
+
"yellow",
|
|
64
|
+
"blue",
|
|
65
|
+
"magenta",
|
|
66
|
+
"cyan",
|
|
67
|
+
"white",
|
|
68
|
+
"blackBright",
|
|
69
|
+
"gray",
|
|
70
|
+
"grey",
|
|
71
|
+
"redBright",
|
|
72
|
+
"greenBright",
|
|
73
|
+
"yellowBright",
|
|
74
|
+
"blueBright",
|
|
75
|
+
"magentaBright",
|
|
76
|
+
"cyanBright",
|
|
77
|
+
"whiteBright",
|
|
78
|
+
|
|
79
|
+
"bgBlack",
|
|
80
|
+
"bgRed",
|
|
81
|
+
"bgGreen",
|
|
82
|
+
"bgYellow",
|
|
83
|
+
"bgBlue",
|
|
84
|
+
"bgMagenta",
|
|
85
|
+
"bgCyan",
|
|
86
|
+
"bgWhite",
|
|
87
|
+
"bgBlackBright",
|
|
88
|
+
"bgGray",
|
|
89
|
+
"bgGrey",
|
|
90
|
+
"bgRedBright",
|
|
91
|
+
"bgGreenBright",
|
|
92
|
+
"bgYellowBright",
|
|
93
|
+
"bgBlueBright",
|
|
94
|
+
"bgMagentaBright",
|
|
95
|
+
"bgCyanBright",
|
|
96
|
+
"bgWhiteBright",
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
for (const prop of PROP_LIST) {
|
|
100
|
+
// eslint-disable-next-line no-console
|
|
101
|
+
console.debug(chalk[prop](prop));
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/core/cli.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as child from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { Store } from "~/app/Store";
|
|
4
|
+
import { Timer } from "~/core/Timer";
|
|
5
|
+
|
|
6
|
+
type SpawnOptions = Parameters<typeof child.spawn>[2];
|
|
7
|
+
|
|
8
|
+
type Options = SpawnOptions & {
|
|
9
|
+
ignoreExitCode?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type Return = {
|
|
13
|
+
command: string;
|
|
14
|
+
code: number;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
output: string;
|
|
18
|
+
duration: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let i = 0;
|
|
22
|
+
|
|
23
|
+
export async function cli(
|
|
24
|
+
unsafe_command: string | Array<string | number>,
|
|
25
|
+
unsafe_options?: Options
|
|
26
|
+
): Promise<Return> {
|
|
27
|
+
const options = Object.assign({}, unsafe_options);
|
|
28
|
+
|
|
29
|
+
const state = Store.getState();
|
|
30
|
+
|
|
31
|
+
let command: string;
|
|
32
|
+
if (Array.isArray(unsafe_command)) {
|
|
33
|
+
command = unsafe_command.join(" ");
|
|
34
|
+
} else {
|
|
35
|
+
command = unsafe_command;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const childProcess = child.spawn("sh", ["-c", command], options);
|
|
40
|
+
|
|
41
|
+
let stdout = "";
|
|
42
|
+
let stderr = "";
|
|
43
|
+
let output = "";
|
|
44
|
+
|
|
45
|
+
const id = `${++i}-${command}`;
|
|
46
|
+
state.actions.debug(log.start(command));
|
|
47
|
+
state.actions.debug(log.pending(command), id);
|
|
48
|
+
|
|
49
|
+
const timer = Timer();
|
|
50
|
+
|
|
51
|
+
function write_output(value: string) {
|
|
52
|
+
output += value;
|
|
53
|
+
state.actions.debug(value, id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
childProcess.stdout?.on("data", (data: Buffer) => {
|
|
57
|
+
const value = String(data);
|
|
58
|
+
stdout += value;
|
|
59
|
+
write_output(value);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
childProcess.stderr?.on("data", (data: Buffer) => {
|
|
63
|
+
const value = String(data);
|
|
64
|
+
stderr += value;
|
|
65
|
+
write_output(value);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
childProcess.on("close", (unsafe_code) => {
|
|
69
|
+
const duration = timer.duration();
|
|
70
|
+
|
|
71
|
+
const result = {
|
|
72
|
+
command,
|
|
73
|
+
code: unsafe_code || 0,
|
|
74
|
+
stdout: stdout.trimEnd(),
|
|
75
|
+
stderr: stderr.trimEnd(),
|
|
76
|
+
output: output.trimEnd(),
|
|
77
|
+
duration,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
state.actions.set((state) => state.mutate.end_pending_output(state, id));
|
|
81
|
+
state.actions.debug(log.end(result));
|
|
82
|
+
state.actions.debug(result.output);
|
|
83
|
+
state.actions.debug("\n");
|
|
84
|
+
|
|
85
|
+
if (!options.ignoreExitCode && result.code !== 0) {
|
|
86
|
+
reject(new Error(log.error(result)));
|
|
87
|
+
} else {
|
|
88
|
+
resolve(result);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
childProcess.on("error", (err) => {
|
|
93
|
+
reject(err);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
cli.sync = function cli_sync(
|
|
99
|
+
unsafe_command: string | Array<string | number>,
|
|
100
|
+
unsafe_options?: Options
|
|
101
|
+
): Return {
|
|
102
|
+
const options = Object.assign({}, unsafe_options);
|
|
103
|
+
|
|
104
|
+
const state = Store.getState();
|
|
105
|
+
|
|
106
|
+
let command: string;
|
|
107
|
+
if (Array.isArray(unsafe_command)) {
|
|
108
|
+
command = unsafe_command.join(" ");
|
|
109
|
+
} else {
|
|
110
|
+
command = unsafe_command;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
state.actions.debug(log.start(command));
|
|
114
|
+
|
|
115
|
+
const timer = Timer();
|
|
116
|
+
|
|
117
|
+
const spawn_return = child.spawnSync("sh", ["-c", command], options);
|
|
118
|
+
|
|
119
|
+
const duration = timer.duration();
|
|
120
|
+
|
|
121
|
+
const stdout = String(spawn_return.stdout);
|
|
122
|
+
const stderr = String(spawn_return.stderr);
|
|
123
|
+
|
|
124
|
+
const result = {
|
|
125
|
+
command,
|
|
126
|
+
code: spawn_return.status || 0,
|
|
127
|
+
stdout,
|
|
128
|
+
stderr,
|
|
129
|
+
output: [stdout, stderr].join(""),
|
|
130
|
+
duration,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
state.actions.debug(log.end(result));
|
|
134
|
+
state.actions.debug(result.output);
|
|
135
|
+
|
|
136
|
+
if (!options.ignoreExitCode && result.code !== 0) {
|
|
137
|
+
throw new Error(log.error(result));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const log = {
|
|
144
|
+
start(command: string) {
|
|
145
|
+
return `[start] ${command}`;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
pending(command: string) {
|
|
149
|
+
return `[⏳] ${command}\n`;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
end(result: Return) {
|
|
153
|
+
const { command, code, duration } = result;
|
|
154
|
+
return `[end] ${command} (exit_code=${code} duration=${duration})`;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
error(result: Return) {
|
|
158
|
+
const { command, code, duration } = result;
|
|
159
|
+
return `${command} (exit_code=${code} duration=${duration})`;
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ink uses chalk internally
|
|
2
|
+
// https://github.com/vadimdemedes/ink#color
|
|
3
|
+
|
|
4
|
+
export const colors = {
|
|
5
|
+
red: "rgb(248, 81, 73)",
|
|
6
|
+
// red-emphasis rgb(218, 54, 51)
|
|
7
|
+
|
|
8
|
+
green: "rgb(63, 185, 80)",
|
|
9
|
+
// green-emphasis rgb(35, 134, 54)
|
|
10
|
+
|
|
11
|
+
purple: "rgb(163, 113, 247)",
|
|
12
|
+
// purple-emphasis rgb(137, 87, 229)
|
|
13
|
+
|
|
14
|
+
blue: "rgb(47, 129, 247)",
|
|
15
|
+
|
|
16
|
+
orange: "rgb(255, 166, 87)",
|
|
17
|
+
|
|
18
|
+
yellow: "rgb(234, 179, 8)",
|
|
19
|
+
|
|
20
|
+
gray: "rgb(110, 118, 129)",
|
|
21
|
+
|
|
22
|
+
lightGray: "rgb(125, 133, 144)",
|
|
23
|
+
};
|
package/src/core/date.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function format(date: Date) {
|
|
2
|
+
const year = intl_format(date, { year: "numeric" });
|
|
3
|
+
const month = intl_format(date, { month: "2-digit" });
|
|
4
|
+
const day = intl_format(date, { day: "2-digit" });
|
|
5
|
+
|
|
6
|
+
const time = format_time(date);
|
|
7
|
+
|
|
8
|
+
return `${year}-${month}-${day} ${time}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type IntlDateTimeFormatOptions = Parameters<typeof Intl.DateTimeFormat>[1];
|
|
12
|
+
|
|
13
|
+
export function intl_format(date: Date, options: IntlDateTimeFormatOptions) {
|
|
14
|
+
return new Intl.DateTimeFormat("en-US", options).format(date);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function format_time(date: Date) {
|
|
18
|
+
const time = intl_format(date, {
|
|
19
|
+
hour: "numeric",
|
|
20
|
+
minute: "2-digit",
|
|
21
|
+
hour12: true,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return time;
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import https from "node:https";
|
|
2
|
+
|
|
3
|
+
export async function fetch_json<T = any>(url: string): Promise<T> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
https
|
|
6
|
+
.get(url, (res) => {
|
|
7
|
+
let data = "";
|
|
8
|
+
|
|
9
|
+
res.on("data", (chunk) => {
|
|
10
|
+
data += chunk;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
res.on("end", () => {
|
|
14
|
+
try {
|
|
15
|
+
const json = JSON.parse(data);
|
|
16
|
+
resolve(json);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
reject(error);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
})
|
|
22
|
+
.on("error", (error) => {
|
|
23
|
+
reject(error);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
import * as Ink from "ink-cjs";
|
|
8
|
+
|
|
9
|
+
import { Brackets } from "~/app/Brackets";
|
|
10
|
+
import { Store } from "~/app/Store";
|
|
11
|
+
import { cli } from "~/core/cli";
|
|
12
|
+
import { colors } from "~/core/colors";
|
|
13
|
+
import { invariant } from "~/core/invariant";
|
|
14
|
+
import { safe_quote } from "~/core/safe_quote";
|
|
15
|
+
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
const JSON_FIELDS = "--json number,state,baseRefName,headRefName,commits,title,body,url";
|
|
18
|
+
|
|
19
|
+
export async function pr_list(): Promise<Array<PullRequest>> {
|
|
20
|
+
const state = Store.getState();
|
|
21
|
+
const actions = state.actions;
|
|
22
|
+
|
|
23
|
+
const username = state.username;
|
|
24
|
+
const repo_path = state.repo_path;
|
|
25
|
+
invariant(username, "username must exist");
|
|
26
|
+
invariant(repo_path, "repo_path must exist");
|
|
27
|
+
|
|
28
|
+
const cli_result = await cli(
|
|
29
|
+
`gh pr list --repo ${repo_path} --author ${username} --state open ${JSON_FIELDS}`,
|
|
30
|
+
{
|
|
31
|
+
ignoreExitCode: true,
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (cli_result.code !== 0) {
|
|
36
|
+
handle_error(cli_result.output);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result_pr_list: Array<PullRequest> = JSON.parse(cli_result.stdout);
|
|
40
|
+
|
|
41
|
+
if (actions.isDebug()) {
|
|
42
|
+
actions.output(
|
|
43
|
+
<Ink.Text dimColor>
|
|
44
|
+
<Ink.Text>{"Github cache "}</Ink.Text>
|
|
45
|
+
<Ink.Text bold color={colors.yellow}>
|
|
46
|
+
{result_pr_list.length}
|
|
47
|
+
</Ink.Text>
|
|
48
|
+
<Ink.Text>{" open PRs from "}</Ink.Text>
|
|
49
|
+
<Brackets>{repo_path}</Brackets>
|
|
50
|
+
<Ink.Text>{" authored by "}</Ink.Text>
|
|
51
|
+
<Brackets>{username}</Brackets>
|
|
52
|
+
</Ink.Text>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
actions.set((state) => {
|
|
57
|
+
for (const pr of result_pr_list) {
|
|
58
|
+
state.pr[pr.headRefName] = pr;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return result_pr_list;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function pr_status(branch: string): Promise<null | PullRequest> {
|
|
66
|
+
const state = Store.getState();
|
|
67
|
+
const actions = state.actions;
|
|
68
|
+
|
|
69
|
+
const username = state.username;
|
|
70
|
+
const repo_path = state.repo_path;
|
|
71
|
+
invariant(username, "username must exist");
|
|
72
|
+
invariant(repo_path, "repo_path must exist");
|
|
73
|
+
|
|
74
|
+
const cache = state.pr[branch];
|
|
75
|
+
|
|
76
|
+
if (cache) {
|
|
77
|
+
if (actions.isDebug()) {
|
|
78
|
+
actions.output(
|
|
79
|
+
<Ink.Text>
|
|
80
|
+
<Ink.Text dimColor>Github pr_status cache</Ink.Text>
|
|
81
|
+
<Ink.Text> </Ink.Text>
|
|
82
|
+
<Ink.Text bold color={colors.green}>
|
|
83
|
+
{"HIT "}
|
|
84
|
+
</Ink.Text>
|
|
85
|
+
<Ink.Text> </Ink.Text>
|
|
86
|
+
<Ink.Text dimColor>{branch}</Ink.Text>
|
|
87
|
+
</Ink.Text>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return cache;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (actions.isDebug()) {
|
|
95
|
+
actions.output(
|
|
96
|
+
<Ink.Text>
|
|
97
|
+
<Ink.Text dimColor>Github pr_status cache</Ink.Text>
|
|
98
|
+
<Ink.Text> </Ink.Text>
|
|
99
|
+
<Ink.Text bold color={colors.red}>
|
|
100
|
+
MISS
|
|
101
|
+
</Ink.Text>
|
|
102
|
+
<Ink.Text> </Ink.Text>
|
|
103
|
+
<Ink.Text dimColor>{branch}</Ink.Text>
|
|
104
|
+
</Ink.Text>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const cli_result = await cli(
|
|
109
|
+
`gh pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`,
|
|
110
|
+
{
|
|
111
|
+
ignoreExitCode: true,
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (cli_result.code !== 0) {
|
|
116
|
+
// handle_error(cli_result.output);
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pr: PullRequest = JSON.parse(cli_result.stdout);
|
|
121
|
+
|
|
122
|
+
actions.set((state) => {
|
|
123
|
+
state.pr[pr.headRefName] = pr;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return pr;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type CreatePullRequestArgs = {
|
|
130
|
+
branch: string;
|
|
131
|
+
base: string;
|
|
132
|
+
title: string;
|
|
133
|
+
body: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export async function pr_create(args: CreatePullRequestArgs) {
|
|
137
|
+
const title = safe_quote(args.title);
|
|
138
|
+
|
|
139
|
+
const cli_result = await cli(
|
|
140
|
+
`gh pr create --fill --head ${args.branch} --base ${args.base} --title="${title}" --body="${args.body}"`
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (cli_result.code !== 0) {
|
|
144
|
+
handle_error(cli_result.output);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return cli_result.stdout;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type EditPullRequestArgs = {
|
|
152
|
+
branch: string;
|
|
153
|
+
base: string;
|
|
154
|
+
body: string;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export async function pr_edit(args: EditPullRequestArgs) {
|
|
158
|
+
const cli_result = await cli(
|
|
159
|
+
// prettier-ignore
|
|
160
|
+
`gh pr edit ${args.branch} --base ${args.base} --body-file="${body_file(args.body)}"`
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (cli_result.code !== 0) {
|
|
164
|
+
handle_error(cli_result.output);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handle_error(output: string): never {
|
|
169
|
+
const state = Store.getState();
|
|
170
|
+
const actions = state.actions;
|
|
171
|
+
|
|
172
|
+
actions.set((state) => {
|
|
173
|
+
state.step = "github-api-error";
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
throw new Error(output);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// convert a string to a file for use via github cli `--body-file`
|
|
180
|
+
function body_file(body: string) {
|
|
181
|
+
const temp_dir = os.tmpdir();
|
|
182
|
+
const temp_path = path.join(temp_dir, "git-stack-body");
|
|
183
|
+
if (fs.existsSync(temp_path)) {
|
|
184
|
+
fs.rmSync(temp_path);
|
|
185
|
+
}
|
|
186
|
+
fs.writeFileSync(temp_path, body);
|
|
187
|
+
return temp_path;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type Commit = {
|
|
191
|
+
authoredDate: string; // "2023-10-22T23:13:35Z"
|
|
192
|
+
authors: [
|
|
193
|
+
{
|
|
194
|
+
email: string;
|
|
195
|
+
id: string;
|
|
196
|
+
login: string; // magus
|
|
197
|
+
name: string; // magus
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
committedDate: string; // "2023-10-23T08:41:27Z"
|
|
201
|
+
messageBody: string;
|
|
202
|
+
messageHeadline: string;
|
|
203
|
+
oid: string; // "ce7eadaa73518a92ae6a892c1e54c4f4afa6fbdd"
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export type PullRequest = {
|
|
207
|
+
number: number;
|
|
208
|
+
state: "OPEN" | "MERGED" | "CLOSED";
|
|
209
|
+
baseRefName: string;
|
|
210
|
+
headRefName: string;
|
|
211
|
+
commits: Array<Commit>;
|
|
212
|
+
title: string;
|
|
213
|
+
body: string;
|
|
214
|
+
url: string;
|
|
215
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { invariant } from "~/core/invariant";
|
|
5
|
+
|
|
6
|
+
export function is_command_available(command: string) {
|
|
7
|
+
const PATH = process.env.PATH;
|
|
8
|
+
|
|
9
|
+
invariant(PATH, "PATH env must exist");
|
|
10
|
+
|
|
11
|
+
const path_list = PATH.split(path.delimiter);
|
|
12
|
+
|
|
13
|
+
for (const dir of path_list) {
|
|
14
|
+
const full_path = path.join(dir, command);
|
|
15
|
+
if (fs.existsSync(full_path)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
}
|
package/src/core/json.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function serialize(obj: any): any {
|
|
2
|
+
if (obj instanceof Map) {
|
|
3
|
+
return {
|
|
4
|
+
_type: "Map",
|
|
5
|
+
_value: Array.from(obj.entries()).map(([k, v]) => [k, serialize(v)]),
|
|
6
|
+
};
|
|
7
|
+
} else if (Array.isArray(obj)) {
|
|
8
|
+
return obj.map(serialize);
|
|
9
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
10
|
+
const serializedObj: any = {};
|
|
11
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
12
|
+
serializedObj[key] = serialize(value);
|
|
13
|
+
}
|
|
14
|
+
return serializedObj;
|
|
15
|
+
}
|
|
16
|
+
return obj;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function deserialize(obj: any): any {
|
|
20
|
+
if (obj && obj._type === "Map") {
|
|
21
|
+
return new Map(obj._value.map(([k, v]: [any, any]) => [k, deserialize(v)]));
|
|
22
|
+
} else if (Array.isArray(obj)) {
|
|
23
|
+
return obj.map(deserialize);
|
|
24
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
25
|
+
const deserializedObj: any = {};
|
|
26
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
27
|
+
deserializedObj[key] = deserialize(value);
|
|
28
|
+
}
|
|
29
|
+
return deserializedObj;
|
|
30
|
+
}
|
|
31
|
+
return obj;
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { invariant } from "~/core/invariant";
|
|
2
|
+
|
|
3
|
+
export function match_group(value: string, re: RegExp, group: string) {
|
|
4
|
+
const match = value.match(re);
|
|
5
|
+
const debug = `[${value}.match(${re})]`;
|
|
6
|
+
invariant(match?.groups, `match.groups must exist ${debug}`);
|
|
7
|
+
const result = match?.groups[group];
|
|
8
|
+
invariant(result, `match.groups must contain [${group}] ${debug}`);
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export function read_json<T = unknown>(path: string): null | T {
|
|
4
|
+
try {
|
|
5
|
+
const file_buffer = fs.readFileSync(path);
|
|
6
|
+
const json_str = String(file_buffer);
|
|
7
|
+
const json = JSON.parse(json_str);
|
|
8
|
+
return json;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// returns +1 if version_a is greater than version_b
|
|
2
|
+
// returns -1 if version_a is less than version_b
|
|
3
|
+
// returns +0 if version_a is exactly equal to version_b
|
|
4
|
+
//
|
|
5
|
+
// Examples
|
|
6
|
+
//
|
|
7
|
+
// semver_compare("0.1.1", "0.0.2"); // 1
|
|
8
|
+
// semver_compare("1.0.1", "0.0.2"); // 1
|
|
9
|
+
// semver_compare("0.0.1", "1.0.2"); // -1
|
|
10
|
+
// semver_compare("0.0.1", "0.1.2"); // -1
|
|
11
|
+
// semver_compare("1.0.1", "1.0.1"); // 0
|
|
12
|
+
//
|
|
13
|
+
export function semver_compare(version_a: string, version_b: string) {
|
|
14
|
+
const split_a = version_a.split(".").map(Number);
|
|
15
|
+
const split_b = version_b.split(".").map(Number);
|
|
16
|
+
|
|
17
|
+
const max_split_parts = Math.max(split_a.length, split_b.length);
|
|
18
|
+
for (let i = 0; i < max_split_parts; i++) {
|
|
19
|
+
const num_a = split_a[i] || 0;
|
|
20
|
+
const num_b = split_b[i] || 0;
|
|
21
|
+
|
|
22
|
+
if (num_a > num_b) return 1;
|
|
23
|
+
if (num_a < num_b) return -1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 0;
|
|
27
|
+
}
|