git-stack-cli 0.2.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.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/__fixtures__/metadata.js +763 -0
- package/dist/__fixtures__/metadata.json +186 -0
- package/dist/app/App.js +30 -0
- package/dist/app/ArgCheck.js +21 -0
- package/dist/app/Await.js +45 -0
- package/dist/app/Brackets.js +10 -0
- package/dist/app/Counter.js +19 -0
- package/dist/app/Debug.js +31 -0
- package/dist/app/DependencyCheck.js +52 -0
- package/dist/app/Exit.js +14 -0
- package/dist/app/GatherMetadata copy.js +54 -0
- package/dist/app/GatherMetadata.js +54 -0
- package/dist/app/GithubApiError.js +49 -0
- package/dist/app/InitMetadata.js +14 -0
- package/dist/app/KeepAlive.js +11 -0
- package/dist/app/Main copy.js +200 -0
- package/dist/app/ManualRebase.js +126 -0
- package/dist/app/MultiSelect copy.js +76 -0
- package/dist/app/MultiSelect.js +136 -0
- package/dist/app/Output.js +9 -0
- package/dist/app/Parens copy.js +9 -0
- package/dist/app/Parens.js +9 -0
- package/dist/app/PostRebaseStatus copy.js +23 -0
- package/dist/app/PostRebaseStatus.js +23 -0
- package/dist/app/PreSelectCommitRanges copy.js +29 -0
- package/dist/app/PreSelectCommitRanges.js +11 -0
- package/dist/app/SelectCommitRange.js +1 -0
- package/dist/app/SelectCommitRanges.js +182 -0
- package/dist/app/Status copy.js +46 -0
- package/dist/app/Status.js +45 -0
- package/dist/app/StatusTable.js +94 -0
- package/dist/app/Store.js +73 -0
- package/dist/app/Waterfall.js +20 -0
- package/dist/app/YesNoPrompt copy.js +24 -0
- package/dist/app/YesNoPrompt.js +23 -0
- package/dist/app/main.js +31 -0
- package/dist/cli.js +9 -0
- package/dist/command.js +22 -0
- package/dist/core/CommitMetadata.js +143 -0
- package/dist/core/Metadata.js +36 -0
- package/dist/core/ZustandStore.js +23 -0
- package/dist/core/assertNever.js +4 -0
- package/dist/core/cache.js +39 -0
- package/dist/core/capitalize.js +5 -0
- package/dist/core/clamp.js +6 -0
- package/dist/core/cli copy.js +44 -0
- package/dist/core/cli.js +44 -0
- package/dist/core/color.js +83 -0
- package/dist/core/date.js +18 -0
- package/dist/core/dependency_check.js +27 -0
- package/dist/core/exit.js +4 -0
- package/dist/core/get_commit_metadata.js +61 -0
- package/dist/core/github.js +45 -0
- package/dist/core/invariant.js +5 -0
- package/dist/core/is_command_available.js +15 -0
- package/dist/core/json.js +35 -0
- package/dist/core/match_group.js +9 -0
- package/dist/core/serialize_json.js +17 -0
- package/dist/core/sleep.js +3 -0
- package/dist/core/wrap_index.js +10 -0
- package/dist/index.js +13 -0
- package/dist/main copy.js +266 -0
- package/dist/main.backup.js +266 -0
- package/dist/main.js +265 -0
- package/package.json +51 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as Metadata from "./Metadata.js";
|
|
2
|
+
import { cli } from "./cli.js";
|
|
3
|
+
import * as github from "./github.js";
|
|
4
|
+
export async function range(commit_map) {
|
|
5
|
+
const commit_list = await get_commit_list();
|
|
6
|
+
let invalid = false;
|
|
7
|
+
const group_map = new Map();
|
|
8
|
+
for (const commit of commit_list) {
|
|
9
|
+
let id = commit.branch_id;
|
|
10
|
+
// use commit map if provided (via select commit ranges)
|
|
11
|
+
if (commit_map) {
|
|
12
|
+
id = commit_map[commit.sha];
|
|
13
|
+
}
|
|
14
|
+
const pr = commit.pr;
|
|
15
|
+
if (!pr) {
|
|
16
|
+
// console.debug("INVALID", "MISSING PR", commit.message);
|
|
17
|
+
invalid = true;
|
|
18
|
+
}
|
|
19
|
+
if (id) {
|
|
20
|
+
const group_key_list = Array.from(group_map.keys());
|
|
21
|
+
const last_key = group_key_list[group_key_list.length - 1];
|
|
22
|
+
if (group_map.has(id) && last_key !== id) {
|
|
23
|
+
// if we've seen this id before and it's not
|
|
24
|
+
// the last added key then we are out of order
|
|
25
|
+
// console.debug("INVALID", "OUT OF ORDER", commit.message, id);
|
|
26
|
+
invalid = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// console.debug("INVALID", "NEW COMMIT", { commit });
|
|
31
|
+
invalid = true;
|
|
32
|
+
id = UNASSIGNED;
|
|
33
|
+
}
|
|
34
|
+
const group = group_map.get(id) || {
|
|
35
|
+
id,
|
|
36
|
+
pr,
|
|
37
|
+
base: null,
|
|
38
|
+
dirty: false,
|
|
39
|
+
commits: [],
|
|
40
|
+
};
|
|
41
|
+
group.commits.push(commit);
|
|
42
|
+
group_map.set(id, group);
|
|
43
|
+
}
|
|
44
|
+
// check each group for dirty state and base
|
|
45
|
+
const group_value_list = Array.from(group_map.values());
|
|
46
|
+
const group_list = [];
|
|
47
|
+
let unassigned_group;
|
|
48
|
+
for (let i = 0; i < group_value_list.length; i++) {
|
|
49
|
+
const group = group_value_list[i];
|
|
50
|
+
// console.debug("group", group.pr?.title.substring(0, 40));
|
|
51
|
+
// console.debug(" ", "id", group.id);
|
|
52
|
+
if (group.id === UNASSIGNED) {
|
|
53
|
+
unassigned_group = group;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
group_list.push(group);
|
|
57
|
+
}
|
|
58
|
+
if (i === 0) {
|
|
59
|
+
group.base = "master";
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const last_group = group_value_list[i - 1];
|
|
63
|
+
// console.debug(" ", "last_group", last_group.pr?.title.substring(0, 40));
|
|
64
|
+
// console.debug(" ", "last_group.id", last_group.id);
|
|
65
|
+
// null out base when unassigned and after unassigned
|
|
66
|
+
if (group.id === UNASSIGNED) {
|
|
67
|
+
group.base = null;
|
|
68
|
+
}
|
|
69
|
+
else if (last_group.base === null) {
|
|
70
|
+
group.base = null;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
group.base = last_group.id;
|
|
74
|
+
}
|
|
75
|
+
// console.debug(" ", "group.base", group.base);
|
|
76
|
+
}
|
|
77
|
+
if (!group.pr) {
|
|
78
|
+
group.dirty = true;
|
|
79
|
+
}
|
|
80
|
+
else if (group.pr.commits.length !== group.commits.length) {
|
|
81
|
+
group.dirty = true;
|
|
82
|
+
}
|
|
83
|
+
else if (group.pr.baseRefName !== group.base) {
|
|
84
|
+
group.dirty = true;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
for (let i = 0; i < group.pr.commits.length; i++) {
|
|
88
|
+
const pr_commit = group.pr.commits[i];
|
|
89
|
+
const local_commit = group.commits[i];
|
|
90
|
+
if (pr_commit.oid !== local_commit.sha) {
|
|
91
|
+
group.dirty = true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// console.debug(" ", "group.dirty", group.dirty);
|
|
96
|
+
}
|
|
97
|
+
// reverse group_list to match git log
|
|
98
|
+
group_list.reverse();
|
|
99
|
+
// insert unassigned group at front
|
|
100
|
+
if (unassigned_group) {
|
|
101
|
+
group_list.unshift(unassigned_group);
|
|
102
|
+
}
|
|
103
|
+
return { invalid, group_list, commit_list, UNASSIGNED };
|
|
104
|
+
}
|
|
105
|
+
async function get_commit_list() {
|
|
106
|
+
const log_result = await cli(`git log master..HEAD --oneline --format=%H --color=never`);
|
|
107
|
+
const sha_list = lines(log_result.stdout).reverse();
|
|
108
|
+
const commit_metadata_list = [];
|
|
109
|
+
for (let i = 0; i < sha_list.length; i++) {
|
|
110
|
+
const sha = sha_list[i];
|
|
111
|
+
const commit_metadata = await commit(sha);
|
|
112
|
+
commit_metadata_list.push(commit_metadata);
|
|
113
|
+
}
|
|
114
|
+
return commit_metadata_list;
|
|
115
|
+
}
|
|
116
|
+
export async function commit(sha) {
|
|
117
|
+
const raw_message = (await cli(`git show -s --format=%B ${sha}`)).stdout;
|
|
118
|
+
const branch_id = await Metadata.read(raw_message);
|
|
119
|
+
const message = display_message(raw_message);
|
|
120
|
+
let pr = null;
|
|
121
|
+
if (branch_id) {
|
|
122
|
+
const pr_result = await github.pr_status(branch_id);
|
|
123
|
+
if (pr_result && pr_result.state === "OPEN") {
|
|
124
|
+
pr = pr_result;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
sha,
|
|
129
|
+
message,
|
|
130
|
+
raw_message,
|
|
131
|
+
pr,
|
|
132
|
+
branch_id,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function display_message(message) {
|
|
136
|
+
const line_list = lines(message);
|
|
137
|
+
const first_line = line_list[0];
|
|
138
|
+
return Metadata.remove(first_line);
|
|
139
|
+
}
|
|
140
|
+
function lines(value) {
|
|
141
|
+
return value.split("\n");
|
|
142
|
+
}
|
|
143
|
+
const UNASSIGNED = "unassigned";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { invariant } from "../core/invariant.js";
|
|
2
|
+
export function write(message, branch_id) {
|
|
3
|
+
let result = message;
|
|
4
|
+
// escape double-quote for cli
|
|
5
|
+
result = result.replace(RE.all_double_quote, '\\"');
|
|
6
|
+
// remove any previous metadata lines
|
|
7
|
+
result = remove(result);
|
|
8
|
+
const line_list = [result, "", TEMPLATE.branch_id(branch_id)];
|
|
9
|
+
const new_message = line_list.join("\n");
|
|
10
|
+
return new_message;
|
|
11
|
+
}
|
|
12
|
+
export function read(message) {
|
|
13
|
+
const match = message.match(RE.branch_id);
|
|
14
|
+
if (!match?.groups) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const id = match.groups["id"];
|
|
18
|
+
invariant(id, "id must exist");
|
|
19
|
+
return id;
|
|
20
|
+
}
|
|
21
|
+
export function remove(message) {
|
|
22
|
+
let result = message;
|
|
23
|
+
// remove metadata
|
|
24
|
+
result = result.replace(new RegExp(RE.branch_id, "g"), "");
|
|
25
|
+
result = result.trimEnd();
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
const TEMPLATE = {
|
|
29
|
+
branch_id(id) {
|
|
30
|
+
return `git-multi-diff-id: ${id}`;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const RE = {
|
|
34
|
+
all_double_quote: /"/g,
|
|
35
|
+
branch_id: new RegExp(TEMPLATE.branch_id("(?<id>[a-z0-9-]+)")),
|
|
36
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createStore, useStore } from "zustand";
|
|
2
|
+
import { immer } from "zustand/middleware/immer";
|
|
3
|
+
function ZustandStore() {
|
|
4
|
+
const BaseStore = createStore()(immer((set, get) => ({
|
|
5
|
+
actions: {
|
|
6
|
+
set(setter) {
|
|
7
|
+
set((state) => {
|
|
8
|
+
setter(state);
|
|
9
|
+
});
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})));
|
|
13
|
+
function useState(selector) {
|
|
14
|
+
return useStore(BaseStore, selector);
|
|
15
|
+
}
|
|
16
|
+
function useActions() {
|
|
17
|
+
return useState((state) => state.actions);
|
|
18
|
+
}
|
|
19
|
+
const getState = BaseStore.getState;
|
|
20
|
+
const setState = BaseStore.setState;
|
|
21
|
+
const subscribe = BaseStore.subscribe;
|
|
22
|
+
return { useActions, useState, getState, setState, subscribe };
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function cache(cacheable) {
|
|
2
|
+
let status = "pending";
|
|
3
|
+
let response;
|
|
4
|
+
let suspender;
|
|
5
|
+
function reset() {
|
|
6
|
+
status = "pending";
|
|
7
|
+
response = undefined;
|
|
8
|
+
suspender = undefined;
|
|
9
|
+
}
|
|
10
|
+
function check() {
|
|
11
|
+
return status;
|
|
12
|
+
}
|
|
13
|
+
function read() {
|
|
14
|
+
// cacheable is a function to allow deferred reads
|
|
15
|
+
// this will call cacheable to kickoff async promise
|
|
16
|
+
if (!suspender) {
|
|
17
|
+
suspender = Promise.resolve().then(() => {
|
|
18
|
+
cacheable()
|
|
19
|
+
.then((res) => {
|
|
20
|
+
status = "success";
|
|
21
|
+
response = res;
|
|
22
|
+
})
|
|
23
|
+
.catch((err) => {
|
|
24
|
+
status = "error";
|
|
25
|
+
response = err;
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
switch (status) {
|
|
30
|
+
case "pending":
|
|
31
|
+
throw suspender;
|
|
32
|
+
case "error":
|
|
33
|
+
throw response;
|
|
34
|
+
default:
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { reset, check, read };
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as child from "node:child_process";
|
|
2
|
+
export async function cli(command, unsafe_options) {
|
|
3
|
+
const options = Object.assign({}, unsafe_options);
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const childProcess = child.spawn("sh", ["-c", command], options);
|
|
6
|
+
let stdout = "";
|
|
7
|
+
let stderr = "";
|
|
8
|
+
let output = "";
|
|
9
|
+
childProcess.stdout?.on("data", (data) => {
|
|
10
|
+
stdout += data.toString();
|
|
11
|
+
output += data.toString();
|
|
12
|
+
});
|
|
13
|
+
childProcess.stderr?.on("data", (data) => {
|
|
14
|
+
stderr += data.toString();
|
|
15
|
+
output += data.toString();
|
|
16
|
+
});
|
|
17
|
+
childProcess.on("close", (code) => {
|
|
18
|
+
if (!options.ignoreExitCode && code !== 0) {
|
|
19
|
+
reject(new Error(`[${command}] (${code})`));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const result = {
|
|
23
|
+
code: code || 0,
|
|
24
|
+
stdout: stdout.trimEnd(),
|
|
25
|
+
stderr: stderr.trimEnd(),
|
|
26
|
+
output: output.trimEnd(),
|
|
27
|
+
};
|
|
28
|
+
resolve(result);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
childProcess.on("error", (err) => {
|
|
32
|
+
reject(err);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
cli.sync = function cli_sync(command, unsafe_options) {
|
|
37
|
+
const options = Object.assign({}, unsafe_options);
|
|
38
|
+
const spawn_return = child.spawnSync("sh", ["-c", command], options);
|
|
39
|
+
const stdout = String(spawn_return.stdout);
|
|
40
|
+
const stderr = String(spawn_return.stderr);
|
|
41
|
+
const output = String(spawn_return.output);
|
|
42
|
+
const code = spawn_return.status || 0;
|
|
43
|
+
return { code, stdout, stderr, output };
|
|
44
|
+
};
|
package/dist/core/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as child from "node:child_process";
|
|
2
|
+
export async function cli(command, unsafe_options) {
|
|
3
|
+
const options = Object.assign({}, unsafe_options);
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const childProcess = child.spawn("sh", ["-c", command], options);
|
|
6
|
+
let stdout = "";
|
|
7
|
+
let stderr = "";
|
|
8
|
+
let output = "";
|
|
9
|
+
childProcess.stdout?.on("data", (data) => {
|
|
10
|
+
stdout += data.toString();
|
|
11
|
+
output += data.toString();
|
|
12
|
+
});
|
|
13
|
+
childProcess.stderr?.on("data", (data) => {
|
|
14
|
+
stderr += data.toString();
|
|
15
|
+
output += data.toString();
|
|
16
|
+
});
|
|
17
|
+
childProcess.on("close", (code) => {
|
|
18
|
+
if (!options.ignoreExitCode && code !== 0) {
|
|
19
|
+
reject(new Error(`[${command}] (${code})`));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
const result = {
|
|
23
|
+
code: code || 0,
|
|
24
|
+
stdout: stdout.trimEnd(),
|
|
25
|
+
stderr: stderr.trimEnd(),
|
|
26
|
+
output: output.trimEnd(),
|
|
27
|
+
};
|
|
28
|
+
resolve(result);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
childProcess.on("error", (err) => {
|
|
32
|
+
reject(err);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
cli.sync = function cli_sync(command, unsafe_options) {
|
|
37
|
+
const options = Object.assign({}, unsafe_options);
|
|
38
|
+
const spawn_return = child.spawnSync("sh", ["-c", command], options);
|
|
39
|
+
const stdout = String(spawn_return.stdout);
|
|
40
|
+
const stderr = String(spawn_return.stderr);
|
|
41
|
+
const output = String(spawn_return.output);
|
|
42
|
+
const code = spawn_return.status || 0;
|
|
43
|
+
return { code, stdout, stderr, output };
|
|
44
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
function create_color_proxy(base) {
|
|
3
|
+
return new Proxy(base, {
|
|
4
|
+
get(target, prop) {
|
|
5
|
+
switch (prop) {
|
|
6
|
+
case "test":
|
|
7
|
+
return test;
|
|
8
|
+
case "bracket":
|
|
9
|
+
return (str) => [
|
|
10
|
+
target.bold.whiteBright("["),
|
|
11
|
+
str,
|
|
12
|
+
target.bold.whiteBright("]"),
|
|
13
|
+
].join("");
|
|
14
|
+
case "url":
|
|
15
|
+
return target.bold.underline.blueBright;
|
|
16
|
+
case "cmd":
|
|
17
|
+
return target.bold.yellow;
|
|
18
|
+
case "branch":
|
|
19
|
+
return target.bold.green;
|
|
20
|
+
}
|
|
21
|
+
const target_prop = target[prop];
|
|
22
|
+
return target_prop;
|
|
23
|
+
},
|
|
24
|
+
apply(target, _this_arg, arguments_list) {
|
|
25
|
+
return target(...arguments_list);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export const color = create_color_proxy(chalk);
|
|
30
|
+
function test() {
|
|
31
|
+
const PROP_LIST = [
|
|
32
|
+
"reset",
|
|
33
|
+
"bold",
|
|
34
|
+
"dim",
|
|
35
|
+
"italic",
|
|
36
|
+
"underline",
|
|
37
|
+
"overline",
|
|
38
|
+
"inverse",
|
|
39
|
+
"hidden",
|
|
40
|
+
"strikethrough",
|
|
41
|
+
"visible",
|
|
42
|
+
"black",
|
|
43
|
+
"red",
|
|
44
|
+
"green",
|
|
45
|
+
"yellow",
|
|
46
|
+
"blue",
|
|
47
|
+
"magenta",
|
|
48
|
+
"cyan",
|
|
49
|
+
"white",
|
|
50
|
+
"blackBright",
|
|
51
|
+
"gray",
|
|
52
|
+
"grey",
|
|
53
|
+
"redBright",
|
|
54
|
+
"greenBright",
|
|
55
|
+
"yellowBright",
|
|
56
|
+
"blueBright",
|
|
57
|
+
"magentaBright",
|
|
58
|
+
"cyanBright",
|
|
59
|
+
"whiteBright",
|
|
60
|
+
"bgBlack",
|
|
61
|
+
"bgRed",
|
|
62
|
+
"bgGreen",
|
|
63
|
+
"bgYellow",
|
|
64
|
+
"bgBlue",
|
|
65
|
+
"bgMagenta",
|
|
66
|
+
"bgCyan",
|
|
67
|
+
"bgWhite",
|
|
68
|
+
"bgBlackBright",
|
|
69
|
+
"bgGray",
|
|
70
|
+
"bgGrey",
|
|
71
|
+
"bgRedBright",
|
|
72
|
+
"bgGreenBright",
|
|
73
|
+
"bgYellowBright",
|
|
74
|
+
"bgBlueBright",
|
|
75
|
+
"bgMagentaBright",
|
|
76
|
+
"bgCyanBright",
|
|
77
|
+
"bgWhiteBright",
|
|
78
|
+
];
|
|
79
|
+
for (const prop of PROP_LIST) {
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.debug(chalk[prop](prop));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function format(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
|
+
const time = format_time(date);
|
|
6
|
+
return `${year}-${month}-${day} ${time}`;
|
|
7
|
+
}
|
|
8
|
+
export function intl_format(date, options) {
|
|
9
|
+
return new Intl.DateTimeFormat("en-US", options).format(date);
|
|
10
|
+
}
|
|
11
|
+
export function format_time(date) {
|
|
12
|
+
const time = intl_format(date, {
|
|
13
|
+
hour: "numeric",
|
|
14
|
+
minute: "2-digit",
|
|
15
|
+
hour12: true,
|
|
16
|
+
});
|
|
17
|
+
return time;
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cli } from "./cli.js";
|
|
2
|
+
import { color } from "./color.js";
|
|
3
|
+
import { is_command_available } from "./is_command_available.js";
|
|
4
|
+
export async function dependency_check() {
|
|
5
|
+
if (!is_command_available("git")) {
|
|
6
|
+
console.error(`${color.cmd("git")} must be installed.`);
|
|
7
|
+
process.exitCode = 2;
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (!is_command_available("gh")) {
|
|
11
|
+
console.error(`${color.cmd("gh")} must be installed.`);
|
|
12
|
+
// prettier-ignore
|
|
13
|
+
console.error(`Visit ${color.url("https://cli.github.com/")} to install the github cli (${color.cmd("gh")})`);
|
|
14
|
+
process.exitCode = 3;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const gh_auth_status_cli = await cli(`gh auth status`, {
|
|
18
|
+
ignoreExitCode: true,
|
|
19
|
+
});
|
|
20
|
+
if (gh_auth_status_cli.code !== 0) {
|
|
21
|
+
// prettier-ignore
|
|
22
|
+
console.error(`${color.cmd('gh')} requires login, please run \`${color.cmd('gh auth login')}\`.`);
|
|
23
|
+
process.exitCode = 4;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as github from "../core/github.js";
|
|
2
|
+
import * as Metadata from "./Metadata.js";
|
|
3
|
+
import { cli } from "./cli.js";
|
|
4
|
+
async function all() {
|
|
5
|
+
const log_result = await cli(`git log master..HEAD --oneline --format=%H --color=never`);
|
|
6
|
+
const sha_list = lines(log_result.stdout).reverse();
|
|
7
|
+
const commit_metadata_list = [];
|
|
8
|
+
for (let i = 0; i < sha_list.length; i++) {
|
|
9
|
+
const sha = sha_list[i];
|
|
10
|
+
let base;
|
|
11
|
+
if (i === 0) {
|
|
12
|
+
base = "master";
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
base = commit_metadata_list[i - 1].metadata.id;
|
|
16
|
+
}
|
|
17
|
+
const commit_metadata = await commit(sha, base);
|
|
18
|
+
commit_metadata_list.push(commit_metadata);
|
|
19
|
+
}
|
|
20
|
+
return commit_metadata_list;
|
|
21
|
+
}
|
|
22
|
+
export async function commit(sha, base) {
|
|
23
|
+
const raw_message = (await cli(`git show -s --format=%B ${sha}`)).stdout;
|
|
24
|
+
const metadata = await Metadata.read(raw_message);
|
|
25
|
+
const message = display_message(raw_message);
|
|
26
|
+
let pr = null;
|
|
27
|
+
let pr_exists = false;
|
|
28
|
+
let pr_dirty = false;
|
|
29
|
+
if (metadata.id) {
|
|
30
|
+
const pr_branch = metadata.id;
|
|
31
|
+
pr = await github.pr_status(pr_branch);
|
|
32
|
+
if (pr && pr.state === "OPEN") {
|
|
33
|
+
pr_exists = true;
|
|
34
|
+
const last_commit = pr.commits[pr.commits.length - 1];
|
|
35
|
+
pr_dirty = last_commit.oid !== sha;
|
|
36
|
+
if (pr.baseRefName !== base) {
|
|
37
|
+
// requires base update
|
|
38
|
+
pr_dirty = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
sha,
|
|
44
|
+
base,
|
|
45
|
+
message,
|
|
46
|
+
pr,
|
|
47
|
+
pr_exists,
|
|
48
|
+
pr_dirty,
|
|
49
|
+
metadata,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function display_message(message) {
|
|
53
|
+
// remove metadata
|
|
54
|
+
let result = message;
|
|
55
|
+
result = result.replace(Metadata.id_regex(), "");
|
|
56
|
+
result = result.trimEnd();
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function lines(value) {
|
|
60
|
+
return value.split("\n");
|
|
61
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as Ink from "ink";
|
|
3
|
+
import { Store } from "../app/Store.js";
|
|
4
|
+
import { cli } from "./cli.js";
|
|
5
|
+
export async function pr_status(branch) {
|
|
6
|
+
const state = Store.getState();
|
|
7
|
+
const actions = state.actions;
|
|
8
|
+
const result = await cli(`gh pr view ${branch} --json number,state,baseRefName,headRefName,commits,title,url`, {
|
|
9
|
+
ignoreExitCode: true,
|
|
10
|
+
});
|
|
11
|
+
if (result.code !== 0) {
|
|
12
|
+
actions.output(React.createElement(Ink.Text, { color: "#ef4444" }, result.output));
|
|
13
|
+
actions.set((state) => {
|
|
14
|
+
state.step = "github-api-error";
|
|
15
|
+
});
|
|
16
|
+
throw new Error("Unable to fetch PR status");
|
|
17
|
+
}
|
|
18
|
+
const cache = state.pr[branch];
|
|
19
|
+
if (cache) {
|
|
20
|
+
actions.debug(React.createElement(Ink.Text, null,
|
|
21
|
+
React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
|
|
22
|
+
React.createElement(Ink.Text, null, " "),
|
|
23
|
+
React.createElement(Ink.Text, { bold: true, color: "#22c55e" }, "HIT "),
|
|
24
|
+
React.createElement(Ink.Text, null, " "),
|
|
25
|
+
React.createElement(Ink.Text, { dimColor: true }, branch)));
|
|
26
|
+
return cache;
|
|
27
|
+
}
|
|
28
|
+
actions.debug(React.createElement(Ink.Text, null,
|
|
29
|
+
React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
|
|
30
|
+
React.createElement(Ink.Text, null, " "),
|
|
31
|
+
React.createElement(Ink.Text, { bold: true, color: "#ef4444" }, "MISS"),
|
|
32
|
+
React.createElement(Ink.Text, null, " "),
|
|
33
|
+
React.createElement(Ink.Text, { dimColor: true }, branch)));
|
|
34
|
+
const pr = JSON.parse(result.stdout);
|
|
35
|
+
actions.set((state) => {
|
|
36
|
+
state.pr[pr.headRefName] = pr;
|
|
37
|
+
});
|
|
38
|
+
return pr;
|
|
39
|
+
}
|
|
40
|
+
export async function pr_create(branch, base) {
|
|
41
|
+
await cli(`gh pr create --fill --head ${branch} --base ${base}`);
|
|
42
|
+
}
|
|
43
|
+
export async function pr_base(branch, base) {
|
|
44
|
+
await cli(`gh pr edit ${branch} --base ${base}`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { invariant } from "./invariant.js";
|
|
4
|
+
export function is_command_available(command) {
|
|
5
|
+
const PATH = process.env["PATH"];
|
|
6
|
+
invariant(PATH, "PATH env must exist");
|
|
7
|
+
const path_list = PATH.split(path.delimiter);
|
|
8
|
+
for (const dir of path_list) {
|
|
9
|
+
const full_path = path.join(dir, command);
|
|
10
|
+
if (fs.existsSync(full_path)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function serialize(obj) {
|
|
2
|
+
if (obj instanceof Map) {
|
|
3
|
+
return {
|
|
4
|
+
_type: "Map",
|
|
5
|
+
_value: Array.from(obj.entries()).map(([k, v]) => [k, serialize(v)]),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
else if (Array.isArray(obj)) {
|
|
9
|
+
return obj.map(serialize);
|
|
10
|
+
}
|
|
11
|
+
else if (obj !== null && typeof obj === "object") {
|
|
12
|
+
const serializedObj = {};
|
|
13
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
14
|
+
serializedObj[key] = serialize(value);
|
|
15
|
+
}
|
|
16
|
+
return serializedObj;
|
|
17
|
+
}
|
|
18
|
+
return obj;
|
|
19
|
+
}
|
|
20
|
+
export function deserialize(obj) {
|
|
21
|
+
if (obj && obj._type === "Map") {
|
|
22
|
+
return new Map(obj._value.map(([k, v]) => [k, deserialize(v)]));
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(obj)) {
|
|
25
|
+
return obj.map(deserialize);
|
|
26
|
+
}
|
|
27
|
+
else if (obj !== null && typeof obj === "object") {
|
|
28
|
+
const deserializedObj = {};
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
+
deserializedObj[key] = deserialize(value);
|
|
31
|
+
}
|
|
32
|
+
return deserializedObj;
|
|
33
|
+
}
|
|
34
|
+
return obj;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { invariant } from "./invariant.js";
|
|
2
|
+
export function match_group(value, re, group) {
|
|
3
|
+
const match = value.match(re);
|
|
4
|
+
const debug = `[${value}.match(${re})]`;
|
|
5
|
+
invariant(match?.groups, `match.groups must exist ${debug}`);
|
|
6
|
+
const result = match?.groups[group];
|
|
7
|
+
invariant(result, `match.groups must contain [${group}] ${debug}`);
|
|
8
|
+
return result;
|
|
9
|
+
}
|