@zjex/git-workflow 0.5.2 → 0.6.0
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 +12 -0
- package/README.md +1 -1
- package/dist/index.js +155 -71
- package/docs/commands/config.md +4 -1
- package/docs/commands/review.md +3 -0
- package/docs/config/config-file.md +19 -1
- package/docs/config/index.md +3 -1
- package/docs/guide/ai-review.md +6 -0
- package/docs/guide/tag-management.md +14 -1
- package/package.json +2 -2
- package/src/commands/init.ts +18 -0
- package/src/commands/review.ts +59 -16
- package/src/commands/tag.ts +39 -15
- package/src/config.ts +3 -0
- package/src/tag-utils.ts +37 -0
- package/src/update-notifier.ts +46 -42
- package/tests/config.test.ts +18 -0
- package/tests/init.test.ts +111 -275
- package/tests/tag.test.ts +67 -24
- package/tests/update-notifier.test.ts +29 -76
package/tests/tag.test.ts
CHANGED
|
@@ -1,46 +1,89 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
extractTagPrefix,
|
|
4
|
+
getLatestTagCommand,
|
|
5
|
+
isValidVersionTag,
|
|
6
|
+
normalizeTagLookupStrategy,
|
|
7
|
+
shouldFetchAllTagsForCreateTag,
|
|
8
|
+
} from "../src/tag-utils";
|
|
2
9
|
|
|
3
10
|
describe("Tag 功能测试", () => {
|
|
11
|
+
describe("Tag 策略", () => {
|
|
12
|
+
it("默认应该使用 latest 策略", () => {
|
|
13
|
+
expect(normalizeTagLookupStrategy(undefined)).toBe("latest");
|
|
14
|
+
expect(normalizeTagLookupStrategy("unknown")).toBe("latest");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("应该识别 latest 策略", () => {
|
|
18
|
+
expect(normalizeTagLookupStrategy("latest")).toBe("latest");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("all 策略应该始终全量拉取 tags", () => {
|
|
22
|
+
expect(shouldFetchAllTagsForCreateTag("all", "v")).toBe(true);
|
|
23
|
+
expect(shouldFetchAllTagsForCreateTag("all")).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("latest 策略在已知前缀时不应该全量拉取 tags", () => {
|
|
27
|
+
expect(shouldFetchAllTagsForCreateTag("latest", "v")).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("latest 策略在未知前缀时应该回退为全量拉取", () => {
|
|
31
|
+
expect(shouldFetchAllTagsForCreateTag("latest")).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("应该生成全量排序的最新 tag 查询命令", () => {
|
|
35
|
+
expect(getLatestTagCommand("v", "all")).toBe(
|
|
36
|
+
'git tag -l "v*" --sort=-v:refname',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("应该生成按时间获取最新 tag 的查询命令", () => {
|
|
41
|
+
expect(getLatestTagCommand("v", "latest")).toBe(
|
|
42
|
+
'git for-each-ref --sort=-creatordate --format="%(refname:short)" "refs/tags/v*"',
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
4
47
|
describe("前缀提取", () => {
|
|
5
48
|
it("应该正确提取 v 前缀", () => {
|
|
6
49
|
const tag = "v0.1.0";
|
|
7
|
-
const prefix = tag
|
|
50
|
+
const prefix = extractTagPrefix(tag);
|
|
8
51
|
expect(prefix).toBe("v");
|
|
9
52
|
});
|
|
10
53
|
|
|
11
54
|
it("应该正确提取 release- 前缀", () => {
|
|
12
55
|
const tag = "release-1.0.0";
|
|
13
|
-
const prefix = tag
|
|
56
|
+
const prefix = extractTagPrefix(tag);
|
|
14
57
|
expect(prefix).toBe("release-");
|
|
15
58
|
});
|
|
16
59
|
|
|
17
60
|
it("应该正确提取 @ 开头的 scope 前缀", () => {
|
|
18
61
|
const tag = "@scope/package@1.0.0";
|
|
19
|
-
const prefix = tag
|
|
62
|
+
const prefix = extractTagPrefix(tag);
|
|
20
63
|
expect(prefix).toBe("@scope/package@");
|
|
21
64
|
});
|
|
22
65
|
|
|
23
66
|
it("应该正确处理无前缀 tag", () => {
|
|
24
67
|
const tag = "1.0.0";
|
|
25
|
-
const prefix = tag
|
|
68
|
+
const prefix = extractTagPrefix(tag) || "(无前缀)";
|
|
26
69
|
expect(prefix).toBe("(无前缀)");
|
|
27
70
|
});
|
|
28
71
|
|
|
29
72
|
it("应该正确提取 g 前缀", () => {
|
|
30
73
|
const tag = "g0.1.0";
|
|
31
|
-
const prefix = tag
|
|
74
|
+
const prefix = extractTagPrefix(tag);
|
|
32
75
|
expect(prefix).toBe("g");
|
|
33
76
|
});
|
|
34
77
|
|
|
35
78
|
it("应该正确提取带下划线的前缀", () => {
|
|
36
79
|
const tag = "version_1.0.0";
|
|
37
|
-
const prefix = tag
|
|
80
|
+
const prefix = extractTagPrefix(tag);
|
|
38
81
|
expect(prefix).toBe("version_");
|
|
39
82
|
});
|
|
40
83
|
|
|
41
84
|
it("应该正确提取带点的前缀", () => {
|
|
42
85
|
const tag = "v.1.0.0";
|
|
43
|
-
const prefix = tag
|
|
86
|
+
const prefix = extractTagPrefix(tag);
|
|
44
87
|
expect(prefix).toBe("v.");
|
|
45
88
|
});
|
|
46
89
|
});
|
|
@@ -51,7 +94,7 @@ describe("Tag 功能测试", () => {
|
|
|
51
94
|
const grouped = new Map<string, string[]>();
|
|
52
95
|
|
|
53
96
|
tags.forEach((tag) => {
|
|
54
|
-
const prefix = tag
|
|
97
|
+
const prefix = extractTagPrefix(tag) || "(无前缀)";
|
|
55
98
|
if (!grouped.has(prefix)) {
|
|
56
99
|
grouped.set(prefix, []);
|
|
57
100
|
}
|
|
@@ -75,7 +118,7 @@ describe("Tag 功能测试", () => {
|
|
|
75
118
|
const grouped = new Map<string, string[]>();
|
|
76
119
|
|
|
77
120
|
tags.forEach((tag) => {
|
|
78
|
-
const prefix = tag
|
|
121
|
+
const prefix = extractTagPrefix(tag) || "(无前缀)";
|
|
79
122
|
if (!grouped.has(prefix)) {
|
|
80
123
|
grouped.set(prefix, []);
|
|
81
124
|
}
|
|
@@ -94,7 +137,7 @@ describe("Tag 功能测试", () => {
|
|
|
94
137
|
const grouped = new Map<string, string[]>();
|
|
95
138
|
|
|
96
139
|
tags.forEach((tag) => {
|
|
97
|
-
const prefix = tag
|
|
140
|
+
const prefix = extractTagPrefix(tag) || "(无前缀)";
|
|
98
141
|
if (!grouped.has(prefix)) {
|
|
99
142
|
grouped.set(prefix, []);
|
|
100
143
|
}
|
|
@@ -397,56 +440,56 @@ describe("Tag 功能测试", () => {
|
|
|
397
440
|
describe("无效标签检测", () => {
|
|
398
441
|
it("应该识别不包含数字的标签为无效", () => {
|
|
399
442
|
const tag = "vnull";
|
|
400
|
-
const isInvalid =
|
|
443
|
+
const isInvalid = !isValidVersionTag(tag);
|
|
401
444
|
|
|
402
445
|
expect(isInvalid).toBe(true);
|
|
403
446
|
});
|
|
404
447
|
|
|
405
448
|
it("应该识别 vundefined 为无效标签", () => {
|
|
406
449
|
const tag = "vundefined";
|
|
407
|
-
const isInvalid =
|
|
450
|
+
const isInvalid = !isValidVersionTag(tag);
|
|
408
451
|
|
|
409
452
|
expect(isInvalid).toBe(true);
|
|
410
453
|
});
|
|
411
454
|
|
|
412
455
|
it("应该识别空版本号为无效标签", () => {
|
|
413
456
|
const tag = "v";
|
|
414
|
-
const isInvalid =
|
|
457
|
+
const isInvalid = !isValidVersionTag(tag);
|
|
415
458
|
|
|
416
459
|
expect(isInvalid).toBe(true);
|
|
417
460
|
});
|
|
418
461
|
|
|
419
462
|
it("应该识别纯字母标签为无效", () => {
|
|
420
463
|
const tag = "release";
|
|
421
|
-
const isInvalid =
|
|
464
|
+
const isInvalid = !isValidVersionTag(tag);
|
|
422
465
|
|
|
423
466
|
expect(isInvalid).toBe(true);
|
|
424
467
|
});
|
|
425
468
|
|
|
426
469
|
it("应该识别包含数字的标签为有效", () => {
|
|
427
470
|
const tag = "v1.0.0";
|
|
428
|
-
const isValid =
|
|
471
|
+
const isValid = isValidVersionTag(tag);
|
|
429
472
|
|
|
430
473
|
expect(isValid).toBe(true);
|
|
431
474
|
});
|
|
432
475
|
|
|
433
476
|
it("应该识别预发布版本为有效", () => {
|
|
434
477
|
const tag = "v1.0.0-beta.1";
|
|
435
|
-
const isValid =
|
|
478
|
+
const isValid = isValidVersionTag(tag);
|
|
436
479
|
|
|
437
480
|
expect(isValid).toBe(true);
|
|
438
481
|
});
|
|
439
482
|
|
|
440
483
|
it("应该识别无前缀版本号为有效", () => {
|
|
441
484
|
const tag = "1.0.0";
|
|
442
|
-
const isValid =
|
|
485
|
+
const isValid = isValidVersionTag(tag);
|
|
443
486
|
|
|
444
487
|
expect(isValid).toBe(true);
|
|
445
488
|
});
|
|
446
489
|
|
|
447
490
|
it("应该识别带前缀的单数字版本为有效", () => {
|
|
448
491
|
const tag = "v1";
|
|
449
|
-
const isValid =
|
|
492
|
+
const isValid = isValidVersionTag(tag);
|
|
450
493
|
|
|
451
494
|
expect(isValid).toBe(true);
|
|
452
495
|
});
|
|
@@ -463,7 +506,7 @@ describe("Tag 功能测试", () => {
|
|
|
463
506
|
"v",
|
|
464
507
|
"v2.0.0",
|
|
465
508
|
];
|
|
466
|
-
const invalidTags = allTags.filter((tag) =>
|
|
509
|
+
const invalidTags = allTags.filter((tag) => !isValidVersionTag(tag));
|
|
467
510
|
|
|
468
511
|
expect(invalidTags).toEqual(["vnull", "vundefined", "v"]);
|
|
469
512
|
expect(invalidTags.length).toBe(3);
|
|
@@ -471,7 +514,7 @@ describe("Tag 功能测试", () => {
|
|
|
471
514
|
|
|
472
515
|
it("应该在没有无效标签时返回空数组", () => {
|
|
473
516
|
const allTags = ["v1.0.0", "v1.1.0", "v2.0.0", "release-1.0.0"];
|
|
474
|
-
const invalidTags = allTags.filter((tag) =>
|
|
517
|
+
const invalidTags = allTags.filter((tag) => !isValidVersionTag(tag));
|
|
475
518
|
|
|
476
519
|
expect(invalidTags).toEqual([]);
|
|
477
520
|
expect(invalidTags.length).toBe(0);
|
|
@@ -479,7 +522,7 @@ describe("Tag 功能测试", () => {
|
|
|
479
522
|
|
|
480
523
|
it("应该在全是无效标签时返回所有标签", () => {
|
|
481
524
|
const allTags = ["vnull", "vundefined", "v", "release"];
|
|
482
|
-
const invalidTags = allTags.filter((tag) =>
|
|
525
|
+
const invalidTags = allTags.filter((tag) => !isValidVersionTag(tag));
|
|
483
526
|
|
|
484
527
|
expect(invalidTags).toEqual(allTags);
|
|
485
528
|
expect(invalidTags.length).toBe(4);
|
|
@@ -493,7 +536,7 @@ describe("Tag 功能测试", () => {
|
|
|
493
536
|
"vundefined",
|
|
494
537
|
"v2.0.0-beta.1",
|
|
495
538
|
];
|
|
496
|
-
const validTags = allTags.filter(
|
|
539
|
+
const validTags = allTags.filter(isValidVersionTag);
|
|
497
540
|
|
|
498
541
|
expect(validTags).toEqual(["v1.0.0", "v1.1.0", "v2.0.0-beta.1"]);
|
|
499
542
|
expect(validTags.length).toBe(3);
|
|
@@ -501,7 +544,7 @@ describe("Tag 功能测试", () => {
|
|
|
501
544
|
|
|
502
545
|
it("应该处理空标签列表", () => {
|
|
503
546
|
const allTags: string[] = [];
|
|
504
|
-
const invalidTags = allTags.filter((tag) =>
|
|
547
|
+
const invalidTags = allTags.filter((tag) => !isValidVersionTag(tag));
|
|
505
548
|
|
|
506
549
|
expect(invalidTags).toEqual([]);
|
|
507
550
|
expect(invalidTags.length).toBe(0);
|
|
@@ -516,7 +559,7 @@ describe("Tag 功能测试", () => {
|
|
|
516
559
|
"g1.0.0",
|
|
517
560
|
"tag",
|
|
518
561
|
];
|
|
519
|
-
const invalidTags = allTags.filter((tag) =>
|
|
562
|
+
const invalidTags = allTags.filter((tag) => !isValidVersionTag(tag));
|
|
520
563
|
|
|
521
564
|
expect(invalidTags).toEqual(["vnull", "release-", "hotfix", "tag"]);
|
|
522
565
|
expect(invalidTags.length).toBe(4);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { execSync } from "child_process";
|
|
2
|
+
import { execSync, spawn } from "child_process";
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { checkForUpdates, clearUpdateCache } from "../src/update-notifier";
|
|
@@ -28,6 +28,11 @@ describe("Update Notifier 模块测试", () => {
|
|
|
28
28
|
vi.clearAllMocks();
|
|
29
29
|
vi.mocked(homedir).mockReturnValue("/home/user");
|
|
30
30
|
vi.useFakeTimers();
|
|
31
|
+
|
|
32
|
+
// Mock spawn 返回一个带 unref 的对象
|
|
33
|
+
vi.mocked(spawn).mockReturnValue({
|
|
34
|
+
unref: vi.fn(),
|
|
35
|
+
} as any);
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
afterEach(() => {
|
|
@@ -62,16 +67,20 @@ describe("Update Notifier 模块测试", () => {
|
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
describe("checkForUpdates 函数", () => {
|
|
65
|
-
it("
|
|
70
|
+
it("没有缓存时应该启动后台检查", async () => {
|
|
66
71
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
67
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
68
72
|
|
|
69
73
|
await checkForUpdates("1.0.0");
|
|
70
74
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// 没有缓存时应该启动子进程检查
|
|
76
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
77
|
+
"node",
|
|
78
|
+
expect.arrayContaining(["-e", expect.any(String)]),
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
detached: true,
|
|
81
|
+
stdio: "ignore",
|
|
82
|
+
})
|
|
83
|
+
);
|
|
75
84
|
});
|
|
76
85
|
|
|
77
86
|
it("版本相同时不应该显示提示", async () => {
|
|
@@ -153,61 +162,20 @@ describe("Update Notifier 模块测试", () => {
|
|
|
153
162
|
consoleSpy.mockRestore();
|
|
154
163
|
});
|
|
155
164
|
|
|
156
|
-
it("
|
|
157
|
-
const mockCache = {
|
|
158
|
-
lastCheck: Date.now(),
|
|
159
|
-
latestVersion: "1.0.0",
|
|
160
|
-
checkedVersion: "1.0.0",
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
164
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
165
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
166
|
-
|
|
167
|
-
await checkForUpdates("1.0.0");
|
|
168
|
-
await vi.runAllTimersAsync();
|
|
169
|
-
|
|
170
|
-
// 每次运行都应该后台检查
|
|
171
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("后台检查应该更新缓存中的最新版本", async () => {
|
|
175
|
-
const mockCache = {
|
|
176
|
-
lastCheck: Date.now() - 2 * 60 * 60 * 1000,
|
|
177
|
-
latestVersion: "1.0.0",
|
|
178
|
-
checkedVersion: "1.0.0",
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
182
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
183
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
184
|
-
|
|
185
|
-
await checkForUpdates("1.0.0");
|
|
186
|
-
await vi.runAllTimersAsync();
|
|
187
|
-
|
|
188
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("有新版本时每次都应该后台检查", async () => {
|
|
165
|
+
it("每次运行都应该启动后台检查", async () => {
|
|
192
166
|
const mockCache = {
|
|
193
167
|
lastCheck: Date.now(), // 刚刚检查过
|
|
194
|
-
latestVersion: "1.0.
|
|
168
|
+
latestVersion: "1.0.0",
|
|
195
169
|
checkedVersion: "1.0.0",
|
|
196
170
|
};
|
|
197
171
|
|
|
198
172
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
199
173
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
200
|
-
vi.mocked(execSync).mockReturnValue("1.0.2" as any);
|
|
201
|
-
|
|
202
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
203
174
|
|
|
204
175
|
await checkForUpdates("1.0.0");
|
|
205
|
-
await vi.runAllTimersAsync();
|
|
206
|
-
|
|
207
|
-
// 即使刚检查过,有新版本时也应该继续检查
|
|
208
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
209
176
|
|
|
210
|
-
|
|
177
|
+
// 每次都应该启动子进程检查
|
|
178
|
+
expect(spawn).toHaveBeenCalled();
|
|
211
179
|
});
|
|
212
180
|
|
|
213
181
|
it("缓存文件损坏时应该静默处理", async () => {
|
|
@@ -310,7 +278,7 @@ describe("Update Notifier 模块测试", () => {
|
|
|
310
278
|
{ current: "1.0.0", latest: "1.0.1", shouldShow: true },
|
|
311
279
|
{ current: "1.0.0", latest: "1.1.0", shouldShow: true },
|
|
312
280
|
{ current: "1.0.0", latest: "2.0.0", shouldShow: true },
|
|
313
|
-
{ current: "1.0.1", latest: "1.0.0", shouldShow:
|
|
281
|
+
{ current: "1.0.1", latest: "1.0.0", shouldShow: false }, // 本地版本更高,不提示
|
|
314
282
|
{ current: "1.0.0", latest: "1.0.0", shouldShow: false },
|
|
315
283
|
];
|
|
316
284
|
|
|
@@ -343,23 +311,8 @@ describe("Update Notifier 模块测试", () => {
|
|
|
343
311
|
});
|
|
344
312
|
|
|
345
313
|
describe("缓存读写", () => {
|
|
346
|
-
it("应该正确写入缓存", async () => {
|
|
347
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
348
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
349
|
-
|
|
350
|
-
await checkForUpdates("1.0.0");
|
|
351
|
-
await vi.runAllTimersAsync();
|
|
352
|
-
|
|
353
|
-
expect(writeFileSync).toHaveBeenCalledWith(
|
|
354
|
-
"/home/user/.gw-update-check",
|
|
355
|
-
expect.stringContaining("1.0.1"),
|
|
356
|
-
"utf-8"
|
|
357
|
-
);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
314
|
it("写入缓存失败时应该静默处理", async () => {
|
|
361
315
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
362
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
363
316
|
vi.mocked(writeFileSync).mockImplementation(() => {
|
|
364
317
|
throw new Error("Write failed");
|
|
365
318
|
});
|
|
@@ -380,24 +333,24 @@ describe("Update Notifier 模块测试", () => {
|
|
|
380
333
|
describe("网络请求", () => {
|
|
381
334
|
it("获取最新版本失败时应该静默处理", async () => {
|
|
382
335
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
383
|
-
vi.mocked(
|
|
384
|
-
throw new Error("
|
|
336
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
337
|
+
throw new Error("Spawn error");
|
|
385
338
|
});
|
|
386
339
|
|
|
387
340
|
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
388
341
|
});
|
|
389
342
|
|
|
390
|
-
it("
|
|
343
|
+
it("后台检查应该使用正确的参数", async () => {
|
|
391
344
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
392
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
393
345
|
|
|
394
346
|
await checkForUpdates("1.0.0", "@zjex/git-workflow");
|
|
395
|
-
await vi.runAllTimersAsync();
|
|
396
347
|
|
|
397
|
-
expect(
|
|
398
|
-
"
|
|
348
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
349
|
+
"node",
|
|
350
|
+
expect.arrayContaining(["-e", expect.stringContaining("npm view @zjex/git-workflow version")]),
|
|
399
351
|
expect.objectContaining({
|
|
400
|
-
|
|
352
|
+
detached: true,
|
|
353
|
+
stdio: "ignore",
|
|
401
354
|
})
|
|
402
355
|
);
|
|
403
356
|
});
|