@zjex/git-workflow 0.3.2 → 0.3.4
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/.husky/commit-msg +5 -0
- package/.husky/pre-commit +0 -6
- package/README.md +1 -1
- package/ROADMAP.md +142 -61
- package/dist/index.js +120 -36
- package/docs/.vitepress/cache/deps/_metadata.json +9 -9
- package/docs/.vitepress/config.ts +2 -1
- package/docs/commands/help.md +248 -0
- package/docs/commands/index.md +15 -8
- package/docs/commands/log.md +328 -0
- package/docs/index.md +32 -1
- package/package.json +1 -1
- package/scripts/format-commit-msg.js +258 -0
- package/src/ai-service.ts +69 -14
- package/src/commands/commit.ts +17 -5
- package/src/commands/init.ts +23 -0
- package/src/commands/tag.ts +32 -22
- package/src/config.ts +1 -0
- package/tests/commit-format.test.ts +535 -0
package/src/ai-service.ts
CHANGED
|
@@ -60,7 +60,7 @@ function getGitDiff(): string {
|
|
|
60
60
|
/**
|
|
61
61
|
* 构建 AI prompt
|
|
62
62
|
*/
|
|
63
|
-
function buildPrompt(diff: string, language: string, detailedDescription: boolean = false): string {
|
|
63
|
+
function buildPrompt(diff: string, language: string, detailedDescription: boolean = false, useEmoji: boolean = true): string {
|
|
64
64
|
const isZh = language === "zh-CN";
|
|
65
65
|
|
|
66
66
|
if (detailedDescription) {
|
|
@@ -69,7 +69,7 @@ function buildPrompt(diff: string, language: string, detailedDescription: boolea
|
|
|
69
69
|
? `你是一个专业的 Git commit message 生成助手。请根据提供的 git diff 生成符合 Conventional Commits 规范的详细 commit message。
|
|
70
70
|
|
|
71
71
|
格式要求:
|
|
72
|
-
1.
|
|
72
|
+
1. 第一行:${useEmoji ? '<emoji> ' : ''}<type>(<scope>): <subject>
|
|
73
73
|
2. 空行
|
|
74
74
|
3. 详细描述:列出主要修改点,每个修改点一行,以 "- " 开头
|
|
75
75
|
|
|
@@ -80,9 +80,22 @@ function buildPrompt(diff: string, language: string, detailedDescription: boolea
|
|
|
80
80
|
- 详细描述要列出 3-6 个主要修改点,每个修改点简洁明了
|
|
81
81
|
- 如果修改较少,可以只列出 2-3 个修改点
|
|
82
82
|
- 不要有其他解释或多余内容
|
|
83
|
+
${useEmoji ? `
|
|
84
|
+
Emoji 映射规则:
|
|
85
|
+
- feat: ✨ (新功能)
|
|
86
|
+
- fix: 🐛 (修复Bug)
|
|
87
|
+
- docs: 📝 (文档)
|
|
88
|
+
- style: 💄 (代码格式)
|
|
89
|
+
- refactor: ♻️ (重构)
|
|
90
|
+
- perf: ⚡️ (性能优化)
|
|
91
|
+
- test: ✅ (测试)
|
|
92
|
+
- build: 📦 (构建)
|
|
93
|
+
- ci: 👷 (CI/CD)
|
|
94
|
+
- chore: 🔧 (其他杂项)
|
|
95
|
+
- revert: ⏪ (回滚)` : ''}
|
|
83
96
|
|
|
84
97
|
示例:
|
|
85
|
-
feat(auth): 添加用户登录功能
|
|
98
|
+
${useEmoji ? '✨ ' : ''}feat(auth): 添加用户登录功能
|
|
86
99
|
|
|
87
100
|
- 实现用户名密码登录接口
|
|
88
101
|
- 添加登录状态验证中间件
|
|
@@ -91,7 +104,7 @@ feat(auth): 添加用户登录功能
|
|
|
91
104
|
: `You are a professional Git commit message generator. Generate a detailed commit message following Conventional Commits specification based on the provided git diff.
|
|
92
105
|
|
|
93
106
|
Format requirements:
|
|
94
|
-
1. First line: <type>(<scope>): <subject>
|
|
107
|
+
1. First line: ${useEmoji ? '<emoji> ' : ''}<type>(<scope>): <subject>
|
|
95
108
|
2. Empty line
|
|
96
109
|
3. Detailed description: List main changes, one per line, starting with "- "
|
|
97
110
|
|
|
@@ -102,9 +115,22 @@ Rules:
|
|
|
102
115
|
- Detailed description should list 3-6 main changes, each change should be concise
|
|
103
116
|
- If changes are minimal, list 2-3 changes
|
|
104
117
|
- No explanations or extra content
|
|
118
|
+
${useEmoji ? `
|
|
119
|
+
Emoji mapping rules:
|
|
120
|
+
- feat: ✨ (new feature)
|
|
121
|
+
- fix: 🐛 (bug fix)
|
|
122
|
+
- docs: 📝 (documentation)
|
|
123
|
+
- style: 💄 (code style)
|
|
124
|
+
- refactor: ♻️ (refactoring)
|
|
125
|
+
- perf: ⚡️ (performance)
|
|
126
|
+
- test: ✅ (testing)
|
|
127
|
+
- build: 📦 (build)
|
|
128
|
+
- ci: 👷 (CI/CD)
|
|
129
|
+
- chore: 🔧 (chore)
|
|
130
|
+
- revert: ⏪ (revert)` : ''}
|
|
105
131
|
|
|
106
132
|
Example:
|
|
107
|
-
feat(auth): add user login functionality
|
|
133
|
+
${useEmoji ? '✨ ' : ''}feat(auth): add user login functionality
|
|
108
134
|
|
|
109
135
|
- Implement username/password login API
|
|
110
136
|
- Add login status validation middleware
|
|
@@ -122,31 +148,57 @@ feat(auth): add user login functionality
|
|
|
122
148
|
? `你是一个专业的 Git commit message 生成助手。请根据提供的 git diff 生成符合 Conventional Commits 规范的 commit message。
|
|
123
149
|
|
|
124
150
|
规则:
|
|
125
|
-
1.
|
|
151
|
+
1. 格式:${useEmoji ? '<emoji> ' : ''}<type>(<scope>): <subject>
|
|
126
152
|
2. type 必须是以下之一:feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
127
153
|
3. scope 是可选的,表示影响范围
|
|
128
154
|
4. subject 用中文描述,简洁明了,不超过 50 字
|
|
129
155
|
5. 只返回一条 commit message,即使有多个文件改动也要总结成一条
|
|
130
156
|
6. 不要有其他解释或多余内容
|
|
157
|
+
${useEmoji ? `
|
|
158
|
+
Emoji 映射规则:
|
|
159
|
+
- feat: ✨ (新功能)
|
|
160
|
+
- fix: 🐛 (修复Bug)
|
|
161
|
+
- docs: 📝 (文档)
|
|
162
|
+
- style: 💄 (代码格式)
|
|
163
|
+
- refactor: ♻️ (重构)
|
|
164
|
+
- perf: ⚡️ (性能优化)
|
|
165
|
+
- test: ✅ (测试)
|
|
166
|
+
- build: 📦 (构建)
|
|
167
|
+
- ci: 👷 (CI/CD)
|
|
168
|
+
- chore: 🔧 (其他杂项)
|
|
169
|
+
- revert: ⏪ (回滚)` : ''}
|
|
131
170
|
|
|
132
171
|
示例:
|
|
133
|
-
- feat(auth): 添加用户登录功能
|
|
134
|
-
- fix(api): 修复数据获取失败的问题
|
|
135
|
-
- docs(readme): 更新安装说明`
|
|
172
|
+
- ${useEmoji ? '✨ ' : ''}feat(auth): 添加用户登录功能
|
|
173
|
+
- ${useEmoji ? '🐛 ' : ''}fix(api): 修复数据获取失败的问题
|
|
174
|
+
- ${useEmoji ? '📝 ' : ''}docs(readme): 更新安装说明`
|
|
136
175
|
: `You are a professional Git commit message generator. Generate a commit message following Conventional Commits specification based on the provided git diff.
|
|
137
176
|
|
|
138
177
|
Rules:
|
|
139
|
-
1. Format: <type>(<scope>): <subject>
|
|
178
|
+
1. Format: ${useEmoji ? '<emoji> ' : ''}<type>(<scope>): <subject>
|
|
140
179
|
2. type must be one of: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
141
180
|
3. scope is optional, indicates the affected area
|
|
142
181
|
4. subject should be concise, no more than 50 characters
|
|
143
182
|
5. Return only ONE commit message, even if multiple files are changed, summarize into one message
|
|
144
183
|
6. No explanations or extra content
|
|
184
|
+
${useEmoji ? `
|
|
185
|
+
Emoji mapping rules:
|
|
186
|
+
- feat: ✨ (new feature)
|
|
187
|
+
- fix: 🐛 (bug fix)
|
|
188
|
+
- docs: 📝 (documentation)
|
|
189
|
+
- style: 💄 (code style)
|
|
190
|
+
- refactor: ♻️ (refactoring)
|
|
191
|
+
- perf: ⚡️ (performance)
|
|
192
|
+
- test: ✅ (testing)
|
|
193
|
+
- build: 📦 (build)
|
|
194
|
+
- ci: 👷 (CI/CD)
|
|
195
|
+
- chore: 🔧 (chore)
|
|
196
|
+
- revert: ⏪ (revert)` : ''}
|
|
145
197
|
|
|
146
198
|
Examples:
|
|
147
|
-
- feat(auth): add user login functionality
|
|
148
|
-
- fix(api): resolve data fetching failure
|
|
149
|
-
- docs(readme): update installation guide`;
|
|
199
|
+
- ${useEmoji ? '✨ ' : ''}feat(auth): add user login functionality
|
|
200
|
+
- ${useEmoji ? '🐛 ' : ''}fix(api): resolve data fetching failure
|
|
201
|
+
- ${useEmoji ? '📝 ' : ''}docs(readme): update installation guide`;
|
|
150
202
|
|
|
151
203
|
const userPrompt = isZh
|
|
152
204
|
? `请根据以下 git diff 生成 commit message:\n\n${diff}`
|
|
@@ -327,6 +379,9 @@ export async function generateAICommitMessage(
|
|
|
327
379
|
const language = aiConfig.language || "zh-CN";
|
|
328
380
|
const detailedDescription = aiConfig.detailedDescription !== false; // 默认启用详细描述
|
|
329
381
|
const maxTokens = aiConfig.maxTokens || (detailedDescription ? 400 : 200);
|
|
382
|
+
|
|
383
|
+
// AI emoji配置:优先使用aiCommit.useEmoji,如果未设置则使用全局useEmoji,默认true
|
|
384
|
+
const useEmoji = aiConfig.useEmoji !== undefined ? aiConfig.useEmoji : (config.useEmoji !== false);
|
|
330
385
|
|
|
331
386
|
// 获取 git diff
|
|
332
387
|
const diff = getGitDiff();
|
|
@@ -340,7 +395,7 @@ export async function generateAICommitMessage(
|
|
|
340
395
|
diff.length > maxDiffLength ? diff.slice(0, maxDiffLength) + "\n..." : diff;
|
|
341
396
|
|
|
342
397
|
// 构建 prompt
|
|
343
|
-
const prompt = buildPrompt(truncatedDiff, language, detailedDescription);
|
|
398
|
+
const prompt = buildPrompt(truncatedDiff, language, detailedDescription, useEmoji);
|
|
344
399
|
|
|
345
400
|
// 根据提供商调用对应的 API
|
|
346
401
|
const providerInfo = AI_PROVIDERS[provider];
|
package/src/commands/commit.ts
CHANGED
|
@@ -265,7 +265,17 @@ export async function commit(): Promise<void> {
|
|
|
265
265
|
|
|
266
266
|
try {
|
|
267
267
|
// 提交前再次检查是否有暂存的文件
|
|
268
|
-
|
|
268
|
+
let finalStatus = parseGitStatus();
|
|
269
|
+
|
|
270
|
+
// 如果暂存区为空,但有未暂存的更改,且开启了自动暂存,则重新暂存
|
|
271
|
+
if (finalStatus.staged.length === 0 && finalStatus.unstaged.length > 0) {
|
|
272
|
+
const autoStage = config.autoStage ?? true;
|
|
273
|
+
if (autoStage) {
|
|
274
|
+
execSync("git add -A", { stdio: "pipe" });
|
|
275
|
+
finalStatus = parseGitStatus();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
269
279
|
if (finalStatus.staged.length === 0) {
|
|
270
280
|
spinner.fail("没有暂存的文件可以提交");
|
|
271
281
|
console.log("");
|
|
@@ -277,9 +287,11 @@ export async function commit(): Promise<void> {
|
|
|
277
287
|
return;
|
|
278
288
|
}
|
|
279
289
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
execSync(`git commit -
|
|
290
|
+
// 处理多行消息:使用 git commit -F - 通过 stdin 传递
|
|
291
|
+
// 这样可以正确处理包含换行符的 commit message
|
|
292
|
+
execSync(`git commit -F -`, {
|
|
293
|
+
input: message,
|
|
294
|
+
});
|
|
283
295
|
spinner.succeed("提交成功");
|
|
284
296
|
|
|
285
297
|
// 显示提交信息
|
|
@@ -300,7 +312,7 @@ export async function commit(): Promise<void> {
|
|
|
300
312
|
console.log(colors.yellow("你可以手动执行以下命令:"));
|
|
301
313
|
console.log(colors.cyan(` git commit -m "${message}"`));
|
|
302
314
|
console.log("");
|
|
303
|
-
|
|
315
|
+
|
|
304
316
|
// 重新抛出错误,让调用者知道提交失败了
|
|
305
317
|
throw error;
|
|
306
318
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -283,12 +283,35 @@ export async function init(): Promise<void> {
|
|
|
283
283
|
theme,
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
const aiUseEmoji = await select({
|
|
287
|
+
message: "AI 生成的 commit message 是否包含 emoji?",
|
|
288
|
+
choices: [
|
|
289
|
+
{
|
|
290
|
+
name: "是(推荐)",
|
|
291
|
+
value: true,
|
|
292
|
+
description: "如:✨ feat(auth): 添加用户登录功能"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "否",
|
|
296
|
+
value: false,
|
|
297
|
+
description: "如:feat(auth): 添加用户登录功能"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "跟随全局设置",
|
|
301
|
+
value: undefined,
|
|
302
|
+
description: `当前全局设置:${useEmoji ? '启用' : '禁用'} emoji`
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
theme,
|
|
306
|
+
});
|
|
307
|
+
|
|
286
308
|
config.aiCommit = {
|
|
287
309
|
enabled: true,
|
|
288
310
|
provider: aiProvider as "github" | "openai" | "claude" | "ollama",
|
|
289
311
|
apiKey: apiKey || undefined,
|
|
290
312
|
language: language as "zh-CN" | "en-US",
|
|
291
313
|
detailedDescription,
|
|
314
|
+
useEmoji: aiUseEmoji,
|
|
292
315
|
};
|
|
293
316
|
|
|
294
317
|
// 根据提供商设置默认模型
|
package/src/commands/tag.ts
CHANGED
|
@@ -16,11 +16,15 @@ export async function listTags(prefix?: string): Promise<void> {
|
|
|
16
16
|
|
|
17
17
|
// 2. 获取 tags 列表(按版本号升序排序,最新的在最后)
|
|
18
18
|
const pattern = prefix ? `${prefix}*` : "";
|
|
19
|
-
const
|
|
19
|
+
const allTags = execOutput(`git tag -l ${pattern} --sort=v:refname`)
|
|
20
20
|
.split("\n")
|
|
21
21
|
.filter(Boolean);
|
|
22
22
|
|
|
23
|
-
// 3.
|
|
23
|
+
// 3. 过滤无效 tag(如 vnull、vundefined 等误操作产生的 tag)
|
|
24
|
+
// 有效 tag 必须包含数字(版本号)
|
|
25
|
+
const tags = allTags.filter((tag) => /\d/.test(tag));
|
|
26
|
+
|
|
27
|
+
// 4. 如果没有 tags,提示并返回
|
|
24
28
|
if (tags.length === 0) {
|
|
25
29
|
console.log(
|
|
26
30
|
colors.yellow(prefix ? `没有 '${prefix}' 开头的 tag` : "没有 tag")
|
|
@@ -28,36 +32,39 @@ export async function listTags(prefix?: string): Promise<void> {
|
|
|
28
32
|
return;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
//
|
|
35
|
+
// 5. 如果指定了前缀,直接显示单列(最多 5 个)
|
|
32
36
|
if (prefix) {
|
|
33
|
-
console.log(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (tags.length >
|
|
37
|
-
console.log(colors.
|
|
37
|
+
console.log(
|
|
38
|
+
colors.green(`以 '${prefix}' 开头的 tags (共 ${tags.length} 个):`)
|
|
39
|
+
);
|
|
40
|
+
if (tags.length > 5) {
|
|
41
|
+
console.log(colors.dim(" ..."));
|
|
38
42
|
}
|
|
43
|
+
const displayTags = tags.slice(-5);
|
|
44
|
+
displayTags.forEach((tag) => console.log(` ${tag}`));
|
|
39
45
|
return;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
//
|
|
48
|
+
// 6. 按前缀分组(提取 tag 名称中数字前的部分作为前缀)
|
|
49
|
+
// 由于已过滤无效 tag,所有 tag 都包含数字
|
|
43
50
|
const grouped = new Map<string, string[]>();
|
|
44
51
|
tags.forEach((tag) => {
|
|
45
|
-
//
|
|
46
|
-
const prefix = tag.replace(
|
|
52
|
+
// 提取数字之前的字母部分作为前缀(如 "v0.1.0" -> "v")
|
|
53
|
+
const prefix = tag.replace(/\d.*/, "") || "(无前缀)";
|
|
47
54
|
if (!grouped.has(prefix)) {
|
|
48
55
|
grouped.set(prefix, []);
|
|
49
56
|
}
|
|
50
57
|
grouped.get(prefix)!.push(tag);
|
|
51
58
|
});
|
|
52
59
|
|
|
53
|
-
//
|
|
60
|
+
// 7. 如果只有一个前缀,使用单列显示(最多 5 个)
|
|
54
61
|
if (grouped.size === 1) {
|
|
55
|
-
console.log(colors.green(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (tags.length > 20) {
|
|
59
|
-
console.log(colors.yellow(`\n共 ${tags.length} 个,仅显示最新 20 个`));
|
|
62
|
+
console.log(colors.green(`所有 tags (共 ${tags.length} 个):`));
|
|
63
|
+
if (tags.length > 5) {
|
|
64
|
+
console.log(colors.dim(" ..."));
|
|
60
65
|
}
|
|
66
|
+
const displayTags = tags.slice(-5);
|
|
67
|
+
displayTags.forEach((tag) => console.log(` ${tag}`));
|
|
61
68
|
return;
|
|
62
69
|
}
|
|
63
70
|
|
|
@@ -131,11 +138,11 @@ interface TagChoice {
|
|
|
131
138
|
value: string;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
//
|
|
141
|
+
// 获取指定前缀的最新有效 tag(必须包含数字)
|
|
135
142
|
function getLatestTag(prefix: string): string {
|
|
136
143
|
const tags = execOutput(`git tag -l "${prefix}*" --sort=-v:refname`)
|
|
137
144
|
.split("\n")
|
|
138
|
-
.filter(
|
|
145
|
+
.filter((tag) => tag && /\d/.test(tag)); // 过滤无效 tag
|
|
139
146
|
return tags[0] || "";
|
|
140
147
|
}
|
|
141
148
|
|
|
@@ -156,7 +163,10 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
if (!prefix) {
|
|
159
|
-
|
|
166
|
+
// 过滤无效 tag(如 vnull、vundefined 等误操作产生的 tag)
|
|
167
|
+
const allTags = execOutput("git tag -l")
|
|
168
|
+
.split("\n")
|
|
169
|
+
.filter((tag) => tag && /\d/.test(tag));
|
|
160
170
|
|
|
161
171
|
// 仓库没有任何 tag 的情况
|
|
162
172
|
if (allTags.length === 0) {
|
|
@@ -209,9 +219,9 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
209
219
|
return;
|
|
210
220
|
}
|
|
211
221
|
|
|
212
|
-
// 从现有 tag
|
|
222
|
+
// 从现有 tag 中提取前缀(数字之前的字母部分)
|
|
213
223
|
const prefixes = [
|
|
214
|
-
...new Set(allTags.map((t) => t.replace(
|
|
224
|
+
...new Set(allTags.map((t) => t.replace(/\d.*/, "")).filter(Boolean)),
|
|
215
225
|
];
|
|
216
226
|
|
|
217
227
|
if (prefixes.length === 0) {
|
package/src/config.ts
CHANGED
|
@@ -46,6 +46,7 @@ export interface GwConfig {
|
|
|
46
46
|
language?: "zh-CN" | "en-US"; // 生成语言,默认 zh-CN
|
|
47
47
|
maxTokens?: number; // 最大 token 数,默认 200
|
|
48
48
|
detailedDescription?: boolean; // 是否生成详细的修改点描述,默认 false
|
|
49
|
+
useEmoji?: boolean; // AI生成的commit message是否包含emoji,默认继承全局useEmoji设置
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
52
|
|