@zjex/git-workflow 0.3.0 → 0.3.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/.husky/pre-commit +5 -2
- package/ROADMAP.md +275 -0
- package/dist/index.js +349 -88
- package/docs/features/git-wrapped.md +199 -0
- package/package.json +2 -1
- package/scripts/release.sh +61 -1
- package/src/ai-service.ts +8 -2
- package/src/commands/commit.ts +4 -0
- package/src/commands/log.ts +503 -0
- package/src/index.ts +37 -13
- package/src/utils.ts +10 -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/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
|
|