@zjex/git-workflow 0.3.0 → 0.3.3
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 +2 -5
- package/README.md +1 -1
- package/ROADMAP.md +356 -0
- package/dist/index.js +448 -110
- 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/features/git-wrapped.md +199 -0
- package/docs/index.md +32 -1
- package/package.json +2 -1
- package/scripts/format-commit-msg.js +258 -0
- package/scripts/release.sh +61 -1
- package/src/ai-service.ts +77 -16
- package/src/commands/commit.ts +9 -3
- package/src/commands/init.ts +23 -0
- package/src/commands/log.ts +503 -0
- package/src/commands/tag.ts +18 -10
- package/src/config.ts +1 -0
- package/src/index.ts +37 -13
- package/src/utils.ts +10 -0
- package/tests/commit-format.test.ts +535 -0
- package/tests/log.test.ts +106 -0
- package/src/commands/help.ts +0 -76
- package/tests/help.test.ts +0 -134
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zjex/git-workflow - Log 命令
|
|
3
|
+
*
|
|
4
|
+
* 提供GitHub风格的时间线日志查看功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import boxen from "boxen";
|
|
9
|
+
import { colors } from "../utils.js";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { createWriteStream } from "fs";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 日志显示选项
|
|
17
|
+
*/
|
|
18
|
+
interface LogOptions {
|
|
19
|
+
author?: string;
|
|
20
|
+
since?: string;
|
|
21
|
+
until?: string;
|
|
22
|
+
grep?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
all?: boolean;
|
|
25
|
+
interactive?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 提交信息接口
|
|
30
|
+
*/
|
|
31
|
+
interface CommitInfo {
|
|
32
|
+
hash: string;
|
|
33
|
+
shortHash: string;
|
|
34
|
+
subject: string;
|
|
35
|
+
author: string;
|
|
36
|
+
date: string;
|
|
37
|
+
relativeDate: string;
|
|
38
|
+
refs: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 解析Git log输出为结构化数据
|
|
43
|
+
*/
|
|
44
|
+
function parseGitLog(output: string): CommitInfo[] {
|
|
45
|
+
const commits: CommitInfo[] = [];
|
|
46
|
+
const lines = output.trim().split('\n');
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
|
|
51
|
+
// 使用分隔符解析
|
|
52
|
+
const parts = line.split('|');
|
|
53
|
+
if (parts.length >= 6) {
|
|
54
|
+
commits.push({
|
|
55
|
+
hash: parts[0],
|
|
56
|
+
shortHash: parts[1],
|
|
57
|
+
subject: parts[2],
|
|
58
|
+
author: parts[3],
|
|
59
|
+
date: parts[4],
|
|
60
|
+
relativeDate: parts[5],
|
|
61
|
+
refs: parts[6] || ''
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return commits;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 获取提交类型图标
|
|
71
|
+
*/
|
|
72
|
+
function getCommitTypeIcon(subject: string): string {
|
|
73
|
+
const lowerSubject = subject.toLowerCase();
|
|
74
|
+
|
|
75
|
+
if (lowerSubject.includes('feat') || lowerSubject.includes('feature')) return '✨';
|
|
76
|
+
if (lowerSubject.includes('fix') || lowerSubject.includes('bug')) return '🐛';
|
|
77
|
+
if (lowerSubject.includes('docs') || lowerSubject.includes('doc')) return '📚';
|
|
78
|
+
if (lowerSubject.includes('style')) return '💄';
|
|
79
|
+
if (lowerSubject.includes('refactor')) return '♻️';
|
|
80
|
+
if (lowerSubject.includes('test')) return '🧪';
|
|
81
|
+
if (lowerSubject.includes('chore')) return '🔧';
|
|
82
|
+
if (lowerSubject.includes('perf')) return '⚡';
|
|
83
|
+
if (lowerSubject.includes('ci')) return '👷';
|
|
84
|
+
if (lowerSubject.includes('build')) return '📦';
|
|
85
|
+
if (lowerSubject.includes('revert')) return '⏪';
|
|
86
|
+
if (lowerSubject.includes('merge')) return '🔀';
|
|
87
|
+
if (lowerSubject.includes('release') || lowerSubject.includes('version')) return '🔖';
|
|
88
|
+
|
|
89
|
+
return '📝';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 按日期分组提交
|
|
94
|
+
*/
|
|
95
|
+
function groupCommitsByDate(commits: CommitInfo[]): Map<string, CommitInfo[]> {
|
|
96
|
+
const groups = new Map<string, CommitInfo[]>();
|
|
97
|
+
|
|
98
|
+
for (const commit of commits) {
|
|
99
|
+
const date = commit.date;
|
|
100
|
+
if (!groups.has(date)) {
|
|
101
|
+
groups.set(date, []);
|
|
102
|
+
}
|
|
103
|
+
groups.get(date)!.push(commit);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return groups;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 格式化相对时间为中文
|
|
111
|
+
*/
|
|
112
|
+
function formatRelativeTime(relativeDate: string): string {
|
|
113
|
+
let result = relativeDate;
|
|
114
|
+
|
|
115
|
+
// 先替换英文单词为中文
|
|
116
|
+
const timeMap: { [key: string]: string } = {
|
|
117
|
+
'second': '秒',
|
|
118
|
+
'seconds': '秒',
|
|
119
|
+
'minute': '分钟',
|
|
120
|
+
'minutes': '分钟',
|
|
121
|
+
'hour': '小时',
|
|
122
|
+
'hours': '小时',
|
|
123
|
+
'day': '天',
|
|
124
|
+
'days': '天',
|
|
125
|
+
'week': '周',
|
|
126
|
+
'weeks': '周',
|
|
127
|
+
'month': '个月',
|
|
128
|
+
'months': '个月',
|
|
129
|
+
'year': '年',
|
|
130
|
+
'years': '年',
|
|
131
|
+
'ago': '前'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
for (const [en, zh] of Object.entries(timeMap)) {
|
|
135
|
+
result = result.replace(new RegExp(`\\b${en}\\b`, 'g'), zh);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 去掉数字和单位之间的空格,以及单位和"前"之间的空格
|
|
139
|
+
// 例如:"22 分钟 前" -> "22分钟前"
|
|
140
|
+
result = result.replace(/(\d+)\s+(秒|分钟|小时|天|周|个月|年)\s+前/g, '$1$2前');
|
|
141
|
+
|
|
142
|
+
// 简化显示格式
|
|
143
|
+
const match = result.match(/(\d+)(分钟|小时|天|周|个月|年)前/);
|
|
144
|
+
if (match) {
|
|
145
|
+
const num = parseInt(match[1]);
|
|
146
|
+
const unit = match[2];
|
|
147
|
+
|
|
148
|
+
// 超过60分钟显示小时
|
|
149
|
+
if (unit === '分钟' && num >= 60) {
|
|
150
|
+
const hours = Math.floor(num / 60);
|
|
151
|
+
return `${hours}小时前`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 超过24小时显示天数
|
|
155
|
+
if (unit === '小时' && num >= 24) {
|
|
156
|
+
const days = Math.floor(num / 24);
|
|
157
|
+
return `${days}天前`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 超过7天显示周数
|
|
161
|
+
if (unit === '天' && num >= 7 && num < 30) {
|
|
162
|
+
const weeks = Math.floor(num / 7);
|
|
163
|
+
return `${weeks}周前`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 超过30天显示月数
|
|
167
|
+
if (unit === '天' && num >= 30) {
|
|
168
|
+
const months = Math.floor(num / 30);
|
|
169
|
+
return `${months}个月前`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 超过4周显示月数
|
|
173
|
+
if (unit === '周' && num >= 4) {
|
|
174
|
+
const months = Math.floor(num / 4);
|
|
175
|
+
return `${months}个月前`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 超过12个月显示年数
|
|
179
|
+
if (unit === '个月' && num >= 12) {
|
|
180
|
+
const years = Math.floor(num / 12);
|
|
181
|
+
return `${years}年前`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 解析提交主题,分离标题和子任务
|
|
190
|
+
*/
|
|
191
|
+
function parseCommitSubject(subject: string): { title: string; tasks: string[] } {
|
|
192
|
+
// 检查是否包含 " - " 分隔的子任务
|
|
193
|
+
if (subject.includes(' - ')) {
|
|
194
|
+
const parts = subject.split(' - ');
|
|
195
|
+
const title = parts[0].trim();
|
|
196
|
+
const tasks = parts.slice(1).map(task => task.trim()).filter(task => task.length > 0);
|
|
197
|
+
return { title, tasks };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { title: subject, tasks: [] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 检查是否支持颜色输出
|
|
205
|
+
*/
|
|
206
|
+
function supportsColor(): boolean {
|
|
207
|
+
// 在交互式模式下强制启用颜色
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 格式化GitHub风格的时间线显示
|
|
213
|
+
*/
|
|
214
|
+
function formatTimelineStyle(commits: CommitInfo[]): string {
|
|
215
|
+
const groupedCommits = groupCommitsByDate(commits);
|
|
216
|
+
let output = '';
|
|
217
|
+
|
|
218
|
+
// 按日期倒序排列
|
|
219
|
+
const sortedDates = Array.from(groupedCommits.keys()).sort((a, b) =>
|
|
220
|
+
new Date(b).getTime() - new Date(a).getTime()
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const useColors = supportsColor() || process.env.FORCE_COLOR;
|
|
224
|
+
|
|
225
|
+
for (let dateIndex = 0; dateIndex < sortedDates.length; dateIndex++) {
|
|
226
|
+
const date = sortedDates[dateIndex];
|
|
227
|
+
const dateCommits = groupedCommits.get(date)!;
|
|
228
|
+
|
|
229
|
+
// 日期标题 - 使用黄色突出显示
|
|
230
|
+
const dateTitle = `📅 Commits on ${date}`;
|
|
231
|
+
if (useColors) {
|
|
232
|
+
output += '\n' + colors.bold(colors.yellow(dateTitle)) + '\n\n';
|
|
233
|
+
} else {
|
|
234
|
+
output += '\n' + dateTitle + '\n\n';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 该日期下的提交
|
|
238
|
+
for (let commitIndex = 0; commitIndex < dateCommits.length; commitIndex++) {
|
|
239
|
+
const commit = dateCommits[commitIndex];
|
|
240
|
+
const icon = getCommitTypeIcon(commit.subject);
|
|
241
|
+
const { title, tasks } = parseCommitSubject(commit.subject);
|
|
242
|
+
|
|
243
|
+
// 构建提交内容
|
|
244
|
+
const commitContent = [];
|
|
245
|
+
|
|
246
|
+
// 主标题 - 使用白色加粗
|
|
247
|
+
if (useColors) {
|
|
248
|
+
commitContent.push(`${icon} ${colors.bold(colors.white(title))}`);
|
|
249
|
+
} else {
|
|
250
|
+
commitContent.push(`${icon} ${title}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 如果有子任务,添加子任务列表
|
|
254
|
+
if (tasks.length > 0) {
|
|
255
|
+
commitContent.push(''); // 空行分隔
|
|
256
|
+
tasks.forEach(task => {
|
|
257
|
+
if (useColors) {
|
|
258
|
+
commitContent.push(` ${colors.dim('–')} ${colors.dim(task)}`);
|
|
259
|
+
} else {
|
|
260
|
+
commitContent.push(` – ${task}`);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 空行分隔
|
|
266
|
+
commitContent.push('');
|
|
267
|
+
|
|
268
|
+
// 作者和时间信息
|
|
269
|
+
if (useColors) {
|
|
270
|
+
commitContent.push(`${colors.dim('👤')} ${colors.blue(commit.author)} ${colors.dim('committed')} ${colors.green(formatRelativeTime(commit.relativeDate))}`);
|
|
271
|
+
// Hash信息 - 使用橙色
|
|
272
|
+
commitContent.push(`${colors.dim('🔗')} ${colors.orange('#' + commit.shortHash)}`);
|
|
273
|
+
// 如果有分支/标签信息 - 区分显示
|
|
274
|
+
if (commit.refs && commit.refs.trim()) {
|
|
275
|
+
const refs = commit.refs.trim();
|
|
276
|
+
// 解析并分别显示分支和标签
|
|
277
|
+
const refParts = refs.split(', ');
|
|
278
|
+
const branches: string[] = [];
|
|
279
|
+
const tags: string[] = [];
|
|
280
|
+
|
|
281
|
+
refParts.forEach(ref => {
|
|
282
|
+
if (ref.startsWith('tag: ')) {
|
|
283
|
+
tags.push(ref.replace('tag: ', ''));
|
|
284
|
+
} else if (ref.includes('/') || ref === 'HEAD') {
|
|
285
|
+
branches.push(ref);
|
|
286
|
+
} else {
|
|
287
|
+
branches.push(ref);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// 显示分支信息
|
|
292
|
+
if (branches.length > 0) {
|
|
293
|
+
commitContent.push(`${colors.dim('🌿')} ${colors.lightPurple(branches.join(', '))}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 显示标签信息
|
|
297
|
+
if (tags.length > 0) {
|
|
298
|
+
const tagText = tags.map(tag => `tag ${tag}`).join(', ');
|
|
299
|
+
commitContent.push(`${colors.dim('🔖')} ${colors.yellow(tagText)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
commitContent.push(`👤 ${commit.author} committed ${formatRelativeTime(commit.relativeDate)}`);
|
|
304
|
+
commitContent.push(`🔗 #${commit.shortHash}`);
|
|
305
|
+
if (commit.refs && commit.refs.trim()) {
|
|
306
|
+
const refs = commit.refs.trim();
|
|
307
|
+
// 解析并分别显示分支和标签
|
|
308
|
+
const refParts = refs.split(', ');
|
|
309
|
+
const branches: string[] = [];
|
|
310
|
+
const tags: string[] = [];
|
|
311
|
+
|
|
312
|
+
refParts.forEach(ref => {
|
|
313
|
+
if (ref.startsWith('tag: ')) {
|
|
314
|
+
tags.push(ref.replace('tag: ', ''));
|
|
315
|
+
} else if (ref.includes('/') || ref === 'HEAD') {
|
|
316
|
+
branches.push(ref);
|
|
317
|
+
} else {
|
|
318
|
+
branches.push(ref);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 显示分支信息
|
|
323
|
+
if (branches.length > 0) {
|
|
324
|
+
commitContent.push(`🌿 ${branches.join(', ')}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 显示标签信息
|
|
328
|
+
if (tags.length > 0) {
|
|
329
|
+
const tagText = tags.map(tag => `tag ${tag}`).join(', ');
|
|
330
|
+
commitContent.push(`🔖 ${tagText}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 使用boxen
|
|
336
|
+
const commitBox = boxen(commitContent.join('\n'), {
|
|
337
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
338
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
339
|
+
borderStyle: 'round',
|
|
340
|
+
borderColor: 'gray'
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
output += commitBox + '\n';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return output;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 启动交互式分页查看器
|
|
352
|
+
*/
|
|
353
|
+
function startInteractivePager(content: string): void {
|
|
354
|
+
// 使用系统的 less 命令作为分页器,启用颜色支持
|
|
355
|
+
const pager = process.env.PAGER || 'less';
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// -R: 支持ANSI颜色代码
|
|
359
|
+
// -S: 不换行长行
|
|
360
|
+
// -F: 如果内容少于一屏则直接退出
|
|
361
|
+
// -X: 不清屏
|
|
362
|
+
// -i: 忽略大小写搜索
|
|
363
|
+
const pagerProcess = spawn(pager, ['-R', '-S', '-F', '-X', '-i'], {
|
|
364
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
365
|
+
env: { ...process.env, LESS: '-R -S -F -X -i' }
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// 将内容写入分页器
|
|
369
|
+
pagerProcess.stdin.write(content);
|
|
370
|
+
pagerProcess.stdin.end();
|
|
371
|
+
|
|
372
|
+
// 处理分页器退出
|
|
373
|
+
pagerProcess.on('exit', () => {
|
|
374
|
+
// 分页器退出后不需要额外处理
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// 处理错误
|
|
378
|
+
pagerProcess.on('error', (err) => {
|
|
379
|
+
// 如果分页器启动失败,直接输出内容
|
|
380
|
+
console.log(content);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
} catch (error) {
|
|
384
|
+
// 如果出错,直接输出内容
|
|
385
|
+
console.log(content);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 执行Git log并显示时间线风格结果
|
|
391
|
+
*/
|
|
392
|
+
function executeTimelineLog(options: LogOptions): void {
|
|
393
|
+
try {
|
|
394
|
+
// 构建Git命令
|
|
395
|
+
let cmd = 'git log --pretty=format:"%H|%h|%s|%an|%ad|%ar|%D" --date=short';
|
|
396
|
+
|
|
397
|
+
// 添加选项
|
|
398
|
+
if (options.limit && !options.interactive) cmd += ` -${options.limit}`;
|
|
399
|
+
if (options.author) cmd += ` --author="${options.author}"`;
|
|
400
|
+
if (options.since) cmd += ` --since="${options.since}"`;
|
|
401
|
+
if (options.until) cmd += ` --until="${options.until}"`;
|
|
402
|
+
if (options.grep) cmd += ` --grep="${options.grep}"`;
|
|
403
|
+
if (options.all) cmd += ` --all`;
|
|
404
|
+
|
|
405
|
+
// 交互式模式默认显示更多提交
|
|
406
|
+
if (options.interactive && !options.limit) {
|
|
407
|
+
cmd += ` -50`; // 默认显示50个提交
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const output = execSync(cmd, {
|
|
411
|
+
encoding: 'utf8',
|
|
412
|
+
stdio: 'pipe',
|
|
413
|
+
maxBuffer: 1024 * 1024 * 10
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (output.trim()) {
|
|
417
|
+
const commits = parseGitLog(output);
|
|
418
|
+
|
|
419
|
+
// 构建完整输出
|
|
420
|
+
let fullOutput = '';
|
|
421
|
+
|
|
422
|
+
// 显示标题
|
|
423
|
+
const title = `📊 共显示 ${commits.length} 个提交`;
|
|
424
|
+
fullOutput += '\n' + boxen(title, {
|
|
425
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
426
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
427
|
+
borderStyle: 'double',
|
|
428
|
+
borderColor: 'green',
|
|
429
|
+
textAlignment: 'center'
|
|
430
|
+
}) + '\n';
|
|
431
|
+
|
|
432
|
+
// 显示时间线
|
|
433
|
+
const timelineOutput = formatTimelineStyle(commits);
|
|
434
|
+
fullOutput += timelineOutput;
|
|
435
|
+
|
|
436
|
+
// 根据是否交互式模式选择输出方式
|
|
437
|
+
if (options.interactive) {
|
|
438
|
+
startInteractivePager(fullOutput);
|
|
439
|
+
} else {
|
|
440
|
+
console.log(fullOutput);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
} else {
|
|
444
|
+
const noCommitsMsg = '\n' + boxen('📭 没有找到匹配的提交记录', {
|
|
445
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
446
|
+
borderStyle: 'round',
|
|
447
|
+
borderColor: 'yellow',
|
|
448
|
+
textAlignment: 'center'
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (options.interactive) {
|
|
452
|
+
startInteractivePager(noCommitsMsg);
|
|
453
|
+
} else {
|
|
454
|
+
console.log(noCommitsMsg);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch (error: any) {
|
|
458
|
+
let errorMessage = '❌ 执行失败';
|
|
459
|
+
if (error.status === 128) {
|
|
460
|
+
errorMessage = '❌ Git仓库错误或没有提交记录';
|
|
461
|
+
} else {
|
|
462
|
+
errorMessage = `❌ 执行失败: ${error.message}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const errorBox = '\n' + boxen(errorMessage, {
|
|
466
|
+
padding: { top: 0, bottom: 0, left: 2, right: 2 },
|
|
467
|
+
borderStyle: 'round',
|
|
468
|
+
borderColor: 'red',
|
|
469
|
+
textAlignment: 'center'
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (options.interactive) {
|
|
473
|
+
startInteractivePager(errorBox);
|
|
474
|
+
} else {
|
|
475
|
+
console.log(errorBox);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 主要的log命令函数
|
|
482
|
+
*/
|
|
483
|
+
export async function log(options: LogOptions = {}): Promise<void> {
|
|
484
|
+
// 默认启用交互式模式
|
|
485
|
+
if (options.interactive === undefined) {
|
|
486
|
+
options.interactive = true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 交互式模式下不设置默认limit
|
|
490
|
+
if (!options.interactive && !options.limit) {
|
|
491
|
+
options.limit = 10;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
executeTimelineLog(options);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 快速日志查看
|
|
499
|
+
*/
|
|
500
|
+
export async function quickLog(limit: number = 10): Promise<void> {
|
|
501
|
+
const options: LogOptions = { limit };
|
|
502
|
+
executeTimelineLog(options);
|
|
503
|
+
}
|
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")
|
|
@@ -39,11 +43,12 @@ export async function listTags(prefix?: string): Promise<void> {
|
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
//
|
|
46
|
+
// 6. 按前缀分组(提取 tag 名称中数字前的部分作为前缀)
|
|
47
|
+
// 由于已过滤无效 tag,所有 tag 都包含数字
|
|
43
48
|
const grouped = new Map<string, string[]>();
|
|
44
49
|
tags.forEach((tag) => {
|
|
45
|
-
//
|
|
46
|
-
const prefix = tag.replace(
|
|
50
|
+
// 提取数字之前的字母部分作为前缀(如 "v0.1.0" -> "v")
|
|
51
|
+
const prefix = tag.replace(/\d.*/, "") || "(无前缀)";
|
|
47
52
|
if (!grouped.has(prefix)) {
|
|
48
53
|
grouped.set(prefix, []);
|
|
49
54
|
}
|
|
@@ -131,11 +136,11 @@ interface TagChoice {
|
|
|
131
136
|
value: string;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
//
|
|
139
|
+
// 获取指定前缀的最新有效 tag(必须包含数字)
|
|
135
140
|
function getLatestTag(prefix: string): string {
|
|
136
141
|
const tags = execOutput(`git tag -l "${prefix}*" --sort=-v:refname`)
|
|
137
142
|
.split("\n")
|
|
138
|
-
.filter(
|
|
143
|
+
.filter((tag) => tag && /\d/.test(tag)); // 过滤无效 tag
|
|
139
144
|
return tags[0] || "";
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -156,7 +161,10 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
if (!prefix) {
|
|
159
|
-
|
|
164
|
+
// 过滤无效 tag(如 vnull、vundefined 等误操作产生的 tag)
|
|
165
|
+
const allTags = execOutput("git tag -l")
|
|
166
|
+
.split("\n")
|
|
167
|
+
.filter((tag) => tag && /\d/.test(tag));
|
|
160
168
|
|
|
161
169
|
// 仓库没有任何 tag 的情况
|
|
162
170
|
if (allTags.length === 0) {
|
|
@@ -209,9 +217,9 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
209
217
|
return;
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
// 从现有 tag
|
|
220
|
+
// 从现有 tag 中提取前缀(数字之前的字母部分)
|
|
213
221
|
const prefixes = [
|
|
214
|
-
...new Set(allTags.map((t) => t.replace(
|
|
222
|
+
...new Set(allTags.map((t) => t.replace(/\d.*/, "")).filter(Boolean)),
|
|
215
223
|
];
|
|
216
224
|
|
|
217
225
|
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
|
|
package/src/index.ts
CHANGED
|
@@ -20,9 +20,9 @@ import { release } from "./commands/release.js";
|
|
|
20
20
|
import { init } from "./commands/init.js";
|
|
21
21
|
import { stash } from "./commands/stash.js";
|
|
22
22
|
import { commit } from "./commands/commit.js";
|
|
23
|
-
import { showHelp } from "./commands/help.js";
|
|
24
23
|
import { checkForUpdates } from "./update-notifier.js";
|
|
25
24
|
import { update } from "./commands/update.js";
|
|
25
|
+
import { log, quickLog } from "./commands/log.js";
|
|
26
26
|
|
|
27
27
|
// ========== 全局错误处理 ==========
|
|
28
28
|
|
|
@@ -151,7 +151,11 @@ async function mainMenu(): Promise<void> {
|
|
|
151
151
|
value: "stash",
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
|
-
name: `[b]
|
|
154
|
+
name: `[b] 📊 查看日志 ${colors.dim("gw log")}`,
|
|
155
|
+
value: "log",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: `[c] ⚙️ 初始化配置 ${colors.dim("gw init")}`,
|
|
155
159
|
value: "init",
|
|
156
160
|
},
|
|
157
161
|
{ name: "[0] ❓ 帮助", value: "help" },
|
|
@@ -201,11 +205,16 @@ async function mainMenu(): Promise<void> {
|
|
|
201
205
|
checkGitRepo();
|
|
202
206
|
await stash();
|
|
203
207
|
break;
|
|
208
|
+
case "log":
|
|
209
|
+
checkGitRepo();
|
|
210
|
+
await log();
|
|
211
|
+
break;
|
|
204
212
|
case "init":
|
|
205
213
|
await init();
|
|
206
214
|
break;
|
|
207
215
|
case "help":
|
|
208
|
-
|
|
216
|
+
// 使用 cac 自动生成的帮助信息
|
|
217
|
+
cli.outputHelp();
|
|
209
218
|
break;
|
|
210
219
|
case "exit":
|
|
211
220
|
break;
|
|
@@ -339,6 +348,22 @@ cli
|
|
|
339
348
|
return update(version);
|
|
340
349
|
});
|
|
341
350
|
|
|
351
|
+
cli
|
|
352
|
+
.command("log", "交互式Git日志查看 (分页模式)")
|
|
353
|
+
.alias("ls")
|
|
354
|
+
.alias("l")
|
|
355
|
+
.option("--limit <number>", "限制显示数量")
|
|
356
|
+
.action(async (options: any) => {
|
|
357
|
+
await checkForUpdates(version, "@zjex/git-workflow");
|
|
358
|
+
checkGitRepo();
|
|
359
|
+
|
|
360
|
+
// 构建选项对象 - 默认交互式模式
|
|
361
|
+
const logOptions: any = { interactive: true };
|
|
362
|
+
if (options.limit) logOptions.limit = parseInt(options.limit);
|
|
363
|
+
|
|
364
|
+
return log(logOptions);
|
|
365
|
+
});
|
|
366
|
+
|
|
342
367
|
cli.command("clean", "清理缓存文件").action(async () => {
|
|
343
368
|
const { clearUpdateCache } = await import("./update-notifier.js");
|
|
344
369
|
clearUpdateCache();
|
|
@@ -347,20 +372,19 @@ cli.command("clean", "清理缓存文件").action(async () => {
|
|
|
347
372
|
console.log("");
|
|
348
373
|
});
|
|
349
374
|
|
|
350
|
-
|
|
351
|
-
sections.push({
|
|
352
|
-
body: showHelp(),
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// 不使用 cac 的 version,手动处理 --version
|
|
375
|
+
// 不使用 cac 的 version,手动处理 --version 和 --help
|
|
357
376
|
cli.option("-v, --version", "显示版本号");
|
|
377
|
+
cli.option("-h, --help", "显示帮助信息");
|
|
358
378
|
|
|
359
|
-
// 在 parse 之前检查 --version
|
|
360
|
-
const
|
|
361
|
-
if (
|
|
379
|
+
// 在 parse 之前检查 --version 和 --help
|
|
380
|
+
const processArgs = process.argv.slice(2);
|
|
381
|
+
if (processArgs.includes("-v") || processArgs.includes("--version")) {
|
|
362
382
|
console.log(colors.yellow(`v${version}`));
|
|
363
383
|
process.exit(0);
|
|
364
384
|
}
|
|
385
|
+
if (processArgs.includes("-h") || processArgs.includes("--help")) {
|
|
386
|
+
cli.outputHelp();
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
365
389
|
|
|
366
390
|
cli.parse();
|
package/src/utils.ts
CHANGED
|
@@ -4,9 +4,14 @@ export interface Colors {
|
|
|
4
4
|
red: (s: string) => string;
|
|
5
5
|
green: (s: string) => string;
|
|
6
6
|
yellow: (s: string) => string;
|
|
7
|
+
blue: (s: string) => string;
|
|
7
8
|
cyan: (s: string) => string;
|
|
8
9
|
dim: (s: string) => string;
|
|
9
10
|
bold: (s: string) => string;
|
|
11
|
+
purple: (s: string) => string;
|
|
12
|
+
orange: (s: string) => string;
|
|
13
|
+
lightPurple: (s: string) => string;
|
|
14
|
+
white: (s: string) => string;
|
|
10
15
|
reset: string;
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -14,9 +19,14 @@ export const colors: Colors = {
|
|
|
14
19
|
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
15
20
|
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
16
21
|
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
22
|
+
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
17
23
|
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
18
24
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
19
25
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
26
|
+
purple: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
27
|
+
orange: (s) => `\x1b[38;5;208m${s}\x1b[0m`,
|
|
28
|
+
lightPurple: (s) => `\x1b[38;5;141m${s}\x1b[0m`,
|
|
29
|
+
white: (s) => `\x1b[37m${s}\x1b[0m`,
|
|
20
30
|
reset: "\x1b[0m",
|
|
21
31
|
};
|
|
22
32
|
|