git-stack-cli 1.0.1 → 1.0.3
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 +1 -1
- package/package.json +13 -7
- package/rollup.config.mjs +46 -0
- package/scripts/.eslintrc.cjs +61 -0
- package/scripts/core/file.ts +32 -0
- package/scripts/core/spawn.ts +41 -0
- package/scripts/npm-prepublishOnly.ts +8 -0
- package/scripts/prepare-standalone.ts +59 -0
- package/scripts/release-brew.ts +105 -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,212 @@
|
|
|
1
|
+
import { Store } from "~/app/Store";
|
|
2
|
+
import * as Metadata from "~/core/Metadata";
|
|
3
|
+
import { cli } from "~/core/cli";
|
|
4
|
+
import * as github from "~/core/github";
|
|
5
|
+
|
|
6
|
+
export type CommitMetadata = Awaited<ReturnType<typeof commit>>;
|
|
7
|
+
export type CommitRange = Awaited<ReturnType<typeof range>>;
|
|
8
|
+
|
|
9
|
+
type PullRequest = NonNullable<Awaited<ReturnType<typeof github.pr_status>>>;
|
|
10
|
+
|
|
11
|
+
type CommitGroup = {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
pr: null | PullRequest;
|
|
15
|
+
base: null | string;
|
|
16
|
+
dirty: boolean;
|
|
17
|
+
commits: Array<CommitMetadata>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type SimpleGroup = { id: string; title: string };
|
|
21
|
+
type CommitGroupMap = { [sha: string]: SimpleGroup };
|
|
22
|
+
|
|
23
|
+
export async function range(commit_group_map?: CommitGroupMap) {
|
|
24
|
+
const master_branch = Store.getState().master_branch;
|
|
25
|
+
|
|
26
|
+
// gather all open prs in repo first
|
|
27
|
+
// cheaper query to populate cache
|
|
28
|
+
await github.pr_list();
|
|
29
|
+
|
|
30
|
+
const commit_list = await get_commit_list();
|
|
31
|
+
|
|
32
|
+
const pr_lookup: Record<string, void | PullRequest> = {};
|
|
33
|
+
|
|
34
|
+
let invalid = false;
|
|
35
|
+
const group_map = new Map<string, CommitGroup>();
|
|
36
|
+
|
|
37
|
+
for (const commit of commit_list) {
|
|
38
|
+
let id = commit.branch_id;
|
|
39
|
+
let title = id;
|
|
40
|
+
|
|
41
|
+
// use commit map if provided (via select commit ranges)
|
|
42
|
+
if (commit_group_map) {
|
|
43
|
+
const group = commit_group_map[commit.sha];
|
|
44
|
+
|
|
45
|
+
if (group) {
|
|
46
|
+
id = group.id;
|
|
47
|
+
title = group.title;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!id) {
|
|
52
|
+
// console.debug("INVALID", "MISSING ID", commit.message);
|
|
53
|
+
invalid = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (id) {
|
|
57
|
+
const group_key_list = Array.from(group_map.keys());
|
|
58
|
+
const last_key = group_key_list[group_key_list.length - 1];
|
|
59
|
+
|
|
60
|
+
if (group_map.has(id) && last_key !== id) {
|
|
61
|
+
// if we've seen this id before and it's not
|
|
62
|
+
// the last added key then we are out of order
|
|
63
|
+
// console.debug("INVALID", "OUT OF ORDER", commit.message, id);
|
|
64
|
+
invalid = true;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// console.debug("INVALID", "NEW COMMIT", { commit });
|
|
68
|
+
invalid = true;
|
|
69
|
+
|
|
70
|
+
id = UNASSIGNED;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!title) {
|
|
74
|
+
title = id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const group = group_map.get(id) || {
|
|
78
|
+
id,
|
|
79
|
+
title,
|
|
80
|
+
pr: null,
|
|
81
|
+
base: null,
|
|
82
|
+
dirty: false,
|
|
83
|
+
commits: [],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
group.commits.push(commit);
|
|
87
|
+
group_map.set(id, group);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// check each group for dirty state and base
|
|
91
|
+
const group_value_list = Array.from(group_map.values());
|
|
92
|
+
|
|
93
|
+
const group_list = [];
|
|
94
|
+
let unassigned_group;
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < group_value_list.length; i++) {
|
|
97
|
+
const group = group_value_list[i];
|
|
98
|
+
|
|
99
|
+
if (group.id !== UNASSIGNED) {
|
|
100
|
+
const pr_result = await github.pr_status(group.id);
|
|
101
|
+
|
|
102
|
+
if (pr_result && pr_result.state !== "CLOSED") {
|
|
103
|
+
group.pr = pr_result;
|
|
104
|
+
pr_lookup[group.id] = pr_result;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// console.debug("group", group.pr?.title.substring(0, 40));
|
|
109
|
+
// console.debug(" ", "id", group.id);
|
|
110
|
+
|
|
111
|
+
if (group.id === UNASSIGNED) {
|
|
112
|
+
unassigned_group = group;
|
|
113
|
+
} else {
|
|
114
|
+
group_list.push(group);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (i === 0) {
|
|
118
|
+
group.base = master_branch;
|
|
119
|
+
} else {
|
|
120
|
+
const last_group = group_value_list[i - 1];
|
|
121
|
+
// console.debug(" ", "last_group", last_group.pr?.title.substring(0, 40));
|
|
122
|
+
// console.debug(" ", "last_group.id", last_group.id);
|
|
123
|
+
|
|
124
|
+
// null out base when unassigned and after unassigned
|
|
125
|
+
if (group.id === UNASSIGNED) {
|
|
126
|
+
group.base = null;
|
|
127
|
+
} else if (last_group.base === null) {
|
|
128
|
+
group.base = null;
|
|
129
|
+
} else {
|
|
130
|
+
group.base = last_group.id;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// console.debug(" ", "group.base", group.base);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!group.pr) {
|
|
137
|
+
group.dirty = true;
|
|
138
|
+
} else if (group.pr.commits.length !== group.commits.length) {
|
|
139
|
+
group.dirty = true;
|
|
140
|
+
} else if (group.pr.baseRefName !== group.base) {
|
|
141
|
+
group.dirty = true;
|
|
142
|
+
} else {
|
|
143
|
+
for (let i = 0; i < group.pr.commits.length; i++) {
|
|
144
|
+
const pr_commit = group.pr.commits[i];
|
|
145
|
+
const local_commit = group.commits[i];
|
|
146
|
+
|
|
147
|
+
if (pr_commit.oid !== local_commit.sha) {
|
|
148
|
+
group.dirty = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// console.debug(" ", "group.dirty", group.dirty);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// reverse group_list to match git log
|
|
157
|
+
group_list.reverse();
|
|
158
|
+
|
|
159
|
+
// insert unassigned group at front
|
|
160
|
+
if (unassigned_group) {
|
|
161
|
+
group_list.unshift(unassigned_group);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { invalid, group_list, commit_list, pr_lookup, UNASSIGNED };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function get_commit_list() {
|
|
168
|
+
const log_result = await cli(
|
|
169
|
+
`git log master..HEAD --oneline --format=%H --color=never`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (!log_result.stdout) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const sha_list = lines(log_result.stdout).reverse();
|
|
177
|
+
|
|
178
|
+
const commit_metadata_list = [];
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < sha_list.length; i++) {
|
|
181
|
+
const sha = sha_list[i];
|
|
182
|
+
const commit_metadata = await commit(sha);
|
|
183
|
+
commit_metadata_list.push(commit_metadata);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return commit_metadata_list;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function commit(sha: string) {
|
|
190
|
+
const full_message = (await cli(`git show -s --format=%B ${sha}`)).stdout;
|
|
191
|
+
const branch_id = await Metadata.read(full_message);
|
|
192
|
+
const subject_line = get_subject_line(full_message);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
sha,
|
|
196
|
+
full_message,
|
|
197
|
+
subject_line,
|
|
198
|
+
branch_id,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function get_subject_line(message: string) {
|
|
203
|
+
const line_list = lines(message);
|
|
204
|
+
const first_line = line_list[0];
|
|
205
|
+
return Metadata.remove(first_line);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function lines(value: string) {
|
|
209
|
+
return value.split("\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const UNASSIGNED = "unassigned";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import * as Metadata from "~/core/Metadata";
|
|
4
|
+
|
|
5
|
+
test("read handles bulleted lists", () => {
|
|
6
|
+
const body = [
|
|
7
|
+
"[feat] implement various features",
|
|
8
|
+
"",
|
|
9
|
+
"- keyboard modality escape key",
|
|
10
|
+
"- centralize settings",
|
|
11
|
+
"- move logic inside if branch",
|
|
12
|
+
"",
|
|
13
|
+
"git-stack-id: DdKIFyufW",
|
|
14
|
+
].join("\n");
|
|
15
|
+
|
|
16
|
+
expect(Metadata.read(body)).toEqual("DdKIFyufW");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("write handles bulleted lists", () => {
|
|
20
|
+
const body = [
|
|
21
|
+
"[feat] implement various features",
|
|
22
|
+
"",
|
|
23
|
+
"- keyboard modality escape key",
|
|
24
|
+
"- centralize settings",
|
|
25
|
+
"- move logic inside if branch",
|
|
26
|
+
"",
|
|
27
|
+
"git-stack-id: DdKIFyufW",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
30
|
+
expect(Metadata.write(body, "abcd1234")).toEqual(
|
|
31
|
+
[
|
|
32
|
+
"[feat] implement various features",
|
|
33
|
+
"",
|
|
34
|
+
"- keyboard modality escape key",
|
|
35
|
+
"- centralize settings",
|
|
36
|
+
"- move logic inside if branch",
|
|
37
|
+
"",
|
|
38
|
+
"git-stack-id: abcd1234",
|
|
39
|
+
].join("\n")
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { invariant } from "~/core/invariant";
|
|
2
|
+
import { safe_quote } from "~/core/safe_quote";
|
|
3
|
+
|
|
4
|
+
export function write(message: string, stack_id: string) {
|
|
5
|
+
let result = message;
|
|
6
|
+
|
|
7
|
+
// escape double-quote for cli
|
|
8
|
+
result = safe_quote(result);
|
|
9
|
+
|
|
10
|
+
// remove any previous metadata lines
|
|
11
|
+
result = remove(result);
|
|
12
|
+
|
|
13
|
+
const line_list = [result, "", TEMPLATE.stack_id(stack_id)];
|
|
14
|
+
const new_message = line_list.join("\n");
|
|
15
|
+
|
|
16
|
+
return new_message;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function read(message: string): null | string {
|
|
20
|
+
const match = message.match(RE.stack_id);
|
|
21
|
+
|
|
22
|
+
if (!match?.groups) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const id = match.groups["id"];
|
|
27
|
+
invariant(id, "id must exist");
|
|
28
|
+
|
|
29
|
+
return id;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function remove(message: string) {
|
|
33
|
+
let result = message;
|
|
34
|
+
|
|
35
|
+
// remove metadata
|
|
36
|
+
result = result.replace(new RegExp(RE.stack_id, "gmi"), "");
|
|
37
|
+
|
|
38
|
+
result = result.trimEnd();
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const TEMPLATE = {
|
|
44
|
+
stack_id(id: string) {
|
|
45
|
+
return `git-stack-id: ${id}`;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const RE = {
|
|
50
|
+
stack_id: new RegExp(TEMPLATE.stack_id("(?<id>[a-z0-9-+=]+)"), "i"),
|
|
51
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import * as StackSummaryTable from "~/core/StackSummaryTable";
|
|
4
|
+
|
|
5
|
+
test("blank", () => {
|
|
6
|
+
const output = StackSummaryTable.write({
|
|
7
|
+
body: "",
|
|
8
|
+
pr_url_list: [],
|
|
9
|
+
selected_url: "",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(output).toBe("");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("no prs does not modify body", () => {
|
|
16
|
+
const args = {
|
|
17
|
+
body: [
|
|
18
|
+
"## Problem,",
|
|
19
|
+
"",
|
|
20
|
+
",Description of the problem,",
|
|
21
|
+
"",
|
|
22
|
+
",## Solution,",
|
|
23
|
+
"",
|
|
24
|
+
",Solved problem by doing x, y, z.",
|
|
25
|
+
].join("\n"),
|
|
26
|
+
pr_url_list: [],
|
|
27
|
+
selected_url: "",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const output = StackSummaryTable.write(args);
|
|
31
|
+
|
|
32
|
+
expect(output).toBe(args.body);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("handles bulleted lists", () => {
|
|
36
|
+
const body = [
|
|
37
|
+
"## Problem",
|
|
38
|
+
"",
|
|
39
|
+
"Description of the problem",
|
|
40
|
+
"",
|
|
41
|
+
"## Solution",
|
|
42
|
+
"",
|
|
43
|
+
"- keyboard modality escape key",
|
|
44
|
+
"- centralize settings",
|
|
45
|
+
"- move logic inside if branch",
|
|
46
|
+
].join("\n");
|
|
47
|
+
|
|
48
|
+
const args = {
|
|
49
|
+
body,
|
|
50
|
+
pr_url_list: [],
|
|
51
|
+
selected_url: "",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const output = StackSummaryTable.write(args);
|
|
55
|
+
|
|
56
|
+
expect(output).toBe(args.body);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("builds list of prs with selected emoji", () => {
|
|
60
|
+
const args = {
|
|
61
|
+
body: [
|
|
62
|
+
"## Problem,",
|
|
63
|
+
"",
|
|
64
|
+
",Description of the problem,",
|
|
65
|
+
"",
|
|
66
|
+
",## Solution,",
|
|
67
|
+
"",
|
|
68
|
+
",Solved problem by doing x, y, z.",
|
|
69
|
+
].join("\n"),
|
|
70
|
+
pr_url_list: [
|
|
71
|
+
"https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
72
|
+
"https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
73
|
+
],
|
|
74
|
+
selected_url: "https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const output = StackSummaryTable.write(args);
|
|
78
|
+
|
|
79
|
+
expect(output.split("\n")).toEqual([
|
|
80
|
+
...args.body.split("\n"),
|
|
81
|
+
"",
|
|
82
|
+
"#### git stack",
|
|
83
|
+
"- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
84
|
+
"- 👉 `1` https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("can parse stack table from body", () => {
|
|
89
|
+
const body_line_list = [
|
|
90
|
+
"",
|
|
91
|
+
"",
|
|
92
|
+
"#### git stack",
|
|
93
|
+
"- invalid line that will be dropped",
|
|
94
|
+
"- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
95
|
+
"- 👉 `1` https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const parsed = StackSummaryTable.parse(body_line_list.join("\n"));
|
|
99
|
+
|
|
100
|
+
expect(Array.from(parsed.entries())).toEqual([
|
|
101
|
+
[
|
|
102
|
+
"https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
103
|
+
{
|
|
104
|
+
icon: "⏳",
|
|
105
|
+
num: "2",
|
|
106
|
+
pr_url: "https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
111
|
+
{
|
|
112
|
+
icon: "👉",
|
|
113
|
+
num: "1",
|
|
114
|
+
pr_url: "https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("persists removed pr urls from previous stack table", () => {
|
|
121
|
+
const args = {
|
|
122
|
+
body: [
|
|
123
|
+
"Summary of problem",
|
|
124
|
+
"",
|
|
125
|
+
"#### git stack",
|
|
126
|
+
"- 👉 `3` https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
127
|
+
"- ⏳ `2` https://github.com/magus/git-multi-diff-playground/pull/44",
|
|
128
|
+
"- ⏳ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
|
|
131
|
+
pr_url_list: [
|
|
132
|
+
"https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
133
|
+
"https://github.com/magus/git-multi-diff-playground/pull/54",
|
|
134
|
+
"https://github.com/magus/git-multi-diff-playground/pull/61",
|
|
135
|
+
],
|
|
136
|
+
|
|
137
|
+
selected_url: "https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const output = StackSummaryTable.write(args);
|
|
141
|
+
|
|
142
|
+
expect(output.split("\n")).toEqual([
|
|
143
|
+
"Summary of problem",
|
|
144
|
+
"",
|
|
145
|
+
"#### git stack",
|
|
146
|
+
"- ⏳ `5` https://github.com/magus/git-multi-diff-playground/pull/61",
|
|
147
|
+
"- ⏳ `4` https://github.com/magus/git-multi-diff-playground/pull/54",
|
|
148
|
+
"- 👉 `3` https://github.com/magus/git-multi-diff-playground/pull/47",
|
|
149
|
+
"- ✅ `2` https://github.com/magus/git-multi-diff-playground/pull/44",
|
|
150
|
+
"- ✅ `1` https://github.com/magus/git-multi-diff-playground/pull/43",
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// run again on the output to make sure it doesn't change
|
|
154
|
+
const rerun_output = StackSummaryTable.write({ ...args, body: output });
|
|
155
|
+
|
|
156
|
+
expect(rerun_output).toBe(output);
|
|
157
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
type WriteArgs = {
|
|
2
|
+
body: string;
|
|
3
|
+
pr_url_list: Array<string>;
|
|
4
|
+
selected_url: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function write(args: WriteArgs) {
|
|
8
|
+
const stack_table = table(args);
|
|
9
|
+
|
|
10
|
+
let result = args.body;
|
|
11
|
+
|
|
12
|
+
if (RE.stack_table.test(result)) {
|
|
13
|
+
// replace stack table
|
|
14
|
+
result = result.replace(RE.stack_table, stack_table);
|
|
15
|
+
} else {
|
|
16
|
+
// append stack table
|
|
17
|
+
result = `${result}\n\n${stack_table}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
result = result.trimEnd();
|
|
21
|
+
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function table(args: WriteArgs) {
|
|
26
|
+
const stack_pr_url_list = [...args.pr_url_list];
|
|
27
|
+
const old_stack = parse(args.body);
|
|
28
|
+
|
|
29
|
+
// remove existing stack pr urls from the old stack pr urls
|
|
30
|
+
for (const pr_url of stack_pr_url_list) {
|
|
31
|
+
old_stack.delete(pr_url);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// add remaining old stack pr urls to the front of stack pr url list
|
|
35
|
+
for (const pr_url of old_stack.keys()) {
|
|
36
|
+
stack_pr_url_list.unshift(pr_url);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const stack_list = [];
|
|
40
|
+
const num_digits = String(stack_pr_url_list.length).length;
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < stack_pr_url_list.length; i++) {
|
|
43
|
+
const pr_url = stack_pr_url_list[i];
|
|
44
|
+
|
|
45
|
+
const selected = args.selected_url === pr_url;
|
|
46
|
+
|
|
47
|
+
let icon;
|
|
48
|
+
if (old_stack.has(pr_url)) {
|
|
49
|
+
icon = "✅";
|
|
50
|
+
} else if (selected) {
|
|
51
|
+
icon = "👉";
|
|
52
|
+
} else {
|
|
53
|
+
icon = "⏳";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const num = String(i + 1).padStart(num_digits, "0");
|
|
57
|
+
|
|
58
|
+
stack_list.push(TEMPLATE.row({ icon, num, pr_url }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!stack_list.length) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// reverse order of pr list to match the order of git stack
|
|
66
|
+
stack_list.reverse();
|
|
67
|
+
|
|
68
|
+
return TEMPLATE.stack_table(["", ...stack_list, "", ""].join("\n"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parse(body: string): Map<string, StackTableRow> {
|
|
72
|
+
const stack_table_match = body.match(RE.stack_table);
|
|
73
|
+
|
|
74
|
+
if (!stack_table_match?.groups) {
|
|
75
|
+
return new Map();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rows_string = stack_table_match.groups["rows"];
|
|
79
|
+
const row_list = rows_string.split("\n");
|
|
80
|
+
|
|
81
|
+
const result = new Map<string, StackTableRow>();
|
|
82
|
+
|
|
83
|
+
for (const row of row_list) {
|
|
84
|
+
const row_match = row.match(RE.row);
|
|
85
|
+
const parsed_row = row_match?.groups as StackTableRow;
|
|
86
|
+
|
|
87
|
+
if (!parsed_row) {
|
|
88
|
+
// skip invalid row
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result.set(parsed_row.pr_url, parsed_row);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const TEMPLATE = {
|
|
99
|
+
stack_table(rows: string) {
|
|
100
|
+
return `#### git stack${rows}`;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
row(args: StackTableRow) {
|
|
104
|
+
return `- ${args.icon} \`${args.num}\` ${args.pr_url}`;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const RE = {
|
|
109
|
+
// https://regex101.com/r/kqB9Ft/1
|
|
110
|
+
stack_table: new RegExp(
|
|
111
|
+
TEMPLATE.stack_table("\\s+(?<rows>(?:- [^\r^\n]*(?:[\r\n]+)?)+)")
|
|
112
|
+
),
|
|
113
|
+
|
|
114
|
+
row: new RegExp(
|
|
115
|
+
TEMPLATE.row({
|
|
116
|
+
icon: "(?<icon>.+)",
|
|
117
|
+
num: "(?<num>\\d+)",
|
|
118
|
+
pr_url: "(?<pr_url>.+)",
|
|
119
|
+
})
|
|
120
|
+
),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type StackTableRow = {
|
|
124
|
+
icon: string;
|
|
125
|
+
num: string;
|
|
126
|
+
pr_url: string;
|
|
127
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function Timer() {
|
|
2
|
+
const start_time = Date.now();
|
|
3
|
+
|
|
4
|
+
function duration() {
|
|
5
|
+
const duration_ms = Date.now() - start_time;
|
|
6
|
+
|
|
7
|
+
if (duration_ms < MS.second) {
|
|
8
|
+
return `${duration_ms}ms`;
|
|
9
|
+
} else if (duration_ms < MS.minute) {
|
|
10
|
+
const seconds = duration_ms / MS.second;
|
|
11
|
+
return `${seconds.toFixed(1)}s`;
|
|
12
|
+
} else if (duration_ms < MS.hour) {
|
|
13
|
+
const minutes = duration_ms / MS.minute;
|
|
14
|
+
return `${minutes.toFixed(1)}min`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hours = duration_ms / MS.hour;
|
|
18
|
+
return `${hours.toFixed(1)}min`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { duration, start_time };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MS = (function () {
|
|
25
|
+
const millisecond = 1;
|
|
26
|
+
const second = millisecond * 1000;
|
|
27
|
+
const minute = second * 60;
|
|
28
|
+
const hour = minute * 60;
|
|
29
|
+
const day = hour * 24;
|
|
30
|
+
const week = day * 7;
|
|
31
|
+
const month = day * 30;
|
|
32
|
+
const year = day * 365;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
millisecond,
|
|
36
|
+
second,
|
|
37
|
+
minute,
|
|
38
|
+
hour,
|
|
39
|
+
day,
|
|
40
|
+
week,
|
|
41
|
+
month,
|
|
42
|
+
year,
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type Status = "pending" | "success" | "error";
|
|
2
|
+
|
|
3
|
+
type Cacheable<T> = () => Promise<T>;
|
|
4
|
+
|
|
5
|
+
export function cache<T, E>(cacheable: Cacheable<T>) {
|
|
6
|
+
let status: Status = "pending";
|
|
7
|
+
|
|
8
|
+
let response: void | T | E;
|
|
9
|
+
let suspender: void | Promise<void>;
|
|
10
|
+
|
|
11
|
+
function reset() {
|
|
12
|
+
status = "pending";
|
|
13
|
+
response = undefined;
|
|
14
|
+
suspender = undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function check() {
|
|
18
|
+
return status;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function read() {
|
|
22
|
+
// cacheable is a function to allow deferred reads
|
|
23
|
+
// this will call cacheable to kickoff async promise
|
|
24
|
+
if (!suspender) {
|
|
25
|
+
suspender = Promise.resolve().then(() => {
|
|
26
|
+
cacheable()
|
|
27
|
+
.then((res: T) => {
|
|
28
|
+
status = "success";
|
|
29
|
+
response = res;
|
|
30
|
+
})
|
|
31
|
+
.catch((err: E) => {
|
|
32
|
+
status = "error";
|
|
33
|
+
response = err;
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
switch (status) {
|
|
39
|
+
case "pending":
|
|
40
|
+
throw suspender;
|
|
41
|
+
case "error":
|
|
42
|
+
throw response;
|
|
43
|
+
default:
|
|
44
|
+
return response;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { reset, check, read };
|
|
49
|
+
}
|