git-stack-cli 1.8.3 → 1.9.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/dist/cjs/index.cjs +234 -69
- package/package.json +1 -1
- package/src/app/App.tsx +30 -13
- package/src/app/PreManualRebase.tsx +0 -1
- package/src/app/Store.tsx +2 -0
- package/src/command.ts +133 -93
- package/src/commands/Fixup.tsx +121 -0
- package/src/commands/Log.tsx +72 -0
- package/src/core/CommitMetadata.ts +23 -0
- package/src/core/GitReviseTodo.test.ts +389 -397
- package/src/core/GitReviseTodo.ts +4 -1
- package/src/core/Metadata.test.ts +85 -0
- package/src/core/Metadata.ts +5 -5
- package/src/index.tsx +1 -0
package/src/command.ts
CHANGED
|
@@ -1,103 +1,33 @@
|
|
|
1
1
|
import yargs from "yargs";
|
|
2
2
|
import { hideBin } from "yargs/helpers";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import type { Options, InferredOptionTypes, Arguments } from "yargs";
|
|
5
|
+
|
|
6
|
+
export type Argv = Arguments & TGlobalOptions & TFixupOptions & TDefaultOptions;
|
|
7
7
|
|
|
8
8
|
export async function command() {
|
|
9
9
|
// https://yargs.js.org/docs/#api-reference-optionkey-opt
|
|
10
10
|
return (
|
|
11
11
|
yargs(hideBin(process.argv))
|
|
12
|
-
.usage("Usage: git stack [options]")
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
default: true,
|
|
32
|
-
description: "Sync commit ranges to Github, disable with --no-sync",
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
.option("verify", {
|
|
36
|
-
type: "boolean",
|
|
37
|
-
default: true,
|
|
38
|
-
description:
|
|
39
|
-
"Run git hooks such as pre-commit and pre-push, disable with --no-verify",
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
.option("rebase", {
|
|
43
|
-
type: "string",
|
|
44
|
-
choices: [Rebase["git-revise"], Rebase["cherry-pick"]],
|
|
45
|
-
default: Rebase["git-revise"],
|
|
46
|
-
description: [
|
|
47
|
-
"Strategy used for syncing branches",
|
|
48
|
-
`${Rebase["git-revise"]}: perform faster in-memory rebase`,
|
|
49
|
-
`${Rebase["cherry-pick"]}: use disk and incrementally rebase each commit`,
|
|
50
|
-
].join(" | "),
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
.option("verbose", {
|
|
54
|
-
type: "boolean",
|
|
55
|
-
alias: ["v"],
|
|
56
|
-
default: false,
|
|
57
|
-
description: "Print more detailed logs for debugging internals",
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
.option("update", {
|
|
61
|
-
type: "boolean",
|
|
62
|
-
alias: ["u", "upgrade"],
|
|
63
|
-
default: false,
|
|
64
|
-
description: "Check and install the latest version",
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
.option("branch", {
|
|
68
|
-
type: "string",
|
|
69
|
-
alias: ["b"],
|
|
70
|
-
description:
|
|
71
|
-
'Set the master branch name, defaults to "master" (or "main" if "master" is not found)',
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
.option("draft", {
|
|
75
|
-
type: "boolean",
|
|
76
|
-
alias: ["d"],
|
|
77
|
-
default: false,
|
|
78
|
-
description: "Open all PRs as drafts",
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
.option("write-state-json", {
|
|
82
|
-
hidden: true,
|
|
83
|
-
type: "boolean",
|
|
84
|
-
default: false,
|
|
85
|
-
description: "Write state to local json file for debugging",
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
.option("template", {
|
|
89
|
-
type: "boolean",
|
|
90
|
-
default: true,
|
|
91
|
-
description:
|
|
92
|
-
"Use automatic Github PR template, e.g. .github/pull_request_template.md, disable with --no-template",
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
.option("mock-metadata", {
|
|
96
|
-
hidden: true,
|
|
97
|
-
type: "boolean",
|
|
98
|
-
default: false,
|
|
99
|
-
description: "Mock local store metadata for testing",
|
|
100
|
-
})
|
|
12
|
+
.usage("Usage: git stack [command] [options]")
|
|
13
|
+
|
|
14
|
+
.command("$0", "Sync commit ranges to Github", (yargs) =>
|
|
15
|
+
yargs.options(DefaultOptions)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
.command(
|
|
19
|
+
"fixup [commit]",
|
|
20
|
+
"Amend staged changes to a specific commit in history",
|
|
21
|
+
(yargs) => yargs.positional("commit", FixupOptions.commit)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
.command(
|
|
25
|
+
"log [args...]",
|
|
26
|
+
"Print an abbreviated log with numbered commits, useful for git stack fixup",
|
|
27
|
+
(yargs) => yargs.strict(false)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
.option("verbose", GlobalOptions.verbose)
|
|
101
31
|
|
|
102
32
|
// yargs default wraps to 80 columns
|
|
103
33
|
// passing null will wrap to terminal width
|
|
@@ -111,7 +41,8 @@ export async function command() {
|
|
|
111
41
|
"show-hidden",
|
|
112
42
|
"Show hidden options via `git stack help --show-hidden`"
|
|
113
43
|
)
|
|
114
|
-
.help("help", "Show usage via `git stack help`")
|
|
44
|
+
.help("help", "Show usage via `git stack help`")
|
|
45
|
+
.argv as unknown as Promise<Argv>
|
|
115
46
|
);
|
|
116
47
|
}
|
|
117
48
|
|
|
@@ -119,3 +50,112 @@ const Rebase = Object.freeze({
|
|
|
119
50
|
"git-revise": "git-revise",
|
|
120
51
|
"cherry-pick": "cherry-pick",
|
|
121
52
|
});
|
|
53
|
+
|
|
54
|
+
const GlobalOptions = {
|
|
55
|
+
verbose: {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
alias: ["v"],
|
|
58
|
+
default: false,
|
|
59
|
+
description: "Print more detailed logs for debugging internals",
|
|
60
|
+
},
|
|
61
|
+
} satisfies YargsOptions;
|
|
62
|
+
|
|
63
|
+
const DefaultOptions = {
|
|
64
|
+
"force": {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
alias: ["f"],
|
|
67
|
+
default: false,
|
|
68
|
+
description: "Force sync even if no changes are detected",
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
"check": {
|
|
72
|
+
type: "boolean",
|
|
73
|
+
alias: ["c"],
|
|
74
|
+
default: false,
|
|
75
|
+
description: "Print status table and exit without syncing",
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"sync": {
|
|
79
|
+
type: "boolean",
|
|
80
|
+
alias: ["s"],
|
|
81
|
+
default: true,
|
|
82
|
+
description: "Sync commit ranges to Github, disable with --no-sync",
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
"verify": {
|
|
86
|
+
type: "boolean",
|
|
87
|
+
default: true,
|
|
88
|
+
description:
|
|
89
|
+
"Run git hooks such as pre-commit and pre-push, disable with --no-verify",
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
"rebase": {
|
|
93
|
+
type: "string",
|
|
94
|
+
choices: [Rebase["git-revise"], Rebase["cherry-pick"]],
|
|
95
|
+
default: Rebase["git-revise"],
|
|
96
|
+
description: [
|
|
97
|
+
"Strategy used for syncing branches",
|
|
98
|
+
`${Rebase["git-revise"]}: perform faster in-memory rebase`,
|
|
99
|
+
`${Rebase["cherry-pick"]}: use disk and incrementally rebase each commit`,
|
|
100
|
+
].join(" | "),
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
"update": {
|
|
104
|
+
type: "boolean",
|
|
105
|
+
alias: ["u", "upgrade"],
|
|
106
|
+
default: false,
|
|
107
|
+
description: "Check and install the latest version",
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
"branch": {
|
|
111
|
+
type: "string",
|
|
112
|
+
alias: ["b"],
|
|
113
|
+
description:
|
|
114
|
+
'Set the master branch name, defaults to "master" (or "main" if "master" is not found)',
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
"draft": {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
alias: ["d"],
|
|
120
|
+
default: false,
|
|
121
|
+
description: "Open all PRs as drafts",
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
"write-state-json": {
|
|
125
|
+
hidden: true,
|
|
126
|
+
type: "boolean",
|
|
127
|
+
default: false,
|
|
128
|
+
description: "Write state to local json file for debugging",
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
"template": {
|
|
132
|
+
type: "boolean",
|
|
133
|
+
default: true,
|
|
134
|
+
description:
|
|
135
|
+
"Use automatic Github PR template, e.g. .github/pull_request_template.md, disable with --no-template",
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
"mock-metadata": {
|
|
139
|
+
hidden: true,
|
|
140
|
+
type: "boolean",
|
|
141
|
+
default: false,
|
|
142
|
+
description: "Mock local store metadata for testing",
|
|
143
|
+
},
|
|
144
|
+
} satisfies YargsOptions;
|
|
145
|
+
|
|
146
|
+
const FixupOptions = {
|
|
147
|
+
commit: {
|
|
148
|
+
type: "number",
|
|
149
|
+
default: 1,
|
|
150
|
+
description: [
|
|
151
|
+
"Relative number of commit to amend staged changes.",
|
|
152
|
+
"Most recent is 1, next is 2, etc.",
|
|
153
|
+
].join("\n"),
|
|
154
|
+
},
|
|
155
|
+
} satisfies YargsOptions;
|
|
156
|
+
|
|
157
|
+
type YargsOptions = { [key: string]: Options };
|
|
158
|
+
|
|
159
|
+
type TGlobalOptions = InferredOptionTypes<typeof GlobalOptions>;
|
|
160
|
+
type TFixupOptions = InferredOptionTypes<typeof FixupOptions>;
|
|
161
|
+
type TDefaultOptions = InferredOptionTypes<typeof DefaultOptions>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Await } from "~/app/Await";
|
|
6
|
+
import { FormatText } from "~/app/FormatText";
|
|
7
|
+
import { Parens } from "~/app/Parens";
|
|
8
|
+
import { Store } from "~/app/Store";
|
|
9
|
+
import { cli } from "~/core/cli";
|
|
10
|
+
import { colors } from "~/core/colors";
|
|
11
|
+
|
|
12
|
+
export function Fixup() {
|
|
13
|
+
return <Await fallback={null} function={run} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function run() {
|
|
17
|
+
const state = Store.getState();
|
|
18
|
+
const actions = state.actions;
|
|
19
|
+
const argv = state.argv;
|
|
20
|
+
|
|
21
|
+
const relative_number = argv.commit;
|
|
22
|
+
|
|
23
|
+
if (!relative_number) {
|
|
24
|
+
actions.output(
|
|
25
|
+
<Ink.Text color={colors.red}>
|
|
26
|
+
❗️ Usage: git fixup {"<relative-commit-number>"}
|
|
27
|
+
</Ink.Text>
|
|
28
|
+
);
|
|
29
|
+
actions.output("");
|
|
30
|
+
actions.output(
|
|
31
|
+
"This script automates the process of adding staged changes as a fixup commit"
|
|
32
|
+
);
|
|
33
|
+
actions.output(
|
|
34
|
+
"and the subsequent git rebase to flatten the commits based on relative commit number"
|
|
35
|
+
);
|
|
36
|
+
actions.output(
|
|
37
|
+
"You can use a `git log` like below to get the relative commit number"
|
|
38
|
+
);
|
|
39
|
+
actions.output("");
|
|
40
|
+
actions.output(" ❯ git stack log");
|
|
41
|
+
actions.output(
|
|
42
|
+
" 1\te329794d5f881cbf0fc3f26d2108cf6f3fdebabe enable drop_error_subtask test param"
|
|
43
|
+
);
|
|
44
|
+
actions.output(
|
|
45
|
+
" 2\t57f43b596e5c6b97bc47e2a591f82ccc81651156 test drop_error_subtask baseline"
|
|
46
|
+
);
|
|
47
|
+
actions.output(
|
|
48
|
+
" 3\t838e878d483c6a2d5393063fc59baf2407225c6d ErrorSubtask test baseline"
|
|
49
|
+
);
|
|
50
|
+
actions.output("");
|
|
51
|
+
actions.output("To target `838e87` above, you would call `fixup 3`");
|
|
52
|
+
|
|
53
|
+
actions.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const diff_staged_cmd = await cli("git diff --cached --quiet", {
|
|
57
|
+
ignoreExitCode: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!diff_staged_cmd.code) {
|
|
61
|
+
actions.error("🚨 Stage changes before calling fixup");
|
|
62
|
+
actions.exit(1);
|
|
63
|
+
// actions.output(
|
|
64
|
+
// <Ink.Text color={colors.red}>
|
|
65
|
+
// ❗️ Usage: git fixup {"<relative-commit-number>"}
|
|
66
|
+
// </Ink.Text>
|
|
67
|
+
// );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Calculate commit SHA based on the relative commit number
|
|
71
|
+
const adjusted_number = Number(relative_number) - 1;
|
|
72
|
+
|
|
73
|
+
// get the commit SHA of the target commit
|
|
74
|
+
const commit_sha = (await cli(`git rev-parse HEAD~${adjusted_number}`))
|
|
75
|
+
.stdout;
|
|
76
|
+
|
|
77
|
+
await cli(`git commit --fixup ${commit_sha}`);
|
|
78
|
+
|
|
79
|
+
// check if stash required
|
|
80
|
+
let save_stash = false;
|
|
81
|
+
|
|
82
|
+
const diff_cmd = await cli("git diff-index --quiet HEAD --", {
|
|
83
|
+
ignoreExitCode: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (diff_cmd.code) {
|
|
87
|
+
save_stash = true;
|
|
88
|
+
|
|
89
|
+
await cli("git stash -q");
|
|
90
|
+
|
|
91
|
+
actions.output(<Ink.Text>📦 Changes saved to stash</Ink.Text>);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// rebase target needs to account for new commit created above
|
|
95
|
+
const rebase_target = Number(relative_number) + 1;
|
|
96
|
+
await cli(`git rebase -i --autosquash HEAD~${rebase_target}`, {
|
|
97
|
+
env: {
|
|
98
|
+
PATH: process.env.PATH,
|
|
99
|
+
GIT_EDITOR: "true",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
actions.output(
|
|
104
|
+
<FormatText
|
|
105
|
+
wrapper={<Ink.Text color={colors.yellow} />}
|
|
106
|
+
message="🛠️ fixup {relative_number} {commit_sha}"
|
|
107
|
+
values={{
|
|
108
|
+
commit_sha: <Parens>{commit_sha}</Parens>,
|
|
109
|
+
relative_number: relative_number,
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (save_stash) {
|
|
115
|
+
await cli("git stash pop -q");
|
|
116
|
+
|
|
117
|
+
actions.output(
|
|
118
|
+
<Ink.Text color={colors.green}>✅ Changes restored from stash</Ink.Text>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Await } from "~/app/Await";
|
|
6
|
+
import { Store } from "~/app/Store";
|
|
7
|
+
import { cli } from "~/core/cli";
|
|
8
|
+
import { invariant } from "~/core/invariant";
|
|
9
|
+
|
|
10
|
+
export function Log() {
|
|
11
|
+
const { stdout } = Ink.useStdout();
|
|
12
|
+
const available_width = stdout.columns || 80;
|
|
13
|
+
|
|
14
|
+
return <Await fallback={null} function={() => run({ available_width })} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Args = {
|
|
18
|
+
available_width: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function run(args: Args) {
|
|
22
|
+
const state = Store.getState();
|
|
23
|
+
const actions = state.actions;
|
|
24
|
+
const process_argv = state.process_argv;
|
|
25
|
+
|
|
26
|
+
invariant(actions, "actions must exist");
|
|
27
|
+
|
|
28
|
+
// estimate the number of color characters per line
|
|
29
|
+
// assuming an average of 5 color changes per line and 5 characters per color code
|
|
30
|
+
const color_buffer = 12 * 5;
|
|
31
|
+
const truncation_width = args.available_width + color_buffer;
|
|
32
|
+
|
|
33
|
+
// get the number of characters in the short sha for this repo
|
|
34
|
+
const short_sha = (await cli(`git log -1 --format=%h`)).stdout.trim();
|
|
35
|
+
const short_sha_length = short_sha.length + 1;
|
|
36
|
+
|
|
37
|
+
// SHA hash - At least 9 characters wide, truncated
|
|
38
|
+
const sha_format = `%C(green)%<(${short_sha_length},trunc)%h`;
|
|
39
|
+
|
|
40
|
+
// relative commit date - 15 characters wide, truncated
|
|
41
|
+
const date_format = `%C(white)%<(15,trunc)%cr`;
|
|
42
|
+
|
|
43
|
+
// author's abbreviated name - 12 characters wide, truncated
|
|
44
|
+
const author_format = `%C(white)%<(8,trunc)%al`;
|
|
45
|
+
|
|
46
|
+
// decorative information like branch heads or tags
|
|
47
|
+
const decoration_format = `%C(auto)%d`;
|
|
48
|
+
|
|
49
|
+
// commit subject - 80 characters wide, truncated
|
|
50
|
+
const subject_format = `%<(60,trunc)%s`;
|
|
51
|
+
|
|
52
|
+
// combine all the above formats into one
|
|
53
|
+
const format = [
|
|
54
|
+
sha_format,
|
|
55
|
+
date_format,
|
|
56
|
+
author_format,
|
|
57
|
+
decoration_format,
|
|
58
|
+
subject_format,
|
|
59
|
+
].join(" ");
|
|
60
|
+
|
|
61
|
+
// view the SHA, description and history graph of last 20 commits
|
|
62
|
+
const rest_args = process_argv.slice(3).join(" ");
|
|
63
|
+
const command = [
|
|
64
|
+
`git log --pretty=format:"${format}" -n20 --graph --color ${rest_args}`,
|
|
65
|
+
`cut -c 1-"${truncation_width}"`,
|
|
66
|
+
`nl -w3 -s' '`,
|
|
67
|
+
].join(" | ");
|
|
68
|
+
|
|
69
|
+
const result = await cli(command);
|
|
70
|
+
|
|
71
|
+
actions.output(result.stdout);
|
|
72
|
+
}
|
|
@@ -2,6 +2,7 @@ import { Store } from "~/app/Store";
|
|
|
2
2
|
import * as Metadata from "~/core/Metadata";
|
|
3
3
|
import { cli } from "~/core/cli";
|
|
4
4
|
import * as github from "~/core/github";
|
|
5
|
+
import { invariant } from "~/core/invariant";
|
|
5
6
|
|
|
6
7
|
export type CommitMetadata = Awaited<ReturnType<typeof commit>>;
|
|
7
8
|
export type CommitRange = Awaited<ReturnType<typeof range>>;
|
|
@@ -166,6 +167,10 @@ export async function range(commit_group_map?: CommitGroupMap) {
|
|
|
166
167
|
|
|
167
168
|
async function get_commit_list() {
|
|
168
169
|
const master_branch = Store.getState().master_branch;
|
|
170
|
+
const branch_name = Store.getState().branch_name;
|
|
171
|
+
|
|
172
|
+
invariant(branch_name, "branch_name must exist");
|
|
173
|
+
|
|
169
174
|
const log_result = await cli(
|
|
170
175
|
`git log ${master_branch}..HEAD --oneline --format=%H --color=never`
|
|
171
176
|
);
|
|
@@ -178,12 +183,30 @@ async function get_commit_list() {
|
|
|
178
183
|
|
|
179
184
|
const commit_metadata_list = [];
|
|
180
185
|
|
|
186
|
+
let has_metadata = false;
|
|
187
|
+
|
|
181
188
|
for (let i = 0; i < sha_list.length; i++) {
|
|
182
189
|
const sha = sha_list[i];
|
|
183
190
|
const commit_metadata = await commit(sha);
|
|
191
|
+
|
|
192
|
+
if (commit_metadata.branch_id) {
|
|
193
|
+
has_metadata = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
184
196
|
commit_metadata_list.push(commit_metadata);
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
if (!has_metadata) {
|
|
200
|
+
// check for pr with matching branch name to initialize group
|
|
201
|
+
const pr_result = await github.pr_status(branch_name);
|
|
202
|
+
if (pr_result) {
|
|
203
|
+
for (const commit_metadata of commit_metadata_list) {
|
|
204
|
+
commit_metadata.branch_id = branch_name;
|
|
205
|
+
commit_metadata.title = pr_result.title;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
187
210
|
return commit_metadata_list;
|
|
188
211
|
}
|
|
189
212
|
|