@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.
@@ -0,0 +1,535 @@
1
+ /**
2
+ * @zjex/git-workflow - Commit Message 格式化测试
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs';
7
+ import { execSync } from 'child_process';
8
+
9
+ // Mock fs functions for testing
10
+ vi.mock('fs', async () => {
11
+ const actual = await vi.importActual('fs');
12
+ return {
13
+ ...actual,
14
+ readFileSync: vi.fn(),
15
+ writeFileSync: vi.fn(),
16
+ existsSync: vi.fn(),
17
+ };
18
+ });
19
+
20
+ const mockReadFileSync = vi.mocked(readFileSync);
21
+ const mockWriteFileSync = vi.mocked(writeFileSync);
22
+ const mockExistsSync = vi.mocked(existsSync);
23
+
24
+ // 导入要测试的函数(需要从脚本中提取)
25
+ const EMOJI_MAP = {
26
+ feat: '✨',
27
+ fix: '🐛',
28
+ docs: '📝',
29
+ style: '💄',
30
+ refactor: '♻️',
31
+ perf: '⚡️',
32
+ test: '✅',
33
+ build: '📦',
34
+ ci: '👷',
35
+ chore: '🔧',
36
+ revert: '⏪',
37
+ merge: '🔀',
38
+ release: '🔖',
39
+ hotfix: '🚑',
40
+ security: '🔒',
41
+ breaking: '💥'
42
+ };
43
+
44
+ /**
45
+ * 解析 commit message
46
+ */
47
+ function parseCommitMessage(message: string) {
48
+ const cleanMessage = message.trim();
49
+
50
+ // 检测是否以 emoji 开头
51
+ const emojiMatch = cleanMessage.match(/^(\p{Emoji})\s*/u);
52
+ const hasEmoji = emojiMatch !== null;
53
+ const currentEmoji = hasEmoji ? emojiMatch[1] : null;
54
+
55
+ // 移除 emoji 后的消息
56
+ const messageWithoutEmoji = hasEmoji
57
+ ? cleanMessage.replace(/^(\p{Emoji})\s*/u, '').trim()
58
+ : cleanMessage;
59
+
60
+ // 解析 Conventional Commits 格式: type(scope): subject
61
+ const conventionalMatch = messageWithoutEmoji.match(/^(\w+)(\([^)]+\))?(!)?:\s*(.+)/);
62
+
63
+ if (!conventionalMatch) {
64
+ return {
65
+ isConventional: false,
66
+ hasEmoji,
67
+ currentEmoji,
68
+ originalMessage: message,
69
+ cleanMessage: messageWithoutEmoji
70
+ };
71
+ }
72
+
73
+ const [, type, scope, breaking, subject] = conventionalMatch;
74
+
75
+ return {
76
+ isConventional: true,
77
+ hasEmoji,
78
+ currentEmoji,
79
+ type: type.toLowerCase(),
80
+ scope: scope || '',
81
+ breaking: breaking === '!',
82
+ subject,
83
+ originalMessage: message,
84
+ cleanMessage: messageWithoutEmoji,
85
+ messageWithoutEmoji
86
+ };
87
+ }
88
+
89
+ /**
90
+ * 获取正确的 emoji
91
+ */
92
+ function getCorrectEmoji(type: string): string {
93
+ return EMOJI_MAP[type as keyof typeof EMOJI_MAP] || EMOJI_MAP.chore;
94
+ }
95
+
96
+ /**
97
+ * 检查 emoji 是否匹配 type
98
+ */
99
+ function isEmojiCorrect(emoji: string, type: string): boolean {
100
+ return emoji === getCorrectEmoji(type);
101
+ }
102
+
103
+ /**
104
+ * 格式化 commit message
105
+ */
106
+ function formatCommitMessage(message: string) {
107
+ const parsed = parseCommitMessage(message);
108
+
109
+ // 如果不是 Conventional Commits 格式,不处理
110
+ if (!parsed.isConventional) {
111
+ return {
112
+ needsUpdate: false,
113
+ originalMessage: message,
114
+ formattedMessage: message,
115
+ reason: 'Not a conventional commit format'
116
+ };
117
+ }
118
+
119
+ const correctEmoji = getCorrectEmoji(parsed.type);
120
+ let needsUpdate = false;
121
+ let reason = '';
122
+
123
+ // 检查是否需要更新
124
+ if (!parsed.hasEmoji) {
125
+ needsUpdate = true;
126
+ reason = `Added missing emoji for type '${parsed.type}'`;
127
+ } else if (!isEmojiCorrect(parsed.currentEmoji!, parsed.type)) {
128
+ needsUpdate = true;
129
+ reason = `Replaced incorrect emoji '${parsed.currentEmoji}' with '${correctEmoji}' for type '${parsed.type}'`;
130
+ }
131
+
132
+ // 构建格式化后的消息
133
+ const formattedMessage = needsUpdate
134
+ ? `${correctEmoji} ${parsed.messageWithoutEmoji}`
135
+ : message;
136
+
137
+ return {
138
+ needsUpdate,
139
+ originalMessage: message,
140
+ formattedMessage,
141
+ reason,
142
+ type: parsed.type,
143
+ currentEmoji: parsed.currentEmoji,
144
+ correctEmoji
145
+ };
146
+ }
147
+
148
+ describe('Commit Message 格式化', () => {
149
+ beforeEach(() => {
150
+ vi.clearAllMocks();
151
+ });
152
+
153
+ describe('parseCommitMessage', () => {
154
+ it('应该正确解析没有emoji的conventional commit', () => {
155
+ const result = parseCommitMessage('feat(auth): 添加用户登录功能');
156
+
157
+ expect(result.isConventional).toBe(true);
158
+ expect(result.hasEmoji).toBe(false);
159
+ expect(result.type).toBe('feat');
160
+ expect(result.subject).toBe('添加用户登录功能');
161
+ });
162
+
163
+ it('应该正确解析带emoji的conventional commit', () => {
164
+ const result = parseCommitMessage('✨ feat(auth): 添加用户登录功能');
165
+
166
+ expect(result.isConventional).toBe(true);
167
+ expect(result.hasEmoji).toBe(true);
168
+ expect(result.currentEmoji).toBe('✨');
169
+ expect(result.type).toBe('feat');
170
+ expect(result.subject).toBe('添加用户登录功能');
171
+ });
172
+
173
+ it('应该正确解析没有scope的commit', () => {
174
+ const result = parseCommitMessage('fix: 修复bug');
175
+
176
+ expect(result.isConventional).toBe(true);
177
+ expect(result.type).toBe('fix');
178
+ expect(result.scope).toBe('');
179
+ expect(result.subject).toBe('修复bug');
180
+ });
181
+
182
+ it('应该识别非conventional格式的commit', () => {
183
+ const result = parseCommitMessage('add new feature');
184
+
185
+ expect(result.isConventional).toBe(false);
186
+ expect(result.hasEmoji).toBe(false);
187
+ });
188
+
189
+ it('应该正确处理breaking change', () => {
190
+ const result = parseCommitMessage('feat!: 重大更新');
191
+
192
+ expect(result.isConventional).toBe(true);
193
+ expect(result.breaking).toBe(true);
194
+ expect(result.type).toBe('feat');
195
+ });
196
+ });
197
+
198
+ describe('getCorrectEmoji', () => {
199
+ it('应该返回正确的emoji', () => {
200
+ expect(getCorrectEmoji('feat')).toBe('✨');
201
+ expect(getCorrectEmoji('fix')).toBe('🐛');
202
+ expect(getCorrectEmoji('docs')).toBe('📝');
203
+ expect(getCorrectEmoji('chore')).toBe('🔧');
204
+ });
205
+
206
+ it('应该为未知类型返回默认emoji', () => {
207
+ expect(getCorrectEmoji('unknown')).toBe('🔧');
208
+ });
209
+ });
210
+
211
+ describe('isEmojiCorrect', () => {
212
+ it('应该正确判断emoji是否匹配', () => {
213
+ expect(isEmojiCorrect('✨', 'feat')).toBe(true);
214
+ expect(isEmojiCorrect('🐛', 'feat')).toBe(false);
215
+ expect(isEmojiCorrect('🐛', 'fix')).toBe(true);
216
+ });
217
+ });
218
+
219
+ describe('formatCommitMessage', () => {
220
+ it('应该为没有emoji的commit添加emoji', () => {
221
+ const result = formatCommitMessage('feat(auth): 添加用户登录功能');
222
+
223
+ expect(result.needsUpdate).toBe(true);
224
+ expect(result.formattedMessage).toBe('✨ feat(auth): 添加用户登录功能');
225
+ expect(result.reason).toBe("Added missing emoji for type 'feat'");
226
+ });
227
+
228
+ it('应该替换错误的emoji', () => {
229
+ const result = formatCommitMessage('🐛 feat(auth): 添加用户登录功能');
230
+
231
+ expect(result.needsUpdate).toBe(true);
232
+ expect(result.formattedMessage).toBe('✨ feat(auth): 添加用户登录功能');
233
+ expect(result.reason).toBe("Replaced incorrect emoji '🐛' with '✨' for type 'feat'");
234
+ });
235
+
236
+ it('应该保持正确的emoji不变', () => {
237
+ const result = formatCommitMessage('✨ feat(auth): 添加用户登录功能');
238
+
239
+ expect(result.needsUpdate).toBe(false);
240
+ expect(result.formattedMessage).toBe('✨ feat(auth): 添加用户登录功能');
241
+ });
242
+
243
+ it('应该不处理非conventional格式的commit', () => {
244
+ const result = formatCommitMessage('add new feature');
245
+
246
+ expect(result.needsUpdate).toBe(false);
247
+ expect(result.formattedMessage).toBe('add new feature');
248
+ expect(result.reason).toBe('Not a conventional commit format');
249
+ });
250
+
251
+ it('应该处理各种commit类型', () => {
252
+ const testCases = [
253
+ { input: 'fix: 修复bug', expected: '🐛 fix: 修复bug' },
254
+ { input: 'docs: 更新文档', expected: '📝 docs: 更新文档' },
255
+ { input: 'style: 代码格式化', expected: '💄 style: 代码格式化' },
256
+ { input: 'refactor: 重构代码', expected: '♻️ refactor: 重构代码' },
257
+ { input: 'test: 添加测试', expected: '✅ test: 添加测试' },
258
+ { input: 'chore: 更新依赖', expected: '🔧 chore: 更新依赖' },
259
+ ];
260
+
261
+ testCases.forEach(({ input, expected }) => {
262
+ const result = formatCommitMessage(input);
263
+ expect(result.formattedMessage).toBe(expected);
264
+ expect(result.needsUpdate).toBe(true);
265
+ });
266
+ });
267
+
268
+ it('应该处理带scope的commit', () => {
269
+ const result = formatCommitMessage('feat(api): 添加新接口');
270
+
271
+ expect(result.needsUpdate).toBe(true);
272
+ expect(result.formattedMessage).toBe('✨ feat(api): 添加新接口');
273
+ });
274
+
275
+ it('应该处理breaking change', () => {
276
+ const result = formatCommitMessage('feat!: 重大更新');
277
+
278
+ expect(result.needsUpdate).toBe(true);
279
+ expect(result.formattedMessage).toBe('✨ feat!: 重大更新');
280
+ });
281
+
282
+ describe('所有commit类型测试', () => {
283
+ it('应该正确处理所有支持的commit类型', () => {
284
+ const allTypes = [
285
+ { type: 'feat', emoji: '✨', desc: '新增功能' },
286
+ { type: 'fix', emoji: '🐛', desc: '修复bug' },
287
+ { type: 'docs', emoji: '📝', desc: '更新文档' },
288
+ { type: 'style', emoji: '💄', desc: '代码格式化' },
289
+ { type: 'refactor', emoji: '♻️', desc: '重构代码' },
290
+ { type: 'perf', emoji: '⚡️', desc: '性能优化' },
291
+ { type: 'test', emoji: '✅', desc: '添加测试' },
292
+ { type: 'build', emoji: '📦', desc: '构建相关' },
293
+ { type: 'ci', emoji: '👷', desc: 'CI配置' },
294
+ { type: 'chore', emoji: '🔧', desc: '杂项' },
295
+ { type: 'revert', emoji: '⏪', desc: '回滚' },
296
+ { type: 'merge', emoji: '🔀', desc: '合并' },
297
+ { type: 'release', emoji: '🔖', desc: '发布' },
298
+ { type: 'hotfix', emoji: '🚑', desc: '热修复' },
299
+ { type: 'security', emoji: '🔒', desc: '安全修复' }
300
+ ];
301
+
302
+ allTypes.forEach(({ type, emoji, desc }) => {
303
+ const input = `${type}: ${desc}`;
304
+ const expected = `${emoji} ${type}: ${desc}`;
305
+ const result = formatCommitMessage(input);
306
+
307
+ expect(result.needsUpdate).toBe(true);
308
+ expect(result.formattedMessage).toBe(expected);
309
+ expect(result.type).toBe(type);
310
+ expect(result.correctEmoji).toBe(emoji);
311
+ });
312
+ });
313
+ });
314
+
315
+ describe('错误emoji替换测试', () => {
316
+ it('应该替换所有类型的错误emoji', () => {
317
+ const wrongEmojiTests = [
318
+ { input: '🐛 feat: 新功能', expected: '✨ feat: 新功能', wrongEmoji: '🐛', correctEmoji: '✨' },
319
+ { input: '✨ fix: 修复bug', expected: '🐛 fix: 修复bug', wrongEmoji: '✨', correctEmoji: '🐛' },
320
+ { input: '🔧 docs: 更新文档', expected: '📝 docs: 更新文档', wrongEmoji: '🔧', correctEmoji: '📝' },
321
+ { input: '📝 style: 格式化', expected: '💄 style: 格式化', wrongEmoji: '📝', correctEmoji: '💄' },
322
+ { input: '💄 refactor: 重构', expected: '♻️ refactor: 重构', wrongEmoji: '💄', correctEmoji: '♻️' }
323
+ ];
324
+
325
+ wrongEmojiTests.forEach(({ input, expected, wrongEmoji, correctEmoji }) => {
326
+ const result = formatCommitMessage(input);
327
+
328
+ expect(result.needsUpdate).toBe(true);
329
+ expect(result.formattedMessage).toBe(expected);
330
+ expect(result.currentEmoji).toBe(wrongEmoji);
331
+ expect(result.correctEmoji).toBe(correctEmoji);
332
+ expect(result.reason).toContain(`Replaced incorrect emoji '${wrongEmoji}' with '${correctEmoji}'`);
333
+ });
334
+ });
335
+ });
336
+
337
+ describe('复杂scope测试', () => {
338
+ it('应该处理各种复杂的scope格式', () => {
339
+ const scopeTests = [
340
+ { input: 'feat(api): 添加接口', expected: '✨ feat(api): 添加接口' },
341
+ { input: 'fix(ui/button): 修复按钮', expected: '🐛 fix(ui/button): 修复按钮' },
342
+ { input: 'docs(readme): 更新说明', expected: '📝 docs(readme): 更新说明' },
343
+ { input: 'chore(deps): 更新依赖', expected: '🔧 chore(deps): 更新依赖' },
344
+ { input: 'feat(user-auth): OAuth登录', expected: '✨ feat(user-auth): OAuth登录' }
345
+ ];
346
+
347
+ scopeTests.forEach(({ input, expected }) => {
348
+ const result = formatCommitMessage(input);
349
+ expect(result.formattedMessage).toBe(expected);
350
+ expect(result.needsUpdate).toBe(true);
351
+ });
352
+ });
353
+ });
354
+
355
+ describe('breaking change测试', () => {
356
+ it('应该处理各种breaking change格式', () => {
357
+ const breakingTests = [
358
+ { input: 'feat!: 重大更新', expected: '✨ feat!: 重大更新' },
359
+ { input: 'fix!: 破坏性修复', expected: '🐛 fix!: 破坏性修复' },
360
+ { input: 'feat(api)!: 重构接口', expected: '✨ feat(api)!: 重构接口' },
361
+ { input: 'refactor(core)!: 核心重构', expected: '♻️ refactor(core)!: 核心重构' }
362
+ ];
363
+
364
+ breakingTests.forEach(({ input, expected }) => {
365
+ const result = formatCommitMessage(input);
366
+ expect(result.formattedMessage).toBe(expected);
367
+ expect(result.needsUpdate).toBe(true);
368
+ });
369
+ });
370
+ });
371
+
372
+ describe('大小写处理测试', () => {
373
+ it('应该正确处理不同大小写的commit类型', () => {
374
+ const caseTests = [
375
+ { input: 'FEAT: 大写功能', expected: '✨ FEAT: 大写功能' },
376
+ { input: 'Fix: 首字母大写', expected: '🐛 Fix: 首字母大写' },
377
+ { input: 'DoCs: 混合大小写', expected: '📝 DoCs: 混合大小写' }
378
+ ];
379
+
380
+ caseTests.forEach(({ input, expected }) => {
381
+ const result = formatCommitMessage(input);
382
+ expect(result.formattedMessage).toBe(expected);
383
+ expect(result.needsUpdate).toBe(true);
384
+ });
385
+ });
386
+ });
387
+
388
+ describe('未知类型处理', () => {
389
+ it('应该为未知类型使用默认emoji', () => {
390
+ const unknownTests = [
391
+ { input: 'unknown: 未知类型', expected: '🔧 unknown: 未知类型' },
392
+ { input: 'custom: 自定义类型', expected: '🔧 custom: 自定义类型' },
393
+ { input: 'deploy: 部署相关', expected: '🔧 deploy: 部署相关' }
394
+ ];
395
+
396
+ unknownTests.forEach(({ input, expected }) => {
397
+ const result = formatCommitMessage(input);
398
+ expect(result.formattedMessage).toBe(expected);
399
+ expect(result.needsUpdate).toBe(true);
400
+ expect(result.correctEmoji).toBe('🔧');
401
+ });
402
+ });
403
+ });
404
+
405
+ describe('多语言支持测试', () => {
406
+ it('应该支持中英文混合的commit message', () => {
407
+ const multiLangTests = [
408
+ { input: 'feat: add user authentication', expected: '✨ feat: add user authentication' },
409
+ { input: 'fix: resolve login issue', expected: '🐛 fix: resolve login issue' },
410
+ { input: 'docs: update API documentation', expected: '📝 docs: update API documentation' },
411
+ { input: 'feat(api): 添加用户认证接口', expected: '✨ feat(api): 添加用户认证接口' },
412
+ { input: 'fix(ui): 修复登录页面样式问题', expected: '🐛 fix(ui): 修复登录页面样式问题' }
413
+ ];
414
+
415
+ multiLangTests.forEach(({ input, expected }) => {
416
+ const result = formatCommitMessage(input);
417
+ expect(result.formattedMessage).toBe(expected);
418
+ expect(result.needsUpdate).toBe(true);
419
+ });
420
+ });
421
+ });
422
+ });
423
+
424
+ describe('边界情况', () => {
425
+ it('应该处理空消息', () => {
426
+ const result = formatCommitMessage('');
427
+
428
+ expect(result.needsUpdate).toBe(false);
429
+ expect(result.formattedMessage).toBe('');
430
+ });
431
+
432
+ it('应该处理只有空格的消息', () => {
433
+ const result = formatCommitMessage(' ');
434
+
435
+ expect(result.needsUpdate).toBe(false);
436
+ expect(result.formattedMessage).toBe(' ');
437
+ });
438
+
439
+ it('应该处理merge commit', () => {
440
+ const result = formatCommitMessage('Merge branch "feature" into main');
441
+
442
+ expect(result.needsUpdate).toBe(false);
443
+ expect(result.formattedMessage).toBe('Merge branch "feature" into main');
444
+ });
445
+
446
+ it('应该处理多行commit message', () => {
447
+ const multilineMessage = `feat: 添加新功能
448
+
449
+ 这是详细描述
450
+ - 功能1
451
+ - 功能2`;
452
+
453
+ const result = formatCommitMessage(multilineMessage);
454
+
455
+ expect(result.needsUpdate).toBe(true);
456
+ expect(result.formattedMessage).toContain('✨ feat: 添加新功能');
457
+ });
458
+
459
+ describe('特殊格式处理', () => {
460
+ it('应该处理多个空格的commit', () => {
461
+ const result = formatCommitMessage('feat: 添加功能');
462
+ expect(result.formattedMessage).toBe('✨ feat: 添加功能');
463
+ expect(result.needsUpdate).toBe(true);
464
+ });
465
+
466
+ it('应该处理带tab的commit', () => {
467
+ const result = formatCommitMessage('feat:\t添加功能');
468
+ expect(result.formattedMessage).toBe('✨ feat:\t添加功能');
469
+ expect(result.needsUpdate).toBe(true);
470
+ });
471
+
472
+ it('应该处理前后有空格的commit', () => {
473
+ const result = formatCommitMessage(' feat: 添加功能 ');
474
+ expect(result.formattedMessage).toBe('✨ feat: 添加功能');
475
+ expect(result.needsUpdate).toBe(true);
476
+ });
477
+ });
478
+
479
+ describe('特殊commit类型', () => {
480
+ it('应该跳过Merge类型的commit', () => {
481
+ const mergeTests = [
482
+ 'Merge branch "feature" into main',
483
+ 'Merge pull request #123 from feature/branch',
484
+ 'Merge remote-tracking branch "origin/main"'
485
+ ];
486
+
487
+ mergeTests.forEach(input => {
488
+ const result = formatCommitMessage(input);
489
+ expect(result.needsUpdate).toBe(false);
490
+ expect(result.formattedMessage).toBe(input);
491
+ });
492
+ });
493
+
494
+ it('应该跳过Revert类型的Git commit', () => {
495
+ const revertCommit = 'Revert "feat: 添加新功能"';
496
+ const result = formatCommitMessage(revertCommit);
497
+
498
+ expect(result.needsUpdate).toBe(false);
499
+ expect(result.formattedMessage).toBe(revertCommit);
500
+ });
501
+ });
502
+
503
+ describe('emoji边界测试', () => {
504
+ it('应该处理emoji后有多个空格的情况', () => {
505
+ const result = formatCommitMessage('✨ feat: 添加功能');
506
+ expect(result.needsUpdate).toBe(false);
507
+ expect(result.formattedMessage).toBe('✨ feat: 添加功能');
508
+ });
509
+
510
+ it('应该处理emoji和类型之间没有空格的情况', () => {
511
+ const result = formatCommitMessage('✨feat: 添加功能');
512
+ expect(result.needsUpdate).toBe(false);
513
+ expect(result.formattedMessage).toBe('✨feat: 添加功能');
514
+ });
515
+ });
516
+
517
+ describe('长消息处理', () => {
518
+ it('应该处理超长的commit消息', () => {
519
+ const longMessage = 'feat: ' + 'a'.repeat(200);
520
+ const result = formatCommitMessage(longMessage);
521
+
522
+ expect(result.needsUpdate).toBe(true);
523
+ expect(result.formattedMessage).toBe('✨ ' + longMessage);
524
+ });
525
+
526
+ it('应该处理包含特殊字符的commit', () => {
527
+ const specialChars = 'feat: 添加功能 @#$%^&*()_+-=[]{}|;:,.<>?';
528
+ const result = formatCommitMessage(specialChars);
529
+
530
+ expect(result.needsUpdate).toBe(true);
531
+ expect(result.formattedMessage).toBe('✨ ' + specialChars);
532
+ });
533
+ });
534
+ });
535
+ });