git-stack-cli 0.3.0 → 0.4.2
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 +2 -54
- package/dist/__fixtures__/metadata.js +399 -475
- package/dist/app/App copy.js +30 -0
- package/dist/app/App.js +9 -3
- package/dist/app/AutoUpdate.js +147 -0
- package/dist/app/Debug.js +4 -4
- package/dist/app/FormatText.js +9 -0
- package/dist/app/GatherMetadata copy.js +33 -29
- package/dist/app/GatherMetadata.js +30 -39
- package/dist/app/Input.js +15 -0
- package/dist/app/LocalCommitStatus.js +42 -0
- package/dist/app/LocalMergeRebase.js +113 -0
- package/dist/app/ManualRebase copy.js +127 -0
- package/dist/app/ManualRebase.js +23 -12
- package/dist/app/MultiSelect.js +2 -2
- package/dist/app/NPMAutoUpdate.js +34 -0
- package/dist/app/Parens copy.js +1 -1
- package/dist/app/PreLocalMergeRebase.js +21 -0
- package/dist/app/PreSelectCommitRanges copy.js +15 -23
- package/dist/app/PreSelectCommitRanges.js +10 -0
- package/dist/app/Providers.js +5 -0
- package/dist/app/SelectCommitRanges.js +95 -82
- package/dist/app/Status.js +18 -3
- package/dist/app/StatusTable.js +35 -26
- package/dist/app/Store.js +32 -6
- package/dist/app/TextInput.js +37 -0
- package/dist/app/YesNoPrompt.js +19 -5
- package/dist/app/main.js +6 -0
- package/dist/command.js +15 -34
- package/dist/core/CommitMetadata.js +19 -5
- package/dist/core/Metadata.js +4 -3
- package/dist/core/cli.js +4 -0
- package/dist/core/github.js +26 -18
- package/dist/core/id.js +61 -0
- package/dist/core/readJson.js +3 -0
- package/dist/core/read_json.js +12 -0
- package/dist/core/safe_quote.js +9 -0
- package/dist/core/short_id.js +60 -0
- package/dist/core/sleep copy.js +3 -0
- package/package.json +3 -5
package/dist/app/Status.js
CHANGED
|
@@ -14,19 +14,29 @@ async function run(args) {
|
|
|
14
14
|
const commit_range = Store.getState().commit_range;
|
|
15
15
|
invariant(commit_range, "commit_range must exist");
|
|
16
16
|
actions.output(React.createElement(StatusTable, null));
|
|
17
|
+
let needs_rebase = false;
|
|
17
18
|
let needs_update = false;
|
|
18
19
|
for (const group of commit_range.group_list) {
|
|
19
20
|
if (group.dirty) {
|
|
20
21
|
needs_update = true;
|
|
21
|
-
|
|
22
|
+
}
|
|
23
|
+
if (group.pr?.state === "MERGED") {
|
|
24
|
+
needs_rebase = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (let i = 0; i < commit_range.commit_list.length; i++) {
|
|
28
|
+
const commit = commit_range.commit_list[i];
|
|
29
|
+
const commit_pr = commit_range.pr_map.get(commit.branch_id || "");
|
|
30
|
+
if (commit.branch_id && !commit_pr) {
|
|
31
|
+
needs_rebase = true;
|
|
22
32
|
}
|
|
23
33
|
}
|
|
24
34
|
if (args.argv.check) {
|
|
25
35
|
actions.exit(0);
|
|
26
36
|
}
|
|
27
|
-
else if (
|
|
37
|
+
else if (needs_rebase) {
|
|
28
38
|
Store.setState((state) => {
|
|
29
|
-
state.step = "
|
|
39
|
+
state.step = "pre-local-merge-rebase";
|
|
30
40
|
});
|
|
31
41
|
}
|
|
32
42
|
else if (needs_update) {
|
|
@@ -34,6 +44,11 @@ async function run(args) {
|
|
|
34
44
|
state.step = "pre-select-commit-ranges";
|
|
35
45
|
});
|
|
36
46
|
}
|
|
47
|
+
else if (args.argv.force) {
|
|
48
|
+
Store.setState((state) => {
|
|
49
|
+
state.step = "select-commit-ranges";
|
|
50
|
+
});
|
|
51
|
+
}
|
|
37
52
|
else {
|
|
38
53
|
actions.output(React.createElement(Ink.Text, null, "\u2705 Everything up to date."));
|
|
39
54
|
actions.output(React.createElement(Ink.Text, { color: "gray" },
|
package/dist/app/StatusTable.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as Ink from "ink";
|
|
3
|
-
import { clamp } from "../core/clamp.js";
|
|
4
3
|
import { invariant } from "../core/invariant.js";
|
|
5
4
|
import { Store } from "./Store.js";
|
|
6
5
|
export function StatusTable() {
|
|
@@ -14,6 +13,7 @@ export function StatusTable() {
|
|
|
14
13
|
status: "",
|
|
15
14
|
title: "",
|
|
16
15
|
url: "",
|
|
16
|
+
id: group.id,
|
|
17
17
|
};
|
|
18
18
|
if (group.id === commit_range.UNASSIGNED) {
|
|
19
19
|
row.icon = "⭑";
|
|
@@ -32,6 +32,10 @@ export function StatusTable() {
|
|
|
32
32
|
row.status = "SYNCED";
|
|
33
33
|
}
|
|
34
34
|
if (group.pr) {
|
|
35
|
+
if (group.pr.state === "MERGED") {
|
|
36
|
+
row.icon = "↗";
|
|
37
|
+
row.status = "MERGED";
|
|
38
|
+
}
|
|
35
39
|
row.title = group.pr.title;
|
|
36
40
|
row.count = `${group.pr.commits.length}/${group.commits.length}`;
|
|
37
41
|
row.url = group.pr.url;
|
|
@@ -44,7 +48,8 @@ export function StatusTable() {
|
|
|
44
48
|
row_list.push(row);
|
|
45
49
|
}
|
|
46
50
|
if (!row_list.length) {
|
|
47
|
-
return React.createElement(
|
|
51
|
+
return (React.createElement(Container, null,
|
|
52
|
+
React.createElement(Ink.Text, { dimColor: true }, "No data found.")));
|
|
48
53
|
}
|
|
49
54
|
// walk data and discover max width for each column
|
|
50
55
|
const sample_row = row_list[0];
|
|
@@ -62,9 +67,8 @@ export function StatusTable() {
|
|
|
62
67
|
const { stdout } = Ink.useStdout();
|
|
63
68
|
const available_width = stdout.columns;
|
|
64
69
|
const columnGap = 2;
|
|
65
|
-
const breathing_room =
|
|
66
|
-
const
|
|
67
|
-
const remaining_space = clamp(available_width -
|
|
70
|
+
const breathing_room = 0;
|
|
71
|
+
const remaining_space = available_width -
|
|
68
72
|
// icon
|
|
69
73
|
max_col_width.icon -
|
|
70
74
|
// status
|
|
@@ -73,28 +77,33 @@ export function StatusTable() {
|
|
|
73
77
|
max_col_width.count -
|
|
74
78
|
// url
|
|
75
79
|
max_col_width.url -
|
|
76
|
-
// gap * col count
|
|
77
|
-
columnGap * col_list.length -
|
|
80
|
+
// gap * col count (minus one for row.id which is not shown but used at key)
|
|
81
|
+
columnGap * (col_list.length - 1) -
|
|
78
82
|
// remove some extra space
|
|
79
|
-
breathing_room
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
breathing_room;
|
|
84
|
+
// add one for ellipsis character
|
|
85
|
+
const title_width = Math.min(max_col_width.title, remaining_space + 1);
|
|
86
|
+
// prettier-ignore
|
|
87
|
+
// console.debug({ available_width, remaining_space, title_width, max_col_width });
|
|
88
|
+
return (React.createElement(Container, null, row_list.map((row) => {
|
|
89
|
+
return (React.createElement(Ink.Box, { key: row.id,
|
|
90
|
+
// borderStyle="round"
|
|
91
|
+
flexDirection: "row", columnGap: columnGap, width: available_width },
|
|
92
|
+
React.createElement(Ink.Box, { width: max_col_width.icon },
|
|
93
|
+
React.createElement(Ink.Text, null, row.icon)),
|
|
94
|
+
React.createElement(Ink.Box, { width: max_col_width.status },
|
|
95
|
+
React.createElement(Ink.Text, null, row.status)),
|
|
96
|
+
React.createElement(Ink.Box, { width: max_col_width.count },
|
|
97
|
+
React.createElement(Ink.Text, null, row.count)),
|
|
98
|
+
React.createElement(Ink.Box, { width: title_width },
|
|
99
|
+
React.createElement(Ink.Text, { wrap: "truncate-end" }, row.title)),
|
|
100
|
+
React.createElement(Ink.Box, { width: max_col_width.url },
|
|
101
|
+
React.createElement(Ink.Text, null, row.url))));
|
|
102
|
+
})));
|
|
103
|
+
}
|
|
104
|
+
function Container(props) {
|
|
105
|
+
return (React.createElement(Ink.Box, { flexDirection: "column" },
|
|
82
106
|
React.createElement(Ink.Box, { height: 1 }),
|
|
83
|
-
|
|
84
|
-
return (React.createElement(Ink.Box, { key: row.url,
|
|
85
|
-
// borderStyle="round"
|
|
86
|
-
flexDirection: "row", columnGap: columnGap, width: available_width },
|
|
87
|
-
React.createElement(Ink.Box, { width: max_col_width.icon },
|
|
88
|
-
React.createElement(Ink.Text, null, row.icon)),
|
|
89
|
-
React.createElement(Ink.Box, { width: max_col_width.status },
|
|
90
|
-
React.createElement(Ink.Text, null, row.status)),
|
|
91
|
-
React.createElement(Ink.Box, { width: max_col_width.count },
|
|
92
|
-
React.createElement(Ink.Text, null, row.count)),
|
|
93
|
-
React.createElement(Ink.Box, { width: title_width },
|
|
94
|
-
React.createElement(Ink.Text, { wrap: "truncate-end" }, row.title)),
|
|
95
|
-
React.createElement(Ink.Box, { width: max_col_width.url },
|
|
96
|
-
React.createElement(Ink.Text, null, row.url))));
|
|
97
|
-
}),
|
|
107
|
+
props.children,
|
|
98
108
|
React.createElement(Ink.Box, { height: 1 })));
|
|
99
109
|
}
|
|
100
|
-
const MAX_TITLE_LENGTH = 50;
|
package/dist/app/Store.js
CHANGED
|
@@ -31,7 +31,17 @@ const BaseStore = createStore()(immer((set, get) => ({
|
|
|
31
31
|
},
|
|
32
32
|
newline() {
|
|
33
33
|
set((state) => {
|
|
34
|
-
state.mutate.output(state,
|
|
34
|
+
state.mutate.output(state, "");
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
json(value) {
|
|
38
|
+
set((state) => {
|
|
39
|
+
state.mutate.output(state, JSON.stringify(value, null, 2));
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
error(message) {
|
|
43
|
+
set((state) => {
|
|
44
|
+
state.mutate.output(state, React.createElement(Ink.Text, { color: "#ef4444" }, message));
|
|
35
45
|
});
|
|
36
46
|
},
|
|
37
47
|
output(node) {
|
|
@@ -40,11 +50,15 @@ const BaseStore = createStore()(immer((set, get) => ({
|
|
|
40
50
|
});
|
|
41
51
|
},
|
|
42
52
|
debug(node) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
state.mutate.output(state, node);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
53
|
+
if (get().actions.isDebug()) {
|
|
54
|
+
set((state) => {
|
|
55
|
+
state.mutate.output(state, React.createElement(Ink.Text, { dimColor: true }, node));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
isDebug() {
|
|
60
|
+
const state = get();
|
|
61
|
+
return state.select.debug(state);
|
|
48
62
|
},
|
|
49
63
|
reset_pr() {
|
|
50
64
|
set((state) => {
|
|
@@ -59,9 +73,21 @@ const BaseStore = createStore()(immer((set, get) => ({
|
|
|
59
73
|
},
|
|
60
74
|
mutate: {
|
|
61
75
|
output(state, node) {
|
|
76
|
+
switch (typeof node) {
|
|
77
|
+
case "boolean":
|
|
78
|
+
case "number":
|
|
79
|
+
case "string":
|
|
80
|
+
state.output.push(React.createElement(Ink.Text, null, String(node)));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
62
83
|
state.output.push(node);
|
|
63
84
|
},
|
|
64
85
|
},
|
|
86
|
+
select: {
|
|
87
|
+
debug(state) {
|
|
88
|
+
return state.argv?.debug || false;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
65
91
|
})));
|
|
66
92
|
function useState(selector) {
|
|
67
93
|
return useStore(BaseStore, selector);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as Ink from "ink";
|
|
3
|
+
export function TextInput(props) {
|
|
4
|
+
const [value, set_value] = React.useState(get_value(props));
|
|
5
|
+
React.useEffect(function sync_value_prop() {
|
|
6
|
+
set_value(get_value(props));
|
|
7
|
+
}, [props.value]);
|
|
8
|
+
Ink.useInput((input, key) => {
|
|
9
|
+
let next_value = value;
|
|
10
|
+
// console.debug("[useInput]", { input, key });
|
|
11
|
+
if (key.backspace || key.delete) {
|
|
12
|
+
next_value = value.slice(0, -1);
|
|
13
|
+
}
|
|
14
|
+
else if (key.return) {
|
|
15
|
+
props.onSubmit?.(next_value);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
switch (input) {
|
|
19
|
+
case "\r":
|
|
20
|
+
if (props.multiline) {
|
|
21
|
+
next_value = `${value}\n`;
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
default:
|
|
25
|
+
next_value = `${value}${input}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
set_value(next_value);
|
|
29
|
+
props.onChange?.(next_value);
|
|
30
|
+
});
|
|
31
|
+
// console.debug("[TextInput]", { value });
|
|
32
|
+
return (React.createElement(Ink.Box, { borderStyle: "single", minHeight: 1, borderColor: "yellow", borderDimColor: true },
|
|
33
|
+
React.createElement(Ink.Text, null, value || "")));
|
|
34
|
+
}
|
|
35
|
+
function get_value(props) {
|
|
36
|
+
return props.value || "";
|
|
37
|
+
}
|
package/dist/app/YesNoPrompt.js
CHANGED
|
@@ -13,13 +13,27 @@ export function YesNoPrompt(props) {
|
|
|
13
13
|
return props.onYes();
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
|
+
// prettier-ignore
|
|
17
|
+
const y = React.createElement(Ink.Text, { bold: true, color: "#22c55e" }, "Y");
|
|
18
|
+
const n = React.createElement(Ink.Text, { color: "#ef4444" }, "n");
|
|
19
|
+
let choices;
|
|
20
|
+
switch (answer) {
|
|
21
|
+
case "y":
|
|
22
|
+
choices = y;
|
|
23
|
+
break;
|
|
24
|
+
case "n":
|
|
25
|
+
choices = n;
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
choices = (React.createElement(React.Fragment, null,
|
|
29
|
+
y,
|
|
30
|
+
React.createElement(Ink.Text, null, "/"),
|
|
31
|
+
n));
|
|
32
|
+
}
|
|
16
33
|
return (React.createElement(Ink.Box, { flexDirection: "column" },
|
|
17
34
|
React.createElement(Ink.Box, null,
|
|
18
|
-
React.createElement(Ink.Text, { color: "yellow" }, props.message),
|
|
35
|
+
typeof props.message === "object" ? (props.message) : (React.createElement(Ink.Text, { color: "yellow" }, props.message)),
|
|
19
36
|
React.createElement(Ink.Text, null, " "),
|
|
20
37
|
React.createElement(Parens, null,
|
|
21
|
-
React.createElement(Ink.Text, { color: "gray" },
|
|
22
|
-
answer && answer !== "y" ? null : (React.createElement(Ink.Text, { bold: true, color: "#22c55e" }, "Y")),
|
|
23
|
-
answer ? null : React.createElement(Ink.Text, null, "/"),
|
|
24
|
-
answer && answer !== "n" ? null : (React.createElement(Ink.Text, { color: "#ef4444" }, "n")))))));
|
|
38
|
+
React.createElement(Ink.Text, { color: "gray" }, choices)))));
|
|
25
39
|
}
|
package/dist/app/main.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { assertNever } from "../core/assertNever.js";
|
|
3
3
|
import { GithubApiError } from "./GithubApiError.js";
|
|
4
|
+
import { LocalMergeRebase } from "./LocalMergeRebase.js";
|
|
4
5
|
import { ManualRebase } from "./ManualRebase.js";
|
|
5
6
|
import { PostRebaseStatus } from "./PostRebaseStatus.js";
|
|
7
|
+
import { PreLocalMergeRebase } from "./PreLocalMergeRebase.js";
|
|
6
8
|
import { PreSelectCommitRanges } from "./PreSelectCommitRanges.js";
|
|
7
9
|
import { SelectCommitRanges } from "./SelectCommitRanges.js";
|
|
8
10
|
import { Status } from "./Status.js";
|
|
@@ -16,6 +18,10 @@ export function Main() {
|
|
|
16
18
|
return null;
|
|
17
19
|
case "status":
|
|
18
20
|
return React.createElement(Status, null);
|
|
21
|
+
case "local-merge-rebase":
|
|
22
|
+
return React.createElement(LocalMergeRebase, null);
|
|
23
|
+
case "pre-local-merge-rebase":
|
|
24
|
+
return React.createElement(PreLocalMergeRebase, null);
|
|
19
25
|
case "pre-select-commit-ranges":
|
|
20
26
|
return React.createElement(PreSelectCommitRanges, null);
|
|
21
27
|
case "select-commit-ranges":
|
package/dist/command.js
CHANGED
|
@@ -1,62 +1,43 @@
|
|
|
1
1
|
import yargs from "yargs";
|
|
2
2
|
import { hideBin } from "yargs/helpers";
|
|
3
3
|
export async function command() {
|
|
4
|
-
|
|
5
|
-
.option("debug", {
|
|
6
|
-
type: "boolean",
|
|
7
|
-
description: "Enable debug mode with more options for debugging",
|
|
8
|
-
})
|
|
9
|
-
.help(false).argv;
|
|
10
|
-
if (!debug_argv.debug) {
|
|
11
|
-
return NormalMode();
|
|
12
|
-
}
|
|
13
|
-
return DebugMode();
|
|
14
|
-
}
|
|
15
|
-
function NormalMode() {
|
|
4
|
+
// https://yargs.js.org/docs/#api-reference-optionkey-opt
|
|
16
5
|
return (yargs(hideBin(process.argv))
|
|
6
|
+
.usage("Usage: git stack [options]")
|
|
17
7
|
.option("force", {
|
|
18
8
|
type: "boolean",
|
|
9
|
+
alias: ["f"],
|
|
19
10
|
description: "Force sync even if no changes are detected",
|
|
20
11
|
})
|
|
21
12
|
.option("check", {
|
|
22
13
|
type: "boolean",
|
|
14
|
+
alias: ["c"],
|
|
23
15
|
description: "Print status table without syncing",
|
|
24
16
|
})
|
|
25
|
-
.option("
|
|
17
|
+
.option("no-verify", {
|
|
26
18
|
type: "boolean",
|
|
27
|
-
description: "
|
|
28
|
-
})
|
|
29
|
-
// disallow unknown options
|
|
30
|
-
.strict()
|
|
31
|
-
.help().argv);
|
|
32
|
-
}
|
|
33
|
-
function DebugMode() {
|
|
34
|
-
return (yargs(hideBin(process.argv))
|
|
35
|
-
.option("force", {
|
|
36
|
-
type: "boolean",
|
|
37
|
-
description: "Force sync even if no changes are detected",
|
|
38
|
-
})
|
|
39
|
-
.option("check", {
|
|
40
|
-
type: "boolean",
|
|
41
|
-
description: "Print status table without syncing",
|
|
19
|
+
description: "Disable the pre-push hook, bypassing it completely",
|
|
42
20
|
})
|
|
43
21
|
.option("debug", {
|
|
44
22
|
type: "boolean",
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.option("verbose", {
|
|
48
|
-
type: "boolean",
|
|
49
|
-
description: "Log extra information during execution",
|
|
23
|
+
alias: ["verbose", "v", "d"],
|
|
24
|
+
description: "Enable debug mode with more detailed output for debugging",
|
|
50
25
|
})
|
|
51
26
|
.option("write-state-json", {
|
|
27
|
+
hidden: true,
|
|
52
28
|
type: "boolean",
|
|
53
29
|
description: "Write state to local json file for debugging",
|
|
54
30
|
})
|
|
55
31
|
.option("mock-metadata", {
|
|
32
|
+
hidden: true,
|
|
56
33
|
type: "boolean",
|
|
57
34
|
description: "Mock local store metadata for testing",
|
|
58
35
|
})
|
|
36
|
+
// do not wrap to 80 columns (yargs default)
|
|
37
|
+
// .wrap(yargs().terminalWidth()) will fill terminal (maximuize)
|
|
38
|
+
.wrap(null)
|
|
59
39
|
// disallow unknown options
|
|
60
40
|
.strict()
|
|
61
|
-
.
|
|
41
|
+
.version()
|
|
42
|
+
.help("help").argv);
|
|
62
43
|
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import * as Metadata from "./Metadata.js";
|
|
2
2
|
import { cli } from "./cli.js";
|
|
3
3
|
import * as github from "./github.js";
|
|
4
|
-
export async function range(
|
|
4
|
+
export async function range(commit_group_map) {
|
|
5
5
|
// gather all open prs in repo first
|
|
6
6
|
// cheaper query to populate cache
|
|
7
7
|
await github.pr_list();
|
|
8
8
|
const commit_list = await get_commit_list();
|
|
9
|
+
const pr_map = new Map();
|
|
9
10
|
let invalid = false;
|
|
10
11
|
const group_map = new Map();
|
|
11
12
|
for (const commit of commit_list) {
|
|
12
13
|
let id = commit.branch_id;
|
|
14
|
+
let title = id;
|
|
13
15
|
// use commit map if provided (via select commit ranges)
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
+
if (commit_group_map) {
|
|
17
|
+
const group = commit_group_map[commit.sha];
|
|
18
|
+
if (group) {
|
|
19
|
+
id = group.id;
|
|
20
|
+
title = group.title;
|
|
21
|
+
}
|
|
16
22
|
}
|
|
17
23
|
if (!id) {
|
|
18
24
|
// console.debug("INVALID", "MISSING ID", commit.message);
|
|
@@ -33,8 +39,12 @@ export async function range(commit_map) {
|
|
|
33
39
|
invalid = true;
|
|
34
40
|
id = UNASSIGNED;
|
|
35
41
|
}
|
|
42
|
+
if (!title) {
|
|
43
|
+
title = id;
|
|
44
|
+
}
|
|
36
45
|
const group = group_map.get(id) || {
|
|
37
46
|
id,
|
|
47
|
+
title,
|
|
38
48
|
pr: null,
|
|
39
49
|
base: null,
|
|
40
50
|
dirty: false,
|
|
@@ -51,8 +61,9 @@ export async function range(commit_map) {
|
|
|
51
61
|
const group = group_value_list[i];
|
|
52
62
|
if (group.id !== UNASSIGNED) {
|
|
53
63
|
const pr_result = await github.pr_status(group.id);
|
|
54
|
-
if (pr_result && pr_result.state
|
|
64
|
+
if (pr_result && pr_result.state !== "CLOSED") {
|
|
55
65
|
group.pr = pr_result;
|
|
66
|
+
pr_map.set(group.id, pr_result);
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
// console.debug("group", group.pr?.title.substring(0, 40));
|
|
@@ -108,10 +119,13 @@ export async function range(commit_map) {
|
|
|
108
119
|
if (unassigned_group) {
|
|
109
120
|
group_list.unshift(unassigned_group);
|
|
110
121
|
}
|
|
111
|
-
return { invalid, group_list, commit_list, UNASSIGNED };
|
|
122
|
+
return { invalid, group_list, commit_list, pr_map, UNASSIGNED };
|
|
112
123
|
}
|
|
113
124
|
async function get_commit_list() {
|
|
114
125
|
const log_result = await cli(`git log master..HEAD --oneline --format=%H --color=never`);
|
|
126
|
+
if (!log_result.stdout) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
115
129
|
const sha_list = lines(log_result.stdout).reverse();
|
|
116
130
|
const commit_metadata_list = [];
|
|
117
131
|
for (let i = 0; i < sha_list.length; i++) {
|
package/dist/core/Metadata.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { invariant } from "../core/invariant.js";
|
|
2
|
+
import { safe_quote } from "../core/safe_quote.js";
|
|
2
3
|
export function write(message, branch_id) {
|
|
3
4
|
let result = message;
|
|
4
5
|
// escape double-quote for cli
|
|
5
|
-
result = result
|
|
6
|
+
result = safe_quote(result);
|
|
6
7
|
// remove any previous metadata lines
|
|
7
8
|
result = remove(result);
|
|
8
9
|
const line_list = [result, "", TEMPLATE.branch_id(branch_id)];
|
|
@@ -27,10 +28,10 @@ export function remove(message) {
|
|
|
27
28
|
}
|
|
28
29
|
const TEMPLATE = {
|
|
29
30
|
branch_id(id) {
|
|
30
|
-
return `git-
|
|
31
|
+
return `git-stack-id: ${id}`;
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
const RE = {
|
|
34
35
|
all_double_quote: /"/g,
|
|
35
|
-
branch_id: new RegExp(TEMPLATE.branch_id("(?<id>[a-z0-9
|
|
36
|
+
branch_id: new RegExp(TEMPLATE.branch_id("(?<id>[a-z0-9-+]+)"), "i"),
|
|
36
37
|
};
|
package/dist/core/cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as child from "node:child_process";
|
|
2
|
+
import { Store } from "../app/Store.js";
|
|
2
3
|
export async function cli(command, unsafe_options) {
|
|
4
|
+
const state = Store.getState();
|
|
3
5
|
const options = Object.assign({}, unsafe_options);
|
|
4
6
|
return new Promise((resolve, reject) => {
|
|
5
7
|
const childProcess = child.spawn("sh", ["-c", command], options);
|
|
@@ -26,6 +28,8 @@ export async function cli(command, unsafe_options) {
|
|
|
26
28
|
stderr: stderr.trimEnd(),
|
|
27
29
|
output: output.trimEnd(),
|
|
28
30
|
};
|
|
31
|
+
state.actions.debug(`$ ${command}`);
|
|
32
|
+
state.actions.debug(result.output);
|
|
29
33
|
resolve(result);
|
|
30
34
|
}
|
|
31
35
|
});
|
package/dist/core/github.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Brackets } from "../app/Brackets.js";
|
|
|
4
4
|
import { Store } from "../app/Store.js";
|
|
5
5
|
import { cli } from "./cli.js";
|
|
6
6
|
import { invariant } from "./invariant.js";
|
|
7
|
+
import { safe_quote } from "./safe_quote.js";
|
|
7
8
|
// prettier-ignore
|
|
8
9
|
const JSON_FIELDS = "--json number,state,baseRefName,headRefName,commits,title,url";
|
|
9
10
|
export async function pr_list() {
|
|
@@ -20,13 +21,15 @@ export async function pr_list() {
|
|
|
20
21
|
handle_error(cli_result.output);
|
|
21
22
|
}
|
|
22
23
|
const result_pr_list = JSON.parse(cli_result.stdout);
|
|
23
|
-
actions.
|
|
24
|
-
React.createElement(Ink.Text,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
if (actions.isDebug()) {
|
|
25
|
+
actions.output(React.createElement(Ink.Text, { dimColor: true },
|
|
26
|
+
React.createElement(Ink.Text, null, "Github cache "),
|
|
27
|
+
React.createElement(Ink.Text, { bold: true, color: "yellow" }, result_pr_list.length),
|
|
28
|
+
React.createElement(Ink.Text, null, " open PRs from "),
|
|
29
|
+
React.createElement(Brackets, null, repo_path),
|
|
30
|
+
React.createElement(Ink.Text, null, " authored by "),
|
|
31
|
+
React.createElement(Brackets, null, username)));
|
|
32
|
+
}
|
|
30
33
|
actions.set((state) => {
|
|
31
34
|
for (const pr of result_pr_list) {
|
|
32
35
|
state.pr[pr.headRefName] = pr;
|
|
@@ -43,20 +46,24 @@ export async function pr_status(branch) {
|
|
|
43
46
|
invariant(repo_path, "repo_path must exist");
|
|
44
47
|
const cache = state.pr[branch];
|
|
45
48
|
if (cache) {
|
|
46
|
-
actions.
|
|
49
|
+
if (actions.isDebug()) {
|
|
50
|
+
actions.output(React.createElement(Ink.Text, null,
|
|
51
|
+
React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
|
|
52
|
+
React.createElement(Ink.Text, null, " "),
|
|
53
|
+
React.createElement(Ink.Text, { bold: true, color: "#22c55e" }, "HIT "),
|
|
54
|
+
React.createElement(Ink.Text, null, " "),
|
|
55
|
+
React.createElement(Ink.Text, { dimColor: true }, branch)));
|
|
56
|
+
}
|
|
57
|
+
return cache;
|
|
58
|
+
}
|
|
59
|
+
if (actions.isDebug()) {
|
|
60
|
+
actions.output(React.createElement(Ink.Text, null,
|
|
47
61
|
React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
|
|
48
62
|
React.createElement(Ink.Text, null, " "),
|
|
49
|
-
React.createElement(Ink.Text, { bold: true, color: "#
|
|
63
|
+
React.createElement(Ink.Text, { bold: true, color: "#ef4444" }, "MISS"),
|
|
50
64
|
React.createElement(Ink.Text, null, " "),
|
|
51
65
|
React.createElement(Ink.Text, { dimColor: true }, branch)));
|
|
52
|
-
return cache;
|
|
53
66
|
}
|
|
54
|
-
actions.debug(React.createElement(Ink.Text, null,
|
|
55
|
-
React.createElement(Ink.Text, { dimColor: true }, "Github pr_status cache"),
|
|
56
|
-
React.createElement(Ink.Text, null, " "),
|
|
57
|
-
React.createElement(Ink.Text, { bold: true, color: "#ef4444" }, "MISS"),
|
|
58
|
-
React.createElement(Ink.Text, null, " "),
|
|
59
|
-
React.createElement(Ink.Text, { dimColor: true }, branch)));
|
|
60
67
|
const cli_result = await cli(`gh pr view ${branch} --repo ${repo_path} ${JSON_FIELDS}`, {
|
|
61
68
|
ignoreExitCode: true,
|
|
62
69
|
});
|
|
@@ -70,8 +77,9 @@ export async function pr_status(branch) {
|
|
|
70
77
|
});
|
|
71
78
|
return pr;
|
|
72
79
|
}
|
|
73
|
-
export async function pr_create(
|
|
74
|
-
const
|
|
80
|
+
export async function pr_create(args) {
|
|
81
|
+
const title = safe_quote(args.title);
|
|
82
|
+
const cli_result = await cli(`gh pr create --fill --head ${args.branch} --base ${args.base} --title="${title}"`);
|
|
75
83
|
if (cli_result.code !== 0) {
|
|
76
84
|
handle_error(cli_result.output);
|
|
77
85
|
}
|
package/dist/core/id.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
// console.log(id());
|
|
3
|
+
export function id() {
|
|
4
|
+
const timestamp = Date.now();
|
|
5
|
+
// 9 223 372 036 854 775 808
|
|
6
|
+
// 9 trillion possible values
|
|
7
|
+
// (2^53) * (2^10) = 2^63 = 9,223,372,036,854,775,808
|
|
8
|
+
const js_max_bits = 53;
|
|
9
|
+
const timestamp_bits = Math.floor(Math.log2(timestamp)) + 1;
|
|
10
|
+
// padding needed to reach 53 bits
|
|
11
|
+
const padding_bits = js_max_bits - timestamp_bits;
|
|
12
|
+
// random between 0 and 2^padding_bits - 1
|
|
13
|
+
const random = crypto.randomInt(0, Math.pow(2, padding_bits));
|
|
14
|
+
// combine timestamp and random value
|
|
15
|
+
const combined = interleave_bits(timestamp, random);
|
|
16
|
+
// console.debug({ combined, timestamp, random, padding_bits, timestamp_bits });
|
|
17
|
+
return encode(combined);
|
|
18
|
+
}
|
|
19
|
+
function binary(value) {
|
|
20
|
+
return BigInt(value).toString(2);
|
|
21
|
+
}
|
|
22
|
+
function rand_index(list) {
|
|
23
|
+
return Math.floor(Math.random() * list.length);
|
|
24
|
+
}
|
|
25
|
+
function interleave_bits(a, b) {
|
|
26
|
+
const a_binary = binary(a).split("");
|
|
27
|
+
const b_binary = binary(b).split("");
|
|
28
|
+
while (b_binary.length) {
|
|
29
|
+
// pull random bit out of b_binary
|
|
30
|
+
const b_index = rand_index(b_binary);
|
|
31
|
+
const [selected] = b_binary.splice(b_index, 1);
|
|
32
|
+
// insert random bit into a_binary
|
|
33
|
+
const a_index = rand_index(a_binary);
|
|
34
|
+
a_binary.splice(a_index, 0, selected);
|
|
35
|
+
}
|
|
36
|
+
// convert binary list back to integer
|
|
37
|
+
const a_value = parseInt(a_binary.join(""), 2);
|
|
38
|
+
return a_value;
|
|
39
|
+
}
|
|
40
|
+
function encode(value) {
|
|
41
|
+
// base64 encode (64 characters)
|
|
42
|
+
// max character necessary to encode is equal to maximum number
|
|
43
|
+
// of bits in value divided by bits per character in encoding
|
|
44
|
+
//
|
|
45
|
+
// Example
|
|
46
|
+
// in base64 each characters can represent 6 bits (2^6 = 64)
|
|
47
|
+
// 53 bits / 6 bits = 8.833333333333334 characters (9 characters)
|
|
48
|
+
//
|
|
49
|
+
// prettier-ignore
|
|
50
|
+
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+";
|
|
51
|
+
const bits_per_char = Math.log2(chars.length);
|
|
52
|
+
const max_value_bits = 53;
|
|
53
|
+
const max_char_size = Math.ceil(max_value_bits / bits_per_char);
|
|
54
|
+
let result = "";
|
|
55
|
+
while (value > 0) {
|
|
56
|
+
result = chars[value % chars.length] + result;
|
|
57
|
+
value = Math.floor(value / chars.length);
|
|
58
|
+
}
|
|
59
|
+
// pad the result to necessary characters
|
|
60
|
+
return result.padStart(max_char_size, "=");
|
|
61
|
+
}
|