@zjex/git-workflow 0.4.0 → 0.4.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/CHANGELOG.md +488 -12
- package/README.md +8 -10
- package/ROADMAP.md +1 -1
- package/dist/index.js +124 -22
- package/docs/.vitepress/config.ts +111 -100
- package/docs/guide/api.md +607 -0
- package/docs/guide/contributing.md +441 -0
- package/docs/guide/development.md +295 -0
- package/docs/guide/team-collaboration.md +538 -0
- package/docs/guide/testing.md +461 -0
- package/package.json +3 -3
- package/scripts/generate-changelog-manual.js +135 -0
- package/src/commands/stash.ts +176 -7
- package/tests/stash.test.ts +161 -89
- package/CODE_DOCUMENTATION.md +0 -169
- package/TESTING.md +0 -436
- package/TEST_COVERAGE_SUMMARY.md +0 -264
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# 测试指南
|
|
2
|
+
|
|
3
|
+
本指南介绍 git-workflow 的测试策略、框架使用和最佳实践。
|
|
4
|
+
|
|
5
|
+
## 测试框架
|
|
6
|
+
|
|
7
|
+
我们使用 [Vitest](https://vitest.dev/) 作为测试框架,它提供:
|
|
8
|
+
|
|
9
|
+
- ⚡️ 快速的测试执行
|
|
10
|
+
- 🔄 Watch 模式支持
|
|
11
|
+
- 📊 内置代码覆盖率
|
|
12
|
+
- 🎯 与 Jest 兼容的 API
|
|
13
|
+
- 🔧 TypeScript 原生支持
|
|
14
|
+
|
|
15
|
+
## 运行测试
|
|
16
|
+
|
|
17
|
+
### 基本命令
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 运行所有测试
|
|
21
|
+
npm test
|
|
22
|
+
|
|
23
|
+
# 运行特定测试文件
|
|
24
|
+
npm test -- tests/commit.test.ts
|
|
25
|
+
|
|
26
|
+
# Watch 模式(开发时使用)
|
|
27
|
+
npm run test:watch
|
|
28
|
+
|
|
29
|
+
# 查看测试覆盖率
|
|
30
|
+
npm run test:coverage
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 测试输出
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
✓ tests/commit.test.ts (18 tests) 9ms
|
|
37
|
+
✓ tests/branch.test.ts (45 tests) 7ms
|
|
38
|
+
✓ tests/tag.test.ts (58 tests) 14ms
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
Test Files 15 passed (15)
|
|
42
|
+
Tests 375 passed (375)
|
|
43
|
+
Start at 17:34:11
|
|
44
|
+
Duration 426ms
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 测试覆盖率
|
|
48
|
+
|
|
49
|
+
当前测试覆盖率:**375 个测试用例**
|
|
50
|
+
|
|
51
|
+
查看详细覆盖率报告:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run test:coverage
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
覆盖率报告会生成在 `coverage/` 目录,可以在浏览器中打开 `coverage/index.html` 查看。
|
|
58
|
+
|
|
59
|
+
## 测试结构
|
|
60
|
+
|
|
61
|
+
### 测试文件组织
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
tests/
|
|
65
|
+
├── ai-service.test.ts # AI 服务测试
|
|
66
|
+
├── branch.test.ts # 分支管理测试
|
|
67
|
+
├── commit.test.ts # 提交管理测试
|
|
68
|
+
├── tag.test.ts # 标签管理测试
|
|
69
|
+
├── stash.test.ts # Stash 管理测试
|
|
70
|
+
├── release.test.ts # 发布管理测试
|
|
71
|
+
├── log.test.ts # 日志查看测试
|
|
72
|
+
├── update.test.ts # 更新检查测试
|
|
73
|
+
├── config.test.ts # 配置管理测试
|
|
74
|
+
├── utils.test.ts # 工具函数测试
|
|
75
|
+
├── commands.test.ts # 命令别名测试
|
|
76
|
+
├── clean.test.ts # 清理命令测试
|
|
77
|
+
├── commit-format.test.ts # 提交格式测试
|
|
78
|
+
├── init.test.ts # 初始化测试
|
|
79
|
+
├── update-notifier.test.ts # 更新通知测试
|
|
80
|
+
└── setup.ts # 测试配置
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 测试文件命名
|
|
84
|
+
|
|
85
|
+
- 测试文件以 `.test.ts` 结尾
|
|
86
|
+
- 文件名与被测试的源文件对应
|
|
87
|
+
- 例如:`src/config.ts` → `tests/config.test.ts`
|
|
88
|
+
|
|
89
|
+
## 编写测试
|
|
90
|
+
|
|
91
|
+
### 基本测试结构
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
95
|
+
|
|
96
|
+
describe("功能模块名称", () => {
|
|
97
|
+
beforeEach(() => {
|
|
98
|
+
// 每个测试前的准备工作
|
|
99
|
+
vi.clearAllMocks();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
// 每个测试后的清理工作
|
|
104
|
+
vi.restoreAllMocks();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("子功能", () => {
|
|
108
|
+
it("应该正确处理某个场景", () => {
|
|
109
|
+
// Arrange: 准备测试数据
|
|
110
|
+
const input = "test";
|
|
111
|
+
|
|
112
|
+
// Act: 执行被测试的功能
|
|
113
|
+
const result = someFunction(input);
|
|
114
|
+
|
|
115
|
+
// Assert: 验证结果
|
|
116
|
+
expect(result).toBe("expected");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Mock 外部依赖
|
|
123
|
+
|
|
124
|
+
#### Mock Node.js 模块
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
vi.mock("child_process", () => ({
|
|
128
|
+
execSync: vi.fn(),
|
|
129
|
+
spawn: vi.fn(),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// 使用 mock
|
|
133
|
+
import { execSync } from "child_process";
|
|
134
|
+
const mockExecSync = vi.mocked(execSync);
|
|
135
|
+
mockExecSync.mockReturnValue("output");
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Mock 第三方库
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
vi.mock("@inquirer/prompts", () => ({
|
|
142
|
+
select: vi.fn(),
|
|
143
|
+
input: vi.fn(),
|
|
144
|
+
confirm: vi.fn(),
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
// 使用 mock
|
|
148
|
+
import { select } from "@inquirer/prompts";
|
|
149
|
+
vi.mocked(select).mockResolvedValue("option1");
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### Mock 工具函数
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
vi.mock("../src/utils.js", () => ({
|
|
156
|
+
colors: {
|
|
157
|
+
yellow: (text: string) => text,
|
|
158
|
+
green: (text: string) => text,
|
|
159
|
+
red: (text: string) => text,
|
|
160
|
+
},
|
|
161
|
+
execOutput: vi.fn(),
|
|
162
|
+
}));
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 测试异步函数
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
it("应该正确处理异步操作", async () => {
|
|
169
|
+
const result = await asyncFunction();
|
|
170
|
+
expect(result).toBe("expected");
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 测试错误处理
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
it("应该抛出错误", () => {
|
|
178
|
+
expect(() => {
|
|
179
|
+
functionThatThrows();
|
|
180
|
+
}).toThrow("Error message");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("应该处理异步错误", async () => {
|
|
184
|
+
await expect(asyncFunctionThatThrows()).rejects.toThrow("Error message");
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 测试用户交互
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
it("应该正确处理用户选择", async () => {
|
|
192
|
+
const { select } = await import("@inquirer/prompts");
|
|
193
|
+
vi.mocked(select).mockResolvedValue("option1");
|
|
194
|
+
|
|
195
|
+
await interactiveFunction();
|
|
196
|
+
|
|
197
|
+
expect(select).toHaveBeenCalledWith(
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
message: "选择选项:",
|
|
200
|
+
choices: expect.arrayContaining([
|
|
201
|
+
expect.objectContaining({ value: "option1" }),
|
|
202
|
+
]),
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## 测试最佳实践
|
|
209
|
+
|
|
210
|
+
### 1. 测试命名
|
|
211
|
+
|
|
212
|
+
使用清晰的描述性名称:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// ✅ 好的命名
|
|
216
|
+
it("应该在没有配置文件时返回默认配置", () => {});
|
|
217
|
+
it("应该正确解析带有 Story ID 的分支名", () => {});
|
|
218
|
+
|
|
219
|
+
// ❌ 不好的命名
|
|
220
|
+
it("测试配置", () => {});
|
|
221
|
+
it("test branch", () => {});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 2. 一个测试一个断言
|
|
225
|
+
|
|
226
|
+
尽量让每个测试只验证一个行为:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// ✅ 好的做法
|
|
230
|
+
it("应该返回正确的分支名", () => {
|
|
231
|
+
const result = getBranchName("feature", "PROJ-123", "login");
|
|
232
|
+
expect(result).toBe("feature/20260116-PROJ-123-login");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("应该处理没有 Story ID 的情况", () => {
|
|
236
|
+
const result = getBranchName("feature", "", "login");
|
|
237
|
+
expect(result).toBe("feature/20260116-login");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ❌ 不好的做法
|
|
241
|
+
it("测试分支名生成", () => {
|
|
242
|
+
expect(getBranchName("feature", "PROJ-123", "login")).toBe("...");
|
|
243
|
+
expect(getBranchName("feature", "", "login")).toBe("...");
|
|
244
|
+
expect(getBranchName("hotfix", "BUG-456", "fix")).toBe("...");
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 3. 使用 beforeEach 准备测试数据
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
describe("配置管理", () => {
|
|
252
|
+
let mockConfig: Config;
|
|
253
|
+
|
|
254
|
+
beforeEach(() => {
|
|
255
|
+
mockConfig = {
|
|
256
|
+
branch: { prefix: "feature" },
|
|
257
|
+
commit: { useAI: true },
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("应该正确读取配置", () => {
|
|
262
|
+
// 使用 mockConfig
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 4. 清理 Mock
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
vi.clearAllMocks(); // 清除调用记录
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
afterEach(() => {
|
|
275
|
+
vi.restoreAllMocks(); // 恢复原始实现
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 5. 测试边界情况
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
describe("版本号解析", () => {
|
|
283
|
+
it("应该处理标准版本号", () => {
|
|
284
|
+
expect(parseVersion("v1.2.3")).toEqual({ major: 1, minor: 2, patch: 3 });
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("应该处理没有 v 前缀的版本号", () => {
|
|
288
|
+
expect(parseVersion("1.2.3")).toEqual({ major: 1, minor: 2, patch: 3 });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("应该处理预发布版本", () => {
|
|
292
|
+
expect(parseVersion("v1.2.3-alpha.1")).toEqual({
|
|
293
|
+
major: 1,
|
|
294
|
+
minor: 2,
|
|
295
|
+
patch: 3,
|
|
296
|
+
prerelease: "alpha.1",
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("应该处理无效版本号", () => {
|
|
301
|
+
expect(() => parseVersion("invalid")).toThrow();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## 测试示例
|
|
307
|
+
|
|
308
|
+
### 示例 1: 测试配置读取
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { describe, it, expect, vi } from "vitest";
|
|
312
|
+
import { readConfig } from "../src/config.js";
|
|
313
|
+
|
|
314
|
+
vi.mock("fs", () => ({
|
|
315
|
+
existsSync: vi.fn(),
|
|
316
|
+
readFileSync: vi.fn(),
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
describe("配置读取", () => {
|
|
320
|
+
it("应该读取全局配置", () => {
|
|
321
|
+
const { existsSync, readFileSync } = await import("fs");
|
|
322
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
323
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
324
|
+
JSON.stringify({ branch: { prefix: "feat" } })
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const config = readConfig();
|
|
328
|
+
|
|
329
|
+
expect(config.branch.prefix).toBe("feat");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("应该在配置文件不存在时返回默认配置", () => {
|
|
333
|
+
const { existsSync } = await import("fs");
|
|
334
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
335
|
+
|
|
336
|
+
const config = readConfig();
|
|
337
|
+
|
|
338
|
+
expect(config).toEqual(DEFAULT_CONFIG);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 示例 2: 测试 Git 命令
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { describe, it, expect, vi } from "vitest";
|
|
347
|
+
import { execSync } from "child_process";
|
|
348
|
+
|
|
349
|
+
vi.mock("child_process", () => ({
|
|
350
|
+
execSync: vi.fn(),
|
|
351
|
+
}));
|
|
352
|
+
|
|
353
|
+
describe("分支创建", () => {
|
|
354
|
+
it("应该执行正确的 Git 命令", () => {
|
|
355
|
+
const mockExecSync = vi.mocked(execSync);
|
|
356
|
+
|
|
357
|
+
createBranch("feature/20260116-PROJ-123-login");
|
|
358
|
+
|
|
359
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
360
|
+
'git checkout -b "feature/20260116-PROJ-123-login"',
|
|
361
|
+
{ stdio: "pipe" }
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 示例 3: 测试 AI 服务
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { describe, it, expect, vi } from "vitest";
|
|
371
|
+
import { generateCommitMessage } from "../src/ai-service.js";
|
|
372
|
+
|
|
373
|
+
describe("AI 提交消息生成", () => {
|
|
374
|
+
it("应该生成符合规范的提交消息", async () => {
|
|
375
|
+
const diff = "diff --git a/file.js...";
|
|
376
|
+
|
|
377
|
+
const message = await generateCommitMessage(diff, {
|
|
378
|
+
provider: "github",
|
|
379
|
+
apiKey: "test-key",
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
expect(message).toMatch(/^(feat|fix|docs|style|refactor|test|chore)/);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("应该处理 API 错误", async () => {
|
|
386
|
+
await expect(
|
|
387
|
+
generateCommitMessage("diff", { provider: "invalid" })
|
|
388
|
+
).rejects.toThrow();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## 持续集成
|
|
394
|
+
|
|
395
|
+
测试会在以下情况自动运行:
|
|
396
|
+
|
|
397
|
+
- 每次 push 到 GitHub
|
|
398
|
+
- 每次创建 Pull Request
|
|
399
|
+
- 发布新版本前
|
|
400
|
+
|
|
401
|
+
GitHub Actions 配置在 `.github/workflows/` 目录。
|
|
402
|
+
|
|
403
|
+
## 调试测试
|
|
404
|
+
|
|
405
|
+
### 使用 console.log
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
it("调试测试", () => {
|
|
409
|
+
const result = someFunction();
|
|
410
|
+
console.log("Result:", result);
|
|
411
|
+
expect(result).toBe("expected");
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 运行单个测试
|
|
416
|
+
|
|
417
|
+
```bash
|
|
418
|
+
# 运行特定文件
|
|
419
|
+
npm test -- tests/commit.test.ts
|
|
420
|
+
|
|
421
|
+
# 运行特定测试(使用 .only)
|
|
422
|
+
it.only("只运行这个测试", () => {
|
|
423
|
+
// ...
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 跳过测试
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
it.skip("暂时跳过这个测试", () => {
|
|
431
|
+
// ...
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## 相关资源
|
|
436
|
+
|
|
437
|
+
- [Vitest 文档](https://vitest.dev/)
|
|
438
|
+
- [测试覆盖率报告](https://github.com/iamzjt-front-end/git-workflow/blob/main/TEST_COVERAGE_SUMMARY.md)
|
|
439
|
+
- [测试说明](https://github.com/iamzjt-front-end/git-workflow/blob/main/TESTING.md)
|
|
440
|
+
|
|
441
|
+
## 贡献测试
|
|
442
|
+
|
|
443
|
+
在提交 PR 时,请确保:
|
|
444
|
+
|
|
445
|
+
1. ✅ 所有测试通过
|
|
446
|
+
2. ✅ 新功能有对应的测试
|
|
447
|
+
3. ✅ 测试覆盖率不降低
|
|
448
|
+
4. ✅ 测试命名清晰
|
|
449
|
+
5. ✅ Mock 正确清理
|
|
450
|
+
|
|
451
|
+
运行测试:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
npm test
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
查看覆盖率:
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
npm run test:coverage
|
|
461
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zjex/git-workflow",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"test:ui": "vitest --ui",
|
|
19
19
|
"test:coverage": "vitest --coverage",
|
|
20
20
|
"test:update-readme": "node scripts/update-test-count.js",
|
|
21
|
-
"changelog": "
|
|
21
|
+
"changelog": "node scripts/generate-changelog-manual.js",
|
|
22
|
+
"changelog:full": "node scripts/generate-changelog-manual.js",
|
|
22
23
|
"version": "node scripts/version.js",
|
|
23
24
|
"release": "./scripts/release.sh",
|
|
24
25
|
"release:dry": "./scripts/release.sh --dry-run",
|
|
@@ -63,7 +64,6 @@
|
|
|
63
64
|
"@types/semver": "^7.7.1",
|
|
64
65
|
"@vitest/coverage-v8": "^4.0.16",
|
|
65
66
|
"@vitest/ui": "^4.0.16",
|
|
66
|
-
"changelogen": "^0.6.2",
|
|
67
67
|
"husky": "^9.1.7",
|
|
68
68
|
"tsup": "^8.5.1",
|
|
69
69
|
"tsx": "^4.21.0",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 手动生成 CHANGELOG.md
|
|
5
|
+
* 直接使用 git log,避免 changelogen 的编码问题
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
import { writeFileSync } from "fs";
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
console.log(`📝 生成 CHANGELOG...`);
|
|
14
|
+
|
|
15
|
+
// 获取所有 tags
|
|
16
|
+
const tags = execSync("git tag -l --sort=-version:refname", {
|
|
17
|
+
encoding: "utf8",
|
|
18
|
+
env: { ...process.env, LANG: "zh_CN.UTF-8", LC_ALL: "zh_CN.UTF-8" },
|
|
19
|
+
})
|
|
20
|
+
.trim()
|
|
21
|
+
.split("\n");
|
|
22
|
+
|
|
23
|
+
let changelog = "# Changelog\n\n";
|
|
24
|
+
|
|
25
|
+
// 为每个版本生成变更日志
|
|
26
|
+
for (let i = 0; i < tags.length; i++) {
|
|
27
|
+
const currentTag = tags[i];
|
|
28
|
+
const previousTag = tags[i + 1];
|
|
29
|
+
|
|
30
|
+
if (!previousTag) break;
|
|
31
|
+
|
|
32
|
+
changelog += `## [${currentTag}](https://github.com/iamzjt-front-end/git-workflow/compare/${previousTag}...${currentTag}) (${getTagDate(
|
|
33
|
+
currentTag
|
|
34
|
+
)})\n\n`;
|
|
35
|
+
|
|
36
|
+
// 获取该版本的提交
|
|
37
|
+
const commits = execSync(
|
|
38
|
+
`git log ${previousTag}..${currentTag} --pretty=format:"%s|%h" --no-merges`,
|
|
39
|
+
{
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
env: { ...process.env, LANG: "zh_CN.UTF-8", LC_ALL: "zh_CN.UTF-8" },
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
.trim()
|
|
45
|
+
.split("\n")
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
|
|
48
|
+
// 按类型分组
|
|
49
|
+
const groups = {
|
|
50
|
+
"✨ Features": [],
|
|
51
|
+
"🐛 Bug Fixes": [],
|
|
52
|
+
"📖 Documentation": [],
|
|
53
|
+
"🎨 Styles": [],
|
|
54
|
+
"♻️ Refactors": [],
|
|
55
|
+
"⚡ Performance": [],
|
|
56
|
+
"✅ Tests": [],
|
|
57
|
+
"🔧 Chore": [],
|
|
58
|
+
"🤖 CI": [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
commits.forEach((commit) => {
|
|
62
|
+
const [message, hash] = commit.split("|");
|
|
63
|
+
const link = `([${hash}](https://github.com/iamzjt-front-end/git-workflow/commit/${hash}))`;
|
|
64
|
+
|
|
65
|
+
if (message.match(/^(feat|✨)/i)) {
|
|
66
|
+
groups["✨ Features"].push(
|
|
67
|
+
`- ${message.replace(/^(feat|✨)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
68
|
+
);
|
|
69
|
+
} else if (message.match(/^(fix|🐛)/i)) {
|
|
70
|
+
groups["🐛 Bug Fixes"].push(
|
|
71
|
+
`- ${message.replace(/^(fix|🐛)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
72
|
+
);
|
|
73
|
+
} else if (message.match(/^(docs|📖|📝)/i)) {
|
|
74
|
+
groups["📖 Documentation"].push(
|
|
75
|
+
`- ${message.replace(/^(docs|📖|📝)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
76
|
+
);
|
|
77
|
+
} else if (message.match(/^(style|🎨)/i)) {
|
|
78
|
+
groups["🎨 Styles"].push(
|
|
79
|
+
`- ${message.replace(/^(style|🎨)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
80
|
+
);
|
|
81
|
+
} else if (message.match(/^(refactor|♻️)/i)) {
|
|
82
|
+
groups["♻️ Refactors"].push(
|
|
83
|
+
`- ${message.replace(/^(refactor|♻️)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
84
|
+
);
|
|
85
|
+
} else if (message.match(/^(perf|⚡)/i)) {
|
|
86
|
+
groups["⚡ Performance"].push(
|
|
87
|
+
`- ${message.replace(/^(perf|⚡)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
88
|
+
);
|
|
89
|
+
} else if (message.match(/^(test|✅)/i)) {
|
|
90
|
+
groups["✅ Tests"].push(
|
|
91
|
+
`- ${message.replace(/^(test|✅)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
92
|
+
);
|
|
93
|
+
} else if (message.match(/^(chore|🔧|🏡)/i)) {
|
|
94
|
+
groups["🔧 Chore"].push(
|
|
95
|
+
`- ${message.replace(/^(chore|🔧|🏡)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
96
|
+
);
|
|
97
|
+
} else if (message.match(/^(ci|🤖)/i)) {
|
|
98
|
+
groups["🤖 CI"].push(
|
|
99
|
+
`- ${message.replace(/^(ci|🤖)[:(]\w*\)?:?\s*/i, "")} ${link}`
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
groups["🔧 Chore"].push(`- ${message} ${link}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 输出各分组
|
|
107
|
+
Object.entries(groups).forEach(([title, items]) => {
|
|
108
|
+
if (items.length > 0) {
|
|
109
|
+
changelog += `### ${title}\n\n`;
|
|
110
|
+
items.forEach((item) => {
|
|
111
|
+
changelog += `${item}\n`;
|
|
112
|
+
});
|
|
113
|
+
changelog += "\n";
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
changelog += "\n";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 写入文件(添加 UTF-8 BOM 以确保编辑器正确识别编码)
|
|
121
|
+
const BOM = "\uFEFF";
|
|
122
|
+
writeFileSync("CHANGELOG.md", BOM + changelog, { encoding: "utf8" });
|
|
123
|
+
console.log("✅ CHANGELOG.md 生成成功!");
|
|
124
|
+
|
|
125
|
+
function getTagDate(tag) {
|
|
126
|
+
try {
|
|
127
|
+
const date = execSync(`git log -1 --format=%ai ${tag}`, {
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
env: { ...process.env, LANG: "zh_CN.UTF-8", LC_ALL: "zh_CN.UTF-8" },
|
|
130
|
+
}).trim();
|
|
131
|
+
return date.split(" ")[0];
|
|
132
|
+
} catch {
|
|
133
|
+
return new Date().toISOString().split("T")[0];
|
|
134
|
+
}
|
|
135
|
+
}
|