@zjex/git-workflow 0.0.1 → 0.0.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 +220 -4
- package/dist/index.js +68 -35
- package/package.json +3 -1
- package/scripts/README.md +57 -0
- package/scripts/release.sh +363 -0
- package/src/commands/branch.ts +123 -28
- package/src/commands/commit.ts +263 -0
- package/src/commands/help.ts +26 -17
- package/src/commands/init.ts +40 -0
- package/src/commands/tag.ts +66 -7
- package/src/config.ts +20 -0
- package/src/index.ts +140 -3
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { select, input, confirm, checkbox } from "@inquirer/prompts";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { colors, theme, execOutput, divider } from "../utils.js";
|
|
5
|
+
import { getConfig } from "../config.js";
|
|
6
|
+
|
|
7
|
+
// Conventional Commits 类型 + Gitmoji
|
|
8
|
+
const DEFAULT_COMMIT_TYPES = [
|
|
9
|
+
{ type: "feat", emoji: "✨", description: "新功能" },
|
|
10
|
+
{ type: "fix", emoji: "🐛", description: "修复 Bug" },
|
|
11
|
+
{ type: "docs", emoji: "📝", description: "文档更新" },
|
|
12
|
+
{ type: "style", emoji: "💄", description: "代码格式 (不影响功能)" },
|
|
13
|
+
{ type: "refactor", emoji: "♻️", description: "重构 (非新功能/修复)" },
|
|
14
|
+
{ type: "perf", emoji: "⚡️", description: "性能优化" },
|
|
15
|
+
{ type: "test", emoji: "✅", description: "测试相关" },
|
|
16
|
+
{ type: "build", emoji: "📦", description: "构建/依赖相关" },
|
|
17
|
+
{ type: "ci", emoji: "👷", description: "CI/CD 相关" },
|
|
18
|
+
{ type: "chore", emoji: "🔧", description: "其他杂项" },
|
|
19
|
+
{ type: "revert", emoji: "⏪", description: "回退提交" },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
type CommitType = (typeof DEFAULT_COMMIT_TYPES)[number]["type"];
|
|
23
|
+
|
|
24
|
+
function getCommitTypes(config: ReturnType<typeof getConfig>) {
|
|
25
|
+
const customEmojis = config.commitEmojis || {};
|
|
26
|
+
return DEFAULT_COMMIT_TYPES.map((item) => ({
|
|
27
|
+
...item,
|
|
28
|
+
emoji: customEmojis[item.type as CommitType] || item.emoji,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FileStatus {
|
|
33
|
+
status: string;
|
|
34
|
+
file: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseGitStatus(): { staged: FileStatus[]; unstaged: FileStatus[] } {
|
|
38
|
+
const output = execOutput("git status --porcelain");
|
|
39
|
+
if (!output) return { staged: [], unstaged: [] };
|
|
40
|
+
|
|
41
|
+
const staged: FileStatus[] = [];
|
|
42
|
+
const unstaged: FileStatus[] = [];
|
|
43
|
+
|
|
44
|
+
for (const line of output.split("\n")) {
|
|
45
|
+
if (!line) continue;
|
|
46
|
+
const indexStatus = line[0];
|
|
47
|
+
const workTreeStatus = line[1];
|
|
48
|
+
const file = line.slice(3);
|
|
49
|
+
|
|
50
|
+
// 已暂存的更改 (index 有状态)
|
|
51
|
+
if (indexStatus !== " " && indexStatus !== "?") {
|
|
52
|
+
staged.push({ status: indexStatus, file });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 未暂存的更改 (work tree 有状态,或者是未跟踪文件)
|
|
56
|
+
if (workTreeStatus !== " " || indexStatus === "?") {
|
|
57
|
+
const status = indexStatus === "?" ? "?" : workTreeStatus;
|
|
58
|
+
unstaged.push({ status, file });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { staged, unstaged };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatFileStatus(status: string): string {
|
|
66
|
+
const statusMap: Record<string, string> = {
|
|
67
|
+
M: colors.yellow("M"),
|
|
68
|
+
A: colors.green("A"),
|
|
69
|
+
D: colors.red("D"),
|
|
70
|
+
R: colors.yellow("R"),
|
|
71
|
+
C: colors.yellow("C"),
|
|
72
|
+
"?": colors.green("?"),
|
|
73
|
+
};
|
|
74
|
+
return statusMap[status] || status;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function commit(): Promise<void> {
|
|
78
|
+
const config = getConfig();
|
|
79
|
+
let { staged, unstaged } = parseGitStatus();
|
|
80
|
+
|
|
81
|
+
// 没有暂存的更改
|
|
82
|
+
if (staged.length === 0) {
|
|
83
|
+
if (unstaged.length === 0) {
|
|
84
|
+
console.log(colors.yellow("工作区干净,没有需要提交的更改"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(colors.yellow("没有暂存的更改"));
|
|
89
|
+
divider();
|
|
90
|
+
console.log("未暂存的文件:");
|
|
91
|
+
for (const { status, file } of unstaged) {
|
|
92
|
+
console.log(` ${formatFileStatus(status)} ${file}`);
|
|
93
|
+
}
|
|
94
|
+
divider();
|
|
95
|
+
|
|
96
|
+
// 根据配置决定是否自动暂存
|
|
97
|
+
const autoStage = config.autoStage ?? true;
|
|
98
|
+
|
|
99
|
+
if (autoStage) {
|
|
100
|
+
// 自动暂存所有文件
|
|
101
|
+
execSync("git add -A", { stdio: "pipe" });
|
|
102
|
+
console.log(colors.green("✔ 已自动暂存所有更改"));
|
|
103
|
+
divider();
|
|
104
|
+
// 重新获取状态
|
|
105
|
+
const newStatus = parseGitStatus();
|
|
106
|
+
staged = newStatus.staged;
|
|
107
|
+
} else {
|
|
108
|
+
// 让用户选择要暂存的文件
|
|
109
|
+
const filesToStage = await checkbox({
|
|
110
|
+
message: "选择要暂存的文件:",
|
|
111
|
+
choices: unstaged.map(({ status, file }) => ({
|
|
112
|
+
name: `${formatFileStatus(status)} ${file}`,
|
|
113
|
+
value: file,
|
|
114
|
+
checked: true,
|
|
115
|
+
})),
|
|
116
|
+
theme,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (filesToStage.length === 0) {
|
|
120
|
+
console.log(colors.yellow("没有选择任何文件,已取消"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 暂存选中的文件
|
|
125
|
+
for (const file of filesToStage) {
|
|
126
|
+
execSync(`git add "${file}"`, { stdio: "pipe" });
|
|
127
|
+
}
|
|
128
|
+
console.log(colors.green(`✔ 已暂存 ${filesToStage.length} 个文件`));
|
|
129
|
+
divider();
|
|
130
|
+
|
|
131
|
+
// 重新获取状态
|
|
132
|
+
const newStatus = parseGitStatus();
|
|
133
|
+
staged = newStatus.staged;
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.log("已暂存的文件:");
|
|
137
|
+
for (const { status, file } of staged) {
|
|
138
|
+
console.log(` ${formatFileStatus(status)} ${file}`);
|
|
139
|
+
}
|
|
140
|
+
divider();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 获取提交类型(支持自定义 emoji)
|
|
144
|
+
const commitTypes = getCommitTypes(config);
|
|
145
|
+
|
|
146
|
+
// 选择提交类型
|
|
147
|
+
const typeChoice = await select({
|
|
148
|
+
message: "选择提交类型:",
|
|
149
|
+
choices: commitTypes.map((t) => ({
|
|
150
|
+
name: `${t.emoji} ${t.type.padEnd(10)} ${colors.dim(t.description)}`,
|
|
151
|
+
value: t,
|
|
152
|
+
})),
|
|
153
|
+
theme,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// 输入 scope (可选)
|
|
157
|
+
const scope = await input({
|
|
158
|
+
message: "输入影响范围 scope (可跳过):",
|
|
159
|
+
theme,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// 输入简短描述
|
|
163
|
+
const subject = await input({
|
|
164
|
+
message: "输入简短描述:",
|
|
165
|
+
validate: (value) => {
|
|
166
|
+
if (!value.trim()) return "描述不能为空";
|
|
167
|
+
if (value.length > 72) return "描述不能超过 72 个字符";
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
theme,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// 输入详细描述 (可选)
|
|
174
|
+
const body = await input({
|
|
175
|
+
message: "输入详细描述 (可跳过):",
|
|
176
|
+
theme,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 是否有破坏性变更
|
|
180
|
+
const hasBreaking = await confirm({
|
|
181
|
+
message: "是否包含破坏性变更 (BREAKING CHANGE)?",
|
|
182
|
+
default: false,
|
|
183
|
+
theme,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
let breakingDesc = "";
|
|
187
|
+
if (hasBreaking) {
|
|
188
|
+
breakingDesc = await input({
|
|
189
|
+
message: "描述破坏性变更:",
|
|
190
|
+
validate: (value) => (value.trim() ? true : "请描述破坏性变更"),
|
|
191
|
+
theme,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 关联 Issue (可选)
|
|
196
|
+
const issues = await input({
|
|
197
|
+
message: "关联 Issue (如 #123, 可跳过):",
|
|
198
|
+
theme,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 构建 commit message
|
|
202
|
+
const { type, emoji } = typeChoice;
|
|
203
|
+
const scopePart = scope ? `(${scope})` : "";
|
|
204
|
+
const breakingMark = hasBreaking ? "!" : "";
|
|
205
|
+
|
|
206
|
+
// 根据配置决定是否使用 emoji
|
|
207
|
+
const useEmoji = config.useEmoji ?? true;
|
|
208
|
+
const emojiPrefix = useEmoji ? `${emoji} ` : "";
|
|
209
|
+
|
|
210
|
+
// Header: [emoji] type(scope)!: subject
|
|
211
|
+
let message = `${emojiPrefix}${type}${scopePart}${breakingMark}: ${subject}`;
|
|
212
|
+
|
|
213
|
+
// Body
|
|
214
|
+
if (body || hasBreaking || issues) {
|
|
215
|
+
message += "\n";
|
|
216
|
+
|
|
217
|
+
if (body) {
|
|
218
|
+
message += `\n${body}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (hasBreaking) {
|
|
222
|
+
message += `\n\nBREAKING CHANGE: ${breakingDesc}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (issues) {
|
|
226
|
+
message += `\n\n${issues}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
divider();
|
|
231
|
+
console.log("提交信息预览:");
|
|
232
|
+
console.log(colors.green(message));
|
|
233
|
+
divider();
|
|
234
|
+
|
|
235
|
+
const shouldCommit = await confirm({
|
|
236
|
+
message: "确认提交?",
|
|
237
|
+
default: true,
|
|
238
|
+
theme,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!shouldCommit) {
|
|
242
|
+
console.log(colors.yellow("已取消"));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const spinner = ora("正在提交...").start();
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// 使用 -m 参数,需要转义引号
|
|
250
|
+
const escapedMessage = message.replace(/"/g, '\\"');
|
|
251
|
+
execSync(`git commit -m "${escapedMessage}"`, { stdio: "pipe" });
|
|
252
|
+
spinner.succeed("提交成功");
|
|
253
|
+
|
|
254
|
+
// 显示提交信息
|
|
255
|
+
const commitHash = execOutput("git rev-parse --short HEAD");
|
|
256
|
+
console.log(colors.dim(`commit: ${commitHash}`));
|
|
257
|
+
} catch (error) {
|
|
258
|
+
spinner.fail("提交失败");
|
|
259
|
+
if (error instanceof Error) {
|
|
260
|
+
console.log(colors.red(error.message));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/commands/help.ts
CHANGED
|
@@ -4,43 +4,52 @@ export function showHelp(): string {
|
|
|
4
4
|
return `
|
|
5
5
|
分支命令:
|
|
6
6
|
gw feature [--base <branch>] 创建 feature 分支
|
|
7
|
-
gw feat [--base <branch>] 同上 (
|
|
8
|
-
gw f [--base <branch>] 同上 (
|
|
7
|
+
gw feat [--base <branch>] 同上 (别名)
|
|
8
|
+
gw f [--base <branch>] 同上 (别名)
|
|
9
9
|
|
|
10
10
|
gw hotfix [--base <branch>] 创建 hotfix 分支
|
|
11
|
-
gw fix [--base <branch>] 同上 (
|
|
12
|
-
gw h [--base <branch>] 同上 (
|
|
11
|
+
gw fix [--base <branch>] 同上 (别名)
|
|
12
|
+
gw h [--base <branch>] 同上 (别名)
|
|
13
13
|
|
|
14
14
|
gw delete [branch] 删除本地/远程分支
|
|
15
|
-
gw del [branch] 同上 (
|
|
16
|
-
gw d [branch] 同上 (
|
|
15
|
+
gw del [branch] 同上 (别名)
|
|
16
|
+
gw d [branch] 同上 (别名)
|
|
17
17
|
|
|
18
18
|
Tag 命令:
|
|
19
19
|
gw tags [prefix] 列出所有 tag,可按前缀过滤
|
|
20
|
-
gw ts [prefix] 同上 (
|
|
20
|
+
gw ts [prefix] 同上 (别名)
|
|
21
21
|
|
|
22
22
|
gw tag [prefix] 交互式选择版本类型并创建 tag
|
|
23
|
-
gw t [prefix] 同上 (
|
|
23
|
+
gw t [prefix] 同上 (别名)
|
|
24
24
|
|
|
25
25
|
发布命令:
|
|
26
26
|
gw release 交互式选择版本号并更新 package.json
|
|
27
|
-
gw r 同上 (
|
|
27
|
+
gw r 同上 (别名)
|
|
28
28
|
|
|
29
29
|
配置命令:
|
|
30
30
|
gw init 初始化配置文件 .gwrc.json
|
|
31
31
|
|
|
32
32
|
Stash 命令:
|
|
33
33
|
gw stash 交互式管理 stash
|
|
34
|
-
gw s 同上 (
|
|
34
|
+
gw s 同上 (别名)
|
|
35
|
+
gw st 同上 (别名)
|
|
36
|
+
|
|
37
|
+
Commit 命令:
|
|
38
|
+
gw commit 交互式提交 (Conventional Commits + Gitmoji)
|
|
39
|
+
gw c 同上 (别名)
|
|
40
|
+
gw cm 同上 (别名)
|
|
35
41
|
|
|
36
42
|
示例:
|
|
37
|
-
gw
|
|
38
|
-
gw
|
|
39
|
-
gw
|
|
40
|
-
gw
|
|
41
|
-
gw
|
|
42
|
-
gw
|
|
43
|
-
gw
|
|
43
|
+
gw f 基于 main/master 创建 feature 分支
|
|
44
|
+
gw f --base develop 基于 develop 分支创建 feature 分支
|
|
45
|
+
gw h --base release 基于 release 分支创建 hotfix 分支
|
|
46
|
+
gw d 交互式选择并删除分支
|
|
47
|
+
gw d feature/xxx 直接删除指定分支
|
|
48
|
+
gw ts v 列出所有 v 开头的 tag
|
|
49
|
+
gw t 交互式创建 tag
|
|
50
|
+
gw r 交互式发布版本
|
|
51
|
+
gw s 交互式管理 stash
|
|
52
|
+
gw c 交互式提交代码
|
|
44
53
|
|
|
45
54
|
分支命名格式:
|
|
46
55
|
feature/${TODAY}-<Story ID>-<描述>
|
package/src/commands/init.ts
CHANGED
|
@@ -5,6 +5,21 @@ import type { GwConfig } from "../config.js";
|
|
|
5
5
|
|
|
6
6
|
const CONFIG_FILE = ".gwrc.json";
|
|
7
7
|
|
|
8
|
+
// 默认的 commit emoji 配置
|
|
9
|
+
const DEFAULT_COMMIT_EMOJIS = {
|
|
10
|
+
feat: "✨",
|
|
11
|
+
fix: "🐛",
|
|
12
|
+
docs: "📝",
|
|
13
|
+
style: "💄",
|
|
14
|
+
refactor: "♻️",
|
|
15
|
+
perf: "⚡️",
|
|
16
|
+
test: "✅",
|
|
17
|
+
build: "📦",
|
|
18
|
+
ci: "👷",
|
|
19
|
+
chore: "🔧",
|
|
20
|
+
revert: "⏪",
|
|
21
|
+
};
|
|
22
|
+
|
|
8
23
|
export async function init(): Promise<void> {
|
|
9
24
|
if (existsSync(CONFIG_FILE)) {
|
|
10
25
|
const overwrite = await confirm({
|
|
@@ -94,10 +109,35 @@ export async function init(): Promise<void> {
|
|
|
94
109
|
|
|
95
110
|
divider();
|
|
96
111
|
|
|
112
|
+
// Commit 配置
|
|
113
|
+
const autoStage = await confirm({
|
|
114
|
+
message: "Commit 时是否自动暂存所有更改?",
|
|
115
|
+
default: true,
|
|
116
|
+
theme,
|
|
117
|
+
});
|
|
118
|
+
if (!autoStage) config.autoStage = false;
|
|
119
|
+
|
|
120
|
+
const useEmoji = await confirm({
|
|
121
|
+
message: "Commit 时是否使用 emoji?",
|
|
122
|
+
default: true,
|
|
123
|
+
theme,
|
|
124
|
+
});
|
|
125
|
+
if (!useEmoji) config.useEmoji = false;
|
|
126
|
+
|
|
127
|
+
// 始终写入默认的 commitEmojis 配置,方便用户修改
|
|
128
|
+
config.commitEmojis = DEFAULT_COMMIT_EMOJIS;
|
|
129
|
+
|
|
130
|
+
divider();
|
|
131
|
+
|
|
97
132
|
// 写入配置
|
|
98
133
|
const content = JSON.stringify(config, null, 2);
|
|
99
134
|
writeFileSync(CONFIG_FILE, content + "\n");
|
|
100
135
|
|
|
101
136
|
console.log(colors.green(`✓ 配置已保存到 ${CONFIG_FILE}`));
|
|
137
|
+
console.log(
|
|
138
|
+
colors.dim(
|
|
139
|
+
"\n提示: 可以在配置文件中修改 commitEmojis 来自定义各类型的 emoji"
|
|
140
|
+
)
|
|
141
|
+
);
|
|
102
142
|
console.log(colors.dim("\n" + content));
|
|
103
143
|
}
|
package/src/commands/tag.ts
CHANGED
|
@@ -42,6 +42,14 @@ interface TagChoice {
|
|
|
42
42
|
value: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// 获取指定前缀的最新 tag(不依赖 shell 管道)
|
|
46
|
+
function getLatestTag(prefix: string): string {
|
|
47
|
+
const tags = execOutput(`git tag -l "${prefix}*" --sort=-v:refname`)
|
|
48
|
+
.split("\n")
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
return tags[0] || "";
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
export async function createTag(inputPrefix?: string): Promise<void> {
|
|
46
54
|
const config = getConfig();
|
|
47
55
|
const fetchSpinner = ora("正在获取 tags...").start();
|
|
@@ -60,13 +68,68 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
60
68
|
|
|
61
69
|
if (!prefix) {
|
|
62
70
|
const allTags = execOutput("git tag -l").split("\n").filter(Boolean);
|
|
71
|
+
|
|
72
|
+
// 仓库没有任何 tag 的情况
|
|
73
|
+
if (allTags.length === 0) {
|
|
74
|
+
prefix = await input({
|
|
75
|
+
message: "当前仓库没有 tag,请输入前缀 (如 v):",
|
|
76
|
+
default: "v",
|
|
77
|
+
theme,
|
|
78
|
+
});
|
|
79
|
+
if (!prefix) {
|
|
80
|
+
console.log(colors.yellow("已取消"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 选择初始版本号
|
|
85
|
+
const initialVersion = await select({
|
|
86
|
+
message: "选择初始版本号:",
|
|
87
|
+
choices: [
|
|
88
|
+
{ name: `${prefix}0.0.1`, value: "0.0.1" },
|
|
89
|
+
{ name: `${prefix}0.1.0`, value: "0.1.0" },
|
|
90
|
+
{ name: `${prefix}1.0.0`, value: "1.0.0" },
|
|
91
|
+
{ name: "自定义...", value: "__custom__" },
|
|
92
|
+
],
|
|
93
|
+
theme,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let version = initialVersion;
|
|
97
|
+
if (initialVersion === "__custom__") {
|
|
98
|
+
version = await input({
|
|
99
|
+
message: "请输入版本号 (如 0.0.1):",
|
|
100
|
+
theme,
|
|
101
|
+
});
|
|
102
|
+
if (!version) {
|
|
103
|
+
console.log(colors.yellow("已取消"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const newTag = `${prefix}${version}`;
|
|
109
|
+
const ok = await select({
|
|
110
|
+
message: `确认创建 ${newTag}?`,
|
|
111
|
+
choices: [
|
|
112
|
+
{ name: "是", value: true },
|
|
113
|
+
{ name: "否", value: false },
|
|
114
|
+
],
|
|
115
|
+
theme,
|
|
116
|
+
});
|
|
117
|
+
if (ok) {
|
|
118
|
+
doCreateTag(newTag);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 从现有 tag 中提取前缀
|
|
63
124
|
const prefixes = [
|
|
64
125
|
...new Set(allTags.map((t) => t.replace(/[0-9].*/, "")).filter(Boolean)),
|
|
65
126
|
];
|
|
66
127
|
|
|
67
128
|
if (prefixes.length === 0) {
|
|
129
|
+
// 有 tag 但无法提取前缀(比如纯数字 tag)
|
|
68
130
|
prefix = await input({
|
|
69
|
-
message: "
|
|
131
|
+
message: "请输入 tag 前缀 (如 v):",
|
|
132
|
+
default: "v",
|
|
70
133
|
theme,
|
|
71
134
|
});
|
|
72
135
|
if (!prefix) {
|
|
@@ -75,9 +138,7 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
75
138
|
}
|
|
76
139
|
} else {
|
|
77
140
|
const prefixWithDate: PrefixInfo[] = prefixes.map((p) => {
|
|
78
|
-
const latest =
|
|
79
|
-
`git tag -l "${p}*" --sort=-v:refname | head -1`
|
|
80
|
-
);
|
|
141
|
+
const latest = getLatestTag(p);
|
|
81
142
|
const date = latest
|
|
82
143
|
? execOutput(`git log -1 --format=%ct "${latest}" 2>/dev/null`)
|
|
83
144
|
: "0";
|
|
@@ -108,9 +169,7 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
108
169
|
}
|
|
109
170
|
}
|
|
110
171
|
|
|
111
|
-
const latestTag =
|
|
112
|
-
`git tag -l "${prefix}*" --sort=-v:refname | head -1`
|
|
113
|
-
);
|
|
172
|
+
const latestTag = getLatestTag(prefix);
|
|
114
173
|
|
|
115
174
|
if (!latestTag) {
|
|
116
175
|
const newTag = `${prefix}1.0.0`;
|
package/src/config.ts
CHANGED
|
@@ -18,6 +18,24 @@ export interface GwConfig {
|
|
|
18
18
|
defaultTagPrefix?: string;
|
|
19
19
|
// 创建分支后是否自动推送,默认询问
|
|
20
20
|
autoPush?: boolean;
|
|
21
|
+
// commit 时是否自动暂存所有更改,默认 true
|
|
22
|
+
autoStage?: boolean;
|
|
23
|
+
// commit 时是否使用 emoji,默认 true
|
|
24
|
+
useEmoji?: boolean;
|
|
25
|
+
// 自定义 commit 类型的 emoji
|
|
26
|
+
commitEmojis?: {
|
|
27
|
+
feat?: string;
|
|
28
|
+
fix?: string;
|
|
29
|
+
docs?: string;
|
|
30
|
+
style?: string;
|
|
31
|
+
refactor?: string;
|
|
32
|
+
perf?: string;
|
|
33
|
+
test?: string;
|
|
34
|
+
build?: string;
|
|
35
|
+
ci?: string;
|
|
36
|
+
chore?: string;
|
|
37
|
+
revert?: string;
|
|
38
|
+
};
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
const defaultConfig: GwConfig = {
|
|
@@ -26,6 +44,8 @@ const defaultConfig: GwConfig = {
|
|
|
26
44
|
requireId: false,
|
|
27
45
|
featureIdLabel: "Story ID",
|
|
28
46
|
hotfixIdLabel: "Issue ID",
|
|
47
|
+
autoStage: true,
|
|
48
|
+
useEmoji: true,
|
|
29
49
|
};
|
|
30
50
|
|
|
31
51
|
function getGitRoot(): string {
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,146 @@
|
|
|
1
1
|
// @ts-nocheck shebang handled by tsup banner
|
|
2
2
|
|
|
3
3
|
import { cac } from "cac";
|
|
4
|
-
import {
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import { ExitPromptError } from "@inquirer/core";
|
|
6
|
+
import { checkGitRepo, theme, colors } from "./utils.js";
|
|
5
7
|
import { createBranch, deleteBranch } from "./commands/branch.js";
|
|
6
8
|
import { listTags, createTag } from "./commands/tag.js";
|
|
7
9
|
import { release } from "./commands/release.js";
|
|
8
10
|
import { init } from "./commands/init.js";
|
|
9
11
|
import { stash } from "./commands/stash.js";
|
|
12
|
+
import { commit } from "./commands/commit.js";
|
|
10
13
|
import { showHelp } from "./commands/help.js";
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
// 捕获 Ctrl+C 退出,静默处理
|
|
16
|
+
process.on("uncaughtException", (err) => {
|
|
17
|
+
if (err instanceof ExitPromptError) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
console.error(err);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
declare const __VERSION__: string | undefined;
|
|
25
|
+
|
|
26
|
+
// 开发环境下从 package.json 读取版本号
|
|
27
|
+
const version: string =
|
|
28
|
+
typeof __VERSION__ !== "undefined"
|
|
29
|
+
? __VERSION__
|
|
30
|
+
: (await import("../package.json", { with: { type: "json" } })).default
|
|
31
|
+
.version;
|
|
32
|
+
|
|
33
|
+
// 交互式主菜单
|
|
34
|
+
async function mainMenu(): Promise<void> {
|
|
35
|
+
// ASCII Art Logo
|
|
36
|
+
console.log(
|
|
37
|
+
colors.green(`
|
|
38
|
+
███████╗ ██╗███████╗██╗ ██╗
|
|
39
|
+
╚══███╔╝ ██║██╔════╝╚██╗██╔╝
|
|
40
|
+
███╔╝ ██║█████╗ ╚███╔╝
|
|
41
|
+
███╔╝ ██ ██║██╔══╝ ██╔██╗
|
|
42
|
+
███████╗╚█████╔╝███████╗██╔╝ ██╗
|
|
43
|
+
╚══════╝ ╚════╝ ╚══════╝╚═╝ ╚═╝
|
|
44
|
+
`)
|
|
45
|
+
);
|
|
46
|
+
console.log(colors.dim(` git-workflow v${version}\n`));
|
|
47
|
+
|
|
48
|
+
const action = await select({
|
|
49
|
+
message: "选择操作:",
|
|
50
|
+
choices: [
|
|
51
|
+
{
|
|
52
|
+
name: `[1] ✨ 创建 feature 分支 ${colors.dim("gw f")}`,
|
|
53
|
+
value: "feature",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: `[2] 🐛 创建 hotfix 分支 ${colors.dim("gw h")}`,
|
|
57
|
+
value: "hotfix",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: `[3] 🗑️ 删除分支 ${colors.dim("gw d")}`,
|
|
61
|
+
value: "delete",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: `[4] 📝 提交代码 ${colors.dim("gw c")}`,
|
|
65
|
+
value: "commit",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: `[5] 🏷️ 创建 tag ${colors.dim("gw t")}`,
|
|
69
|
+
value: "tag",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: `[6] 📋 列出 tags ${colors.dim("gw ts")}`,
|
|
73
|
+
value: "tags",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: `[7] 📦 发布版本 ${colors.dim("gw r")}`,
|
|
77
|
+
value: "release",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: `[8] 💾 管理 stash ${colors.dim("gw s")}`,
|
|
81
|
+
value: "stash",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: `[9] ⚙️ 初始化配置 ${colors.dim("gw init")}`,
|
|
85
|
+
value: "init",
|
|
86
|
+
},
|
|
87
|
+
{ name: "[0] ❓ 帮助", value: "help" },
|
|
88
|
+
{ name: "[q] 退出", value: "exit" },
|
|
89
|
+
],
|
|
90
|
+
loop: false,
|
|
91
|
+
theme,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
switch (action) {
|
|
95
|
+
case "feature":
|
|
96
|
+
checkGitRepo();
|
|
97
|
+
await createBranch("feature");
|
|
98
|
+
break;
|
|
99
|
+
case "hotfix":
|
|
100
|
+
checkGitRepo();
|
|
101
|
+
await createBranch("hotfix");
|
|
102
|
+
break;
|
|
103
|
+
case "delete":
|
|
104
|
+
checkGitRepo();
|
|
105
|
+
await deleteBranch();
|
|
106
|
+
break;
|
|
107
|
+
case "tag":
|
|
108
|
+
checkGitRepo();
|
|
109
|
+
await createTag();
|
|
110
|
+
break;
|
|
111
|
+
case "tags":
|
|
112
|
+
checkGitRepo();
|
|
113
|
+
await listTags();
|
|
114
|
+
break;
|
|
115
|
+
case "commit":
|
|
116
|
+
checkGitRepo();
|
|
117
|
+
await commit();
|
|
118
|
+
break;
|
|
119
|
+
case "release":
|
|
120
|
+
await release();
|
|
121
|
+
break;
|
|
122
|
+
case "stash":
|
|
123
|
+
checkGitRepo();
|
|
124
|
+
await stash();
|
|
125
|
+
break;
|
|
126
|
+
case "init":
|
|
127
|
+
await init();
|
|
128
|
+
break;
|
|
129
|
+
case "help":
|
|
130
|
+
console.log(showHelp());
|
|
131
|
+
break;
|
|
132
|
+
case "exit":
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
13
136
|
|
|
14
137
|
const cli = cac("gw");
|
|
15
138
|
|
|
139
|
+
// 默认命令 - 显示交互式菜单
|
|
140
|
+
cli.command("", "显示交互式菜单").action(() => {
|
|
141
|
+
return mainMenu();
|
|
142
|
+
});
|
|
143
|
+
|
|
16
144
|
cli
|
|
17
145
|
.command("feature", "创建 feature 分支")
|
|
18
146
|
.alias("feat")
|
|
@@ -78,11 +206,20 @@ cli
|
|
|
78
206
|
return stash();
|
|
79
207
|
});
|
|
80
208
|
|
|
209
|
+
cli
|
|
210
|
+
.command("commit", "交互式提交 (Conventional Commits + Gitmoji)")
|
|
211
|
+
.alias("c")
|
|
212
|
+
.alias("cm")
|
|
213
|
+
.action(() => {
|
|
214
|
+
checkGitRepo();
|
|
215
|
+
return commit();
|
|
216
|
+
});
|
|
217
|
+
|
|
81
218
|
cli.help((sections) => {
|
|
82
219
|
sections.push({
|
|
83
220
|
body: showHelp(),
|
|
84
221
|
});
|
|
85
222
|
});
|
|
86
|
-
cli.version(
|
|
223
|
+
cli.version(version);
|
|
87
224
|
|
|
88
225
|
cli.parse();
|