git-stack-cli 0.3.1 → 0.5.0
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 +0 -54
- package/dist/__fixtures__/metadata.js +399 -475
- package/dist/app/App.js +8 -3
- package/dist/app/AutoUpdate.js +147 -0
- package/dist/app/Debug.js +4 -4
- package/dist/app/FormatText.js +3 -1
- 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/SelectCommitRanges.js +86 -71
- package/dist/app/Status.js +18 -3
- package/dist/app/StatusTable.js +30 -26
- package/dist/app/Store.js +32 -6
- package/dist/app/TextInput.js +37 -0
- package/dist/app/YesNoPrompt.js +1 -1
- package/dist/app/main.js +6 -0
- package/dist/command.js +15 -34
- package/dist/core/CommitMetadata.js +18 -4
- 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 +2 -5
package/dist/app/App.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { AutoUpdate } from "./AutoUpdate.js";
|
|
2
3
|
import { Debug } from "./Debug.js";
|
|
3
4
|
import { DependencyCheck } from "./DependencyCheck.js";
|
|
4
5
|
import { GatherMetadata } from "./GatherMetadata.js";
|
|
5
6
|
import { GithubApiError } from "./GithubApiError.js";
|
|
7
|
+
import { LocalCommitStatus } from "./LocalCommitStatus.js";
|
|
6
8
|
import { Main } from "./Main.js";
|
|
7
9
|
import { Output } from "./Output.js";
|
|
8
10
|
import { Providers } from "./Providers.js";
|
|
9
11
|
import { Store } from "./Store.js";
|
|
10
12
|
export function App() {
|
|
13
|
+
const actions = Store.useActions();
|
|
11
14
|
const ink = Store.useState((state) => state.ink);
|
|
12
15
|
const argv = Store.useState((state) => state.argv);
|
|
13
16
|
if (!ink || !argv) {
|
|
@@ -24,8 +27,10 @@ export function App() {
|
|
|
24
27
|
return (React.createElement(Providers, null,
|
|
25
28
|
React.createElement(Debug, null),
|
|
26
29
|
React.createElement(Output, null),
|
|
27
|
-
!argv.
|
|
30
|
+
!argv.verbose ? null : React.createElement(GithubApiError, null),
|
|
28
31
|
React.createElement(DependencyCheck, null,
|
|
29
|
-
React.createElement(
|
|
30
|
-
React.createElement(
|
|
32
|
+
React.createElement(AutoUpdate, { name: "git-stack-cli", verbose: argv.verbose, onOutput: actions.output },
|
|
33
|
+
React.createElement(GatherMetadata, null,
|
|
34
|
+
React.createElement(LocalCommitStatus, null,
|
|
35
|
+
React.createElement(Main, null)))))));
|
|
31
36
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as Ink from "ink";
|
|
5
|
+
import { cli } from "../core/cli.js";
|
|
6
|
+
import { read_json } from "../core/read_json.js";
|
|
7
|
+
import { sleep } from "../core/sleep.js";
|
|
8
|
+
import { Brackets } from "./Brackets.js";
|
|
9
|
+
import { FormatText } from "./FormatText.js";
|
|
10
|
+
import { Parens } from "./Parens.js";
|
|
11
|
+
import { YesNoPrompt } from "./YesNoPrompt.js";
|
|
12
|
+
function reducer(state, patch) {
|
|
13
|
+
return { ...state, ...patch };
|
|
14
|
+
}
|
|
15
|
+
export function AutoUpdate(props) {
|
|
16
|
+
const props_ref = React.useRef(props);
|
|
17
|
+
props_ref.current = props;
|
|
18
|
+
const [output, set_output] = React.useState([]);
|
|
19
|
+
const [state, patch] = React.useReducer(reducer, {
|
|
20
|
+
error: null,
|
|
21
|
+
local_version: null,
|
|
22
|
+
latest_version: null,
|
|
23
|
+
status: "init",
|
|
24
|
+
});
|
|
25
|
+
function handle_output(node) {
|
|
26
|
+
if (typeof props.onOutput === "function") {
|
|
27
|
+
props.onOutput(node);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
set_output((current) => {
|
|
31
|
+
return [...current, node];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
let status = "done";
|
|
37
|
+
let local_version = null;
|
|
38
|
+
let latest_version = null;
|
|
39
|
+
async function auto_update() {
|
|
40
|
+
if (props_ref.current.verbose) {
|
|
41
|
+
handle_output(React.createElement(Ink.Text, { key: "init", dimColor: true }, "Checking for latest version..."));
|
|
42
|
+
}
|
|
43
|
+
const timeout_ms = 2 * 1000;
|
|
44
|
+
const npm_res = await Promise.race([
|
|
45
|
+
fetch(`https://registry.npmjs.org/${props.name}`),
|
|
46
|
+
sleep(timeout_ms).then(() => {
|
|
47
|
+
throw new Error("timeout");
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
const npm_json = await npm_res.json();
|
|
51
|
+
latest_version = npm_json?.["dist-tags"]?.latest;
|
|
52
|
+
if (!latest_version) {
|
|
53
|
+
throw new Error("unable to retrieve latest version from npm");
|
|
54
|
+
}
|
|
55
|
+
const script_dir = path.dirname(fs.realpathSync(process.argv[1]));
|
|
56
|
+
const package_json_path = path.join(script_dir, "..", "package.json");
|
|
57
|
+
const package_json = read_json(package_json_path);
|
|
58
|
+
if (!package_json) {
|
|
59
|
+
// unable to find read package.json, skip auto update
|
|
60
|
+
throw new Error(`unable to find read package.json [${package_json_path}]`);
|
|
61
|
+
}
|
|
62
|
+
local_version = package_json.version;
|
|
63
|
+
if (props_ref.current.verbose) {
|
|
64
|
+
handle_output(React.createElement(FormatText, { key: "versions", wrapper: React.createElement(Ink.Text, { dimColor: true }), message: "Auto update found latest version {latest_version} and current local version {local_version}", values: {
|
|
65
|
+
latest_version: React.createElement(Brackets, null, latest_version),
|
|
66
|
+
local_version: React.createElement(Parens, null, local_version),
|
|
67
|
+
} }));
|
|
68
|
+
}
|
|
69
|
+
const semver_result = semver_compare(latest_version, local_version);
|
|
70
|
+
if (semver_result === 0) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (semver_result === -1) {
|
|
74
|
+
// latest version is less than or equal to local version, skip auto update
|
|
75
|
+
throw new Error(`latest version < local_version, skipping auto update [${latest_version} < ${local_version}]`);
|
|
76
|
+
}
|
|
77
|
+
// trigger yes no prompt
|
|
78
|
+
status = "prompt";
|
|
79
|
+
}
|
|
80
|
+
const onError = props_ref.current.onError || (() => { });
|
|
81
|
+
auto_update()
|
|
82
|
+
.then(() => {
|
|
83
|
+
patch({ status, local_version, latest_version });
|
|
84
|
+
})
|
|
85
|
+
.catch((error) => {
|
|
86
|
+
patch({ status, error, local_version, latest_version });
|
|
87
|
+
onError(error);
|
|
88
|
+
if (props_ref.current.verbose) {
|
|
89
|
+
handle_output(React.createElement(Ink.Text, { key: "error", dimColor: true, color: "red" }, error?.message));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}, []);
|
|
93
|
+
const status = (function render_status() {
|
|
94
|
+
switch (state.status) {
|
|
95
|
+
case "init":
|
|
96
|
+
return null;
|
|
97
|
+
case "prompt":
|
|
98
|
+
return (React.createElement(YesNoPrompt, { message: React.createElement(Ink.Text, { color: "yellow" }, "New version available, would you like to update?"), onYes: async () => {
|
|
99
|
+
handle_output(React.createElement(FormatText, { key: "install", wrapper: React.createElement(Ink.Text, null), message: "Installing {name}@{version}...", values: {
|
|
100
|
+
name: React.createElement(Ink.Text, { color: "yellow" }, props.name),
|
|
101
|
+
version: (React.createElement(Ink.Text, { color: "#38bdf8" }, state.latest_version)),
|
|
102
|
+
} }));
|
|
103
|
+
patch({ status: "install" });
|
|
104
|
+
await cli(`npm install -g ${props.name}@latest`);
|
|
105
|
+
patch({ status: "exit" });
|
|
106
|
+
handle_output(React.createElement(Ink.Text, { key: "done", dimColor: true }, "Auto update done."));
|
|
107
|
+
}, onNo: () => {
|
|
108
|
+
patch({ status: "done" });
|
|
109
|
+
} }));
|
|
110
|
+
case "install":
|
|
111
|
+
return null;
|
|
112
|
+
case "exit":
|
|
113
|
+
return null;
|
|
114
|
+
case "done":
|
|
115
|
+
return props.children;
|
|
116
|
+
}
|
|
117
|
+
})();
|
|
118
|
+
return (React.createElement(React.Fragment, null,
|
|
119
|
+
output,
|
|
120
|
+
status));
|
|
121
|
+
}
|
|
122
|
+
// returns +1 if version_a is greater than version_b
|
|
123
|
+
// returns -1 if version_a is less than version_b
|
|
124
|
+
// returns +0 if version_a is exactly equal to version_b
|
|
125
|
+
//
|
|
126
|
+
// Examples
|
|
127
|
+
//
|
|
128
|
+
// semver_compare("0.1.1", "0.0.2"); // 1
|
|
129
|
+
// semver_compare("1.0.1", "0.0.2"); // 1
|
|
130
|
+
// semver_compare("0.0.1", "1.0.2"); // -1
|
|
131
|
+
// semver_compare("0.0.1", "0.1.2"); // -1
|
|
132
|
+
// semver_compare("1.0.1", "1.0.1"); // 0
|
|
133
|
+
//
|
|
134
|
+
function semver_compare(version_a, version_b) {
|
|
135
|
+
const split_a = version_a.split(".").map(Number);
|
|
136
|
+
const split_b = version_b.split(".").map(Number);
|
|
137
|
+
const max_split_parts = Math.max(split_a.length, split_b.length);
|
|
138
|
+
for (let i = 0; i < max_split_parts; i++) {
|
|
139
|
+
const num_a = split_a[i] || 0;
|
|
140
|
+
const num_b = split_b[i] || 0;
|
|
141
|
+
if (num_a > num_b)
|
|
142
|
+
return 1;
|
|
143
|
+
if (num_a < num_b)
|
|
144
|
+
return -1;
|
|
145
|
+
}
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
package/dist/app/Debug.js
CHANGED
|
@@ -9,10 +9,10 @@ export function Debug() {
|
|
|
9
9
|
const actions = Store.useActions();
|
|
10
10
|
const state = Store.useState((state) => state);
|
|
11
11
|
const argv = Store.useState((state) => state.argv);
|
|
12
|
+
const debug = Store.useState((state) => state.select.debug(state));
|
|
12
13
|
React.useEffect(function debugMessageOnce() {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
actions.debug(React.createElement(Ink.Text, { dimColor: true }, JSON.stringify(argv, null, 2)));
|
|
14
|
+
if (debug) {
|
|
15
|
+
actions.output(React.createElement(Ink.Text, { color: "yellow" }, "Debug mode enabled"));
|
|
16
16
|
}
|
|
17
17
|
}, [argv]);
|
|
18
18
|
React.useEffect(function syncStateJson() {
|
|
@@ -20,7 +20,7 @@ export function Debug() {
|
|
|
20
20
|
if (!argv?.["write-state-json"]) {
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
const output_file = path.join(state.cwd, "git-
|
|
23
|
+
const output_file = path.join(state.cwd, "git-stack-state.json");
|
|
24
24
|
if (fs.existsSync(output_file)) {
|
|
25
25
|
fs.rmSync(output_file);
|
|
26
26
|
}
|
package/dist/app/FormatText.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import * as Ink from "ink";
|
|
2
3
|
import { FormattedMessage } from "react-intl";
|
|
3
4
|
export function FormatText(props) {
|
|
5
|
+
const wrapper = props.wrapper || React.createElement(Ink.Text, null);
|
|
4
6
|
return (React.createElement(FormattedMessage, { id: "FormatText", defaultMessage: props.message, values: props.values }, (chunks) => {
|
|
5
|
-
return React.cloneElement(
|
|
7
|
+
return React.cloneElement(wrapper, {}, chunks);
|
|
6
8
|
}));
|
|
7
9
|
}
|
|
@@ -1,54 +1,58 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as Ink from "ink";
|
|
3
|
-
import * as CommitMetadata from "../core/CommitMetadata.js";
|
|
4
3
|
import { cli } from "../core/cli.js";
|
|
5
4
|
import { invariant } from "../core/invariant.js";
|
|
6
|
-
import
|
|
5
|
+
import { match_group } from "../core/match_group.js";
|
|
7
6
|
import { Await } from "./Await.js";
|
|
8
7
|
import { Store } from "./Store.js";
|
|
9
8
|
export function GatherMetadata(props) {
|
|
10
9
|
const argv = Store.useState((state) => state.argv);
|
|
11
10
|
invariant(argv, "argv must exist");
|
|
12
|
-
const fallback = (React.createElement(Ink.Text, { color: "yellow" }, "
|
|
13
|
-
if (argv["mock-metadata"]) {
|
|
14
|
-
return (React.createElement(Await, { fallback: fallback, function: mock_metadata }, props.children));
|
|
15
|
-
}
|
|
11
|
+
const fallback = (React.createElement(Ink.Text, { color: "yellow" }, "Gathering local git information..."));
|
|
16
12
|
return (React.createElement(Await, { fallback: fallback, function: gather_metadata }, props.children));
|
|
17
13
|
}
|
|
18
|
-
async function mock_metadata() {
|
|
19
|
-
const module = await import("../__fixtures__/metadata.js");
|
|
20
|
-
const deserialized = json.deserialize(module.METADATA);
|
|
21
|
-
Store.setState((state) => {
|
|
22
|
-
Object.assign(state, deserialized);
|
|
23
|
-
state.step = "status";
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
14
|
async function gather_metadata() {
|
|
27
15
|
const actions = Store.getState().actions;
|
|
28
|
-
const head = (await cli("git rev-parse HEAD")).stdout;
|
|
29
|
-
const merge_base = (await cli("git merge-base HEAD master")).stdout;
|
|
30
|
-
// handle when there are no detected changes
|
|
31
|
-
if (head === merge_base) {
|
|
32
|
-
actions.newline();
|
|
33
|
-
actions.output(React.createElement(Ink.Text, { color: "gray" }, "No changes detected."));
|
|
34
|
-
actions.exit(0);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const branch_name = (await cli("git rev-parse --abbrev-ref HEAD")).stdout;
|
|
38
16
|
try {
|
|
39
|
-
const
|
|
17
|
+
const branch_name = (await cli("git rev-parse --abbrev-ref HEAD")).stdout;
|
|
18
|
+
// handle when there are no detected changes
|
|
19
|
+
if (branch_name === "master") {
|
|
20
|
+
actions.newline();
|
|
21
|
+
actions.error("Must run within a branch.");
|
|
22
|
+
actions.exit(0);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const head = (await cli("git rev-parse HEAD")).stdout;
|
|
26
|
+
const merge_base = (await cli("git merge-base HEAD master")).stdout;
|
|
27
|
+
// handle when there are no detected changes
|
|
28
|
+
if (head === merge_base) {
|
|
29
|
+
actions.newline();
|
|
30
|
+
actions.output(React.createElement(Ink.Text, { color: "gray" }, "No changes detected."));
|
|
31
|
+
actions.exit(0);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// git@github.com:magus/git-multi-diff-playground.git
|
|
35
|
+
// https://github.com/magus/git-multi-diff-playground.git
|
|
36
|
+
const origin_url = (await cli(`git config --get remote.origin.url`)).stdout;
|
|
37
|
+
const repo_path = match_group(origin_url, RE.repo_path, "repo_path");
|
|
40
38
|
Store.setState((state) => {
|
|
39
|
+
state.repo_path = repo_path;
|
|
41
40
|
state.head = head;
|
|
42
41
|
state.merge_base = merge_base;
|
|
43
42
|
state.branch_name = branch_name;
|
|
44
|
-
state.commit_range = commit_range;
|
|
45
|
-
state.step = "status";
|
|
46
43
|
});
|
|
47
44
|
}
|
|
48
45
|
catch (err) {
|
|
49
|
-
actions.
|
|
46
|
+
actions.error("Unable to gather git metadata.");
|
|
50
47
|
if (err instanceof Error) {
|
|
51
|
-
actions.
|
|
48
|
+
if (actions.isDebug()) {
|
|
49
|
+
actions.error(err.message);
|
|
50
|
+
}
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
}
|
|
54
|
+
const RE = {
|
|
55
|
+
// git@github.com:magus/git-multi-diff-playground.git
|
|
56
|
+
// https://github.com/magus/git-multi-diff-playground.git
|
|
57
|
+
repo_path: /(?<repo_path>[^:^/]+\/[^/]+)\.git/,
|
|
58
|
+
};
|
|
@@ -1,62 +1,53 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as Ink from "ink";
|
|
3
|
-
import * as CommitMetadata from "../core/CommitMetadata.js";
|
|
4
3
|
import { cli } from "../core/cli.js";
|
|
5
4
|
import { invariant } from "../core/invariant.js";
|
|
6
|
-
import * as json from "../core/json.js";
|
|
7
5
|
import { match_group } from "../core/match_group.js";
|
|
8
6
|
import { Await } from "./Await.js";
|
|
9
7
|
import { Store } from "./Store.js";
|
|
10
8
|
export function GatherMetadata(props) {
|
|
11
9
|
const argv = Store.useState((state) => state.argv);
|
|
12
10
|
invariant(argv, "argv must exist");
|
|
13
|
-
const fallback = (React.createElement(Ink.Text, { color: "yellow" }, "
|
|
14
|
-
if (argv["mock-metadata"]) {
|
|
15
|
-
return (React.createElement(Await, { fallback: fallback, function: mock_metadata }, props.children));
|
|
16
|
-
}
|
|
11
|
+
const fallback = (React.createElement(Ink.Text, { color: "yellow" }, "Gathering local git information..."));
|
|
17
12
|
return (React.createElement(Await, { fallback: fallback, function: gather_metadata }, props.children));
|
|
18
13
|
}
|
|
19
|
-
async function mock_metadata() {
|
|
20
|
-
const module = await import("../__fixtures__/metadata.js");
|
|
21
|
-
const deserialized = json.deserialize(module.METADATA);
|
|
22
|
-
Store.setState((state) => {
|
|
23
|
-
Object.assign(state, deserialized);
|
|
24
|
-
state.step = "status";
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
14
|
async function gather_metadata() {
|
|
28
15
|
const actions = Store.getState().actions;
|
|
29
|
-
const head = (await cli("git rev-parse HEAD")).stdout;
|
|
30
|
-
const merge_base = (await cli("git merge-base HEAD master")).stdout;
|
|
31
|
-
// handle when there are no detected changes
|
|
32
|
-
if (head === merge_base) {
|
|
33
|
-
actions.newline();
|
|
34
|
-
actions.output(React.createElement(Ink.Text, { color: "gray" }, "No changes detected."));
|
|
35
|
-
actions.exit(0);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
// git@github.com:magus/git-multi-diff-playground.git
|
|
39
|
-
// https://github.com/magus/git-multi-diff-playground.git
|
|
40
|
-
const origin_url = (await cli(`git config --get remote.origin.url`)).stdout;
|
|
41
|
-
const repo_path = match_group(origin_url, RE.repo_path, "repo_path");
|
|
42
|
-
const branch_name = (await cli("git rev-parse --abbrev-ref HEAD")).stdout;
|
|
43
|
-
Store.setState((state) => {
|
|
44
|
-
state.repo_path = repo_path;
|
|
45
|
-
state.head = head;
|
|
46
|
-
state.merge_base = merge_base;
|
|
47
|
-
state.branch_name = branch_name;
|
|
48
|
-
});
|
|
49
16
|
try {
|
|
50
|
-
const
|
|
17
|
+
const branch_name = (await cli("git rev-parse --abbrev-ref HEAD")).stdout;
|
|
18
|
+
// handle when there are no detected changes
|
|
19
|
+
if (branch_name === "master") {
|
|
20
|
+
actions.newline();
|
|
21
|
+
actions.error("Must run within a branch.");
|
|
22
|
+
actions.exit(0);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const head = (await cli("git rev-parse HEAD")).stdout;
|
|
26
|
+
const merge_base = (await cli("git merge-base HEAD master")).stdout;
|
|
27
|
+
// handle when there are no detected changes
|
|
28
|
+
if (head === merge_base) {
|
|
29
|
+
actions.newline();
|
|
30
|
+
actions.output(React.createElement(Ink.Text, { color: "gray" }, "No changes detected."));
|
|
31
|
+
actions.exit(0);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// git@github.com:magus/git-multi-diff-playground.git
|
|
35
|
+
// https://github.com/magus/git-multi-diff-playground.git
|
|
36
|
+
const origin_url = (await cli(`git config --get remote.origin.url`)).stdout;
|
|
37
|
+
const repo_path = match_group(origin_url, RE.repo_path, "repo_path");
|
|
51
38
|
Store.setState((state) => {
|
|
52
|
-
state.
|
|
53
|
-
state.
|
|
39
|
+
state.repo_path = repo_path;
|
|
40
|
+
state.head = head;
|
|
41
|
+
state.merge_base = merge_base;
|
|
42
|
+
state.branch_name = branch_name;
|
|
54
43
|
});
|
|
55
44
|
}
|
|
56
45
|
catch (err) {
|
|
57
|
-
actions.
|
|
46
|
+
actions.error("Unable to gather git metadata.");
|
|
58
47
|
if (err instanceof Error) {
|
|
59
|
-
actions.
|
|
48
|
+
if (actions.isDebug()) {
|
|
49
|
+
actions.error(err.message);
|
|
50
|
+
}
|
|
60
51
|
}
|
|
61
52
|
}
|
|
62
53
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as Ink from "ink";
|
|
3
|
+
export function Input(props) {
|
|
4
|
+
const [value, set_value] = React.useState(props.value || "");
|
|
5
|
+
Ink.useInput((input, key) => {
|
|
6
|
+
if (key.backspace) {
|
|
7
|
+
set_value((value) => value.slice(0, -1));
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
set_value((value) => `${value}${input}`);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return (React.createElement(Ink.Box, { borderStyle: "single" },
|
|
14
|
+
React.createElement(Ink.Text, null, value)));
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as Ink from "ink";
|
|
3
|
+
import * as CommitMetadata from "../core/CommitMetadata.js";
|
|
4
|
+
import { invariant } from "../core/invariant.js";
|
|
5
|
+
import * as json from "../core/json.js";
|
|
6
|
+
import { Await } from "./Await.js";
|
|
7
|
+
import { Store } from "./Store.js";
|
|
8
|
+
export function LocalCommitStatus(props) {
|
|
9
|
+
const argv = Store.useState((state) => state.argv);
|
|
10
|
+
invariant(argv, "argv must exist");
|
|
11
|
+
const fallback = (React.createElement(Ink.Text, { color: "yellow" }, "Fetching PR status from Github..."));
|
|
12
|
+
if (argv["mock-metadata"]) {
|
|
13
|
+
return (React.createElement(Await, { fallback: fallback, function: mock_metadata }, props.children));
|
|
14
|
+
}
|
|
15
|
+
return (React.createElement(Await, { fallback: fallback, function: gather_metadata }, props.children));
|
|
16
|
+
}
|
|
17
|
+
async function mock_metadata() {
|
|
18
|
+
const module = await import("../__fixtures__/metadata.js");
|
|
19
|
+
const deserialized = json.deserialize(module.METADATA);
|
|
20
|
+
Store.setState((state) => {
|
|
21
|
+
Object.assign(state, deserialized);
|
|
22
|
+
state.step = "status";
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function gather_metadata() {
|
|
26
|
+
const actions = Store.getState().actions;
|
|
27
|
+
try {
|
|
28
|
+
const commit_range = await CommitMetadata.range();
|
|
29
|
+
Store.setState((state) => {
|
|
30
|
+
state.commit_range = commit_range;
|
|
31
|
+
state.step = "status";
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
actions.error("Unable to retrieve local commit status.");
|
|
36
|
+
if (err instanceof Error) {
|
|
37
|
+
if (actions.isDebug()) {
|
|
38
|
+
actions.error(err.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as Ink from "ink";
|
|
3
|
+
import * as CommitMetadata from "../core/CommitMetadata.js";
|
|
4
|
+
import * as Metadata from "../core/Metadata.js";
|
|
5
|
+
import { cli } from "../core/cli.js";
|
|
6
|
+
import { invariant } from "../core/invariant.js";
|
|
7
|
+
import { short_id } from "../core/short_id.js";
|
|
8
|
+
import { Await } from "./Await.js";
|
|
9
|
+
import { Brackets } from "./Brackets.js";
|
|
10
|
+
import { FormatText } from "./FormatText.js";
|
|
11
|
+
import { Parens } from "./Parens.js";
|
|
12
|
+
import { Store } from "./Store.js";
|
|
13
|
+
export function LocalMergeRebase() {
|
|
14
|
+
return (React.createElement(Await, { fallback: React.createElement(Ink.Text, { color: "yellow" }, "Rebasing commits..."), function: run }));
|
|
15
|
+
}
|
|
16
|
+
async function run() {
|
|
17
|
+
const state = Store.getState();
|
|
18
|
+
const actions = state.actions;
|
|
19
|
+
const branch_name = state.branch_name;
|
|
20
|
+
const commit_range = state.commit_range;
|
|
21
|
+
invariant(branch_name, "branch_name must exist");
|
|
22
|
+
invariant(commit_range, "commit_range must exist");
|
|
23
|
+
// always listen for SIGINT event and restore git state
|
|
24
|
+
process.once("SIGINT", handle_exit);
|
|
25
|
+
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
26
|
+
try {
|
|
27
|
+
await cli(`git fetch --no-tags -v origin master:master`);
|
|
28
|
+
const master_sha = (await cli(`git rev-parse master`)).stdout;
|
|
29
|
+
const rebase_merge_base = master_sha;
|
|
30
|
+
// create temporary branch based on merge base
|
|
31
|
+
await cli(`git checkout -b ${temp_branch_name} ${rebase_merge_base}`);
|
|
32
|
+
for (let i = 0; i < commit_range.commit_list.length; i++) {
|
|
33
|
+
const commit = commit_range.commit_list[i];
|
|
34
|
+
const commit_pr = commit_range.pr_map.get(commit.branch_id || "");
|
|
35
|
+
// drop commits that are in groups of merged PRs
|
|
36
|
+
const merged_pr = commit_pr?.state === "MERGED";
|
|
37
|
+
if (merged_pr) {
|
|
38
|
+
if (actions.isDebug()) {
|
|
39
|
+
actions.output(React.createElement(FormatText, { wrapper: React.createElement(Ink.Text, { color: "yellow", wrap: "truncate-end" }), message: "Dropping {commit_message} {pr_status}", values: {
|
|
40
|
+
commit_message: React.createElement(Brackets, null, commit.message),
|
|
41
|
+
pr_status: React.createElement(Parens, null, "MERGED"),
|
|
42
|
+
} }));
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// cherry-pick and amend commits one by one
|
|
47
|
+
if (actions.isDebug()) {
|
|
48
|
+
actions.output(React.createElement(FormatText, { wrapper: React.createElement(Ink.Text, { color: "yellow", wrap: "truncate-end" }), message: "Picking {commit_message}", values: {
|
|
49
|
+
commit_message: React.createElement(Brackets, null, commit.message),
|
|
50
|
+
} }));
|
|
51
|
+
}
|
|
52
|
+
await cli(`git cherry-pick ${commit.sha}`);
|
|
53
|
+
if (commit.branch_id && !commit_pr) {
|
|
54
|
+
if (actions.isDebug()) {
|
|
55
|
+
actions.output(React.createElement(FormatText, { wrapper: React.createElement(Ink.Text, { color: "yellow", wrap: "truncate-end" }), message: "Cleaning up unused group {group}", values: {
|
|
56
|
+
group: React.createElement(Brackets, null, commit.branch_id),
|
|
57
|
+
} }));
|
|
58
|
+
}
|
|
59
|
+
// missing PR, clear branch id from commit
|
|
60
|
+
const new_message = await Metadata.remove(commit.message);
|
|
61
|
+
await cli(`git commit --amend -m "${new_message}"`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// after all commits have been cherry-picked and amended
|
|
65
|
+
// move the branch pointer to the newly created temporary branch
|
|
66
|
+
// now we are in locally in sync with github and on the original branch
|
|
67
|
+
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
68
|
+
restore_git();
|
|
69
|
+
const next_commit_range = await CommitMetadata.range();
|
|
70
|
+
actions.set((state) => {
|
|
71
|
+
state.commit_range = next_commit_range;
|
|
72
|
+
state.step = "status";
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
actions.error("Unable to rebase.");
|
|
77
|
+
if (err instanceof Error) {
|
|
78
|
+
if (actions.isDebug()) {
|
|
79
|
+
actions.error(err.message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
handle_exit();
|
|
83
|
+
}
|
|
84
|
+
// cleanup git operations if cancelled during manual rebase
|
|
85
|
+
function restore_git() {
|
|
86
|
+
// signint handler MUST run synchronously
|
|
87
|
+
// trying to use `await cli(...)` here will silently fail since
|
|
88
|
+
// all children processes receive the SIGINT signal
|
|
89
|
+
const spawn_options = { ignoreExitCode: true };
|
|
90
|
+
// always put self back in original branch
|
|
91
|
+
cli.sync(`git checkout ${branch_name}`, spawn_options);
|
|
92
|
+
// ...and cleanup temporary branch
|
|
93
|
+
cli.sync(`git branch -D ${temp_branch_name}`, spawn_options);
|
|
94
|
+
if (commit_range) {
|
|
95
|
+
// ...and cleanup pr group branches
|
|
96
|
+
for (const group of commit_range.group_list) {
|
|
97
|
+
cli.sync(`git branch -D ${group.id}`, spawn_options);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function handle_exit() {
|
|
102
|
+
actions.output(React.createElement(Ink.Text, { color: "yellow" },
|
|
103
|
+
"Restoring ",
|
|
104
|
+
React.createElement(Brackets, null, branch_name),
|
|
105
|
+
"..."));
|
|
106
|
+
restore_git();
|
|
107
|
+
actions.output(React.createElement(Ink.Text, { color: "yellow" },
|
|
108
|
+
"Restored ",
|
|
109
|
+
React.createElement(Brackets, null, branch_name),
|
|
110
|
+
"."));
|
|
111
|
+
actions.exit(5);
|
|
112
|
+
}
|
|
113
|
+
}
|