@zjex/git-workflow 0.5.3 → 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 +7 -0
- package/README.md +1 -1
- package/dist/index.js +68 -15
- package/docs/commands/config.md +4 -1
- package/docs/config/config-file.md +19 -1
- package/docs/config/index.md +3 -1
- package/docs/guide/tag-management.md +14 -1
- package/package.json +2 -2
- package/src/commands/init.ts +18 -0
- package/src/commands/tag.ts +39 -15
- package/src/config.ts +3 -0
- package/src/tag-utils.ts +37 -0
- package/tests/config.test.ts +18 -0
- package/tests/init.test.ts +111 -275
- package/tests/tag.test.ts +67 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.5.3](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.2...v0.5.3) (2026-02-06)
|
|
4
|
+
|
|
5
|
+
- 🔖 chore(release): 发布 v0.5.3 ([a6b327d](https://github.com/iamzjt-front-end/git-workflow/commit/a6b327d))
|
|
6
|
+
- docs(review): Clarify commit range syntax and refactor parsing logic ([63a77d2](https://github.com/iamzjt-front-end/git-workflow/commit/63a77d2))
|
|
7
|
+
- 📝 docs: 自动更新测试数量徽章 [skip ci] ([21e365c](https://github.com/iamzjt-front-end/git-workflow/commit/21e365c))
|
|
8
|
+
- feat(review): Add commit range syntax support and improve update notifier ([93184b0](https://github.com/iamzjt-front-end/git-workflow/commit/93184b0))
|
|
9
|
+
|
|
3
10
|
## [v0.5.2](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.1...v0.5.2) (2026-02-06)
|
|
4
11
|
|
|
5
12
|
- 🔖 chore(release): 发布 v0.5.2 ([d2b659c](https://github.com/iamzjt-front-end/git-workflow/commit/d2b659c))
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<a href="https://github.com/iamzjt-front-end/git-workflow"><img src="https://img.shields.io/github/stars/iamzjt-front-end/git-workflow?style=flat&colorA=18181B&colorB=F59E0B" alt="github stars"></a>
|
|
13
13
|
<a href="https://github.com/iamzjt-front-end/git-workflow/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zjex/git-workflow?style=flat&colorA=18181B&colorB=10B981" alt="license"></a>
|
|
14
14
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat&logo=node.js&logoColor=white&colorA=18181B" alt="node version"></a>
|
|
15
|
-
<a href="https://github.com/iamzjt-front-end/git-workflow/actions"><img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<a href="https://github.com/iamzjt-front-end/git-workflow/actions"><img src="https://img.shields.io/badge/tests-579%20passed-success?style=flat&colorA=18181B" alt="tests"></a>
|
|
16
16
|
<a href="https://github.com/iamzjt-front-end/git-workflow/issues"><img src="https://img.shields.io/github/issues/iamzjt-front-end/git-workflow?style=flat&colorA=18181B&colorB=EC4899" alt="issues"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
package/dist/index.js
CHANGED
|
@@ -421,6 +421,7 @@ var defaultConfig = {
|
|
|
421
421
|
requireId: false,
|
|
422
422
|
featureIdLabel: "Story ID",
|
|
423
423
|
hotfixIdLabel: "Issue ID",
|
|
424
|
+
tagLookupStrategy: "latest",
|
|
424
425
|
autoStage: true,
|
|
425
426
|
useEmoji: true
|
|
426
427
|
};
|
|
@@ -747,13 +748,38 @@ async function deleteBranch(branchArg) {
|
|
|
747
748
|
init_utils();
|
|
748
749
|
import { select as select2, input as input2 } from "@inquirer/prompts";
|
|
749
750
|
import ora2 from "ora";
|
|
751
|
+
|
|
752
|
+
// src/tag-utils.ts
|
|
753
|
+
function isValidVersionTag(tag) {
|
|
754
|
+
return /\d/.test(tag);
|
|
755
|
+
}
|
|
756
|
+
function extractTagPrefix(tag) {
|
|
757
|
+
return tag.replace(/\d.*/, "");
|
|
758
|
+
}
|
|
759
|
+
function normalizeTagLookupStrategy(value) {
|
|
760
|
+
return value === "all" ? "all" : "latest";
|
|
761
|
+
}
|
|
762
|
+
function getLatestTagCommand(prefix, strategy) {
|
|
763
|
+
if (strategy === "latest") {
|
|
764
|
+
return `git for-each-ref --sort=-creatordate --format="%(refname:short)" "refs/tags/${prefix}*"`;
|
|
765
|
+
}
|
|
766
|
+
return `git tag -l "${prefix}*" --sort=-v:refname`;
|
|
767
|
+
}
|
|
768
|
+
function shouldFetchAllTagsForCreateTag(strategy, prefix) {
|
|
769
|
+
if (strategy === "all") {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
return !prefix;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/commands/tag.ts
|
|
750
776
|
async function listTags(prefix) {
|
|
751
777
|
const spinner = ora2("\u6B63\u5728\u83B7\u53D6 tags...").start();
|
|
752
778
|
exec("git fetch --tags", true);
|
|
753
779
|
spinner.stop();
|
|
754
780
|
const pattern = prefix ? `${prefix}*` : "";
|
|
755
781
|
const allTags = execOutput(`git tag -l ${pattern} --sort=v:refname`).split("\n").filter(Boolean);
|
|
756
|
-
const tags = allTags.filter(
|
|
782
|
+
const tags = allTags.filter(isValidVersionTag);
|
|
757
783
|
if (tags.length === 0) {
|
|
758
784
|
console.log(
|
|
759
785
|
colors.yellow(prefix ? `\u6CA1\u6709 '${prefix}' \u5F00\u5934\u7684 tag` : "\u6CA1\u6709 tag")
|
|
@@ -773,7 +799,7 @@ async function listTags(prefix) {
|
|
|
773
799
|
}
|
|
774
800
|
const grouped = /* @__PURE__ */ new Map();
|
|
775
801
|
tags.forEach((tag) => {
|
|
776
|
-
const prefix2 = tag
|
|
802
|
+
const prefix2 = extractTagPrefix(tag) || "(\u65E0\u524D\u7F00)";
|
|
777
803
|
if (!grouped.has(prefix2)) {
|
|
778
804
|
grouped.set(prefix2, []);
|
|
779
805
|
}
|
|
@@ -827,23 +853,28 @@ async function listTags(prefix) {
|
|
|
827
853
|
console.log(" " + row);
|
|
828
854
|
}
|
|
829
855
|
}
|
|
830
|
-
function getLatestTag(prefix) {
|
|
831
|
-
const tags = execOutput(
|
|
856
|
+
function getLatestTag(prefix, strategy = "latest") {
|
|
857
|
+
const tags = execOutput(getLatestTagCommand(prefix, strategy)).split("\n").filter((tag) => tag && isValidVersionTag(tag));
|
|
832
858
|
return tags[0] || "";
|
|
833
859
|
}
|
|
834
860
|
async function createTag(inputPrefix) {
|
|
835
861
|
const config2 = getConfig();
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
862
|
+
const tagLookupStrategy = normalizeTagLookupStrategy(
|
|
863
|
+
config2.tagLookupStrategy
|
|
864
|
+
);
|
|
839
865
|
divider();
|
|
840
866
|
let prefix = inputPrefix;
|
|
841
867
|
if (!prefix && config2.defaultTagPrefix) {
|
|
842
868
|
prefix = config2.defaultTagPrefix;
|
|
843
869
|
console.log(colors.dim(`(\u4F7F\u7528\u914D\u7F6E\u7684\u9ED8\u8BA4\u524D\u7F00: ${prefix})`));
|
|
844
870
|
}
|
|
871
|
+
if (shouldFetchAllTagsForCreateTag(tagLookupStrategy, prefix)) {
|
|
872
|
+
const fetchSpinner = ora2("\u6B63\u5728\u83B7\u53D6 tags...").start();
|
|
873
|
+
exec("git fetch --tags", true);
|
|
874
|
+
fetchSpinner.stop();
|
|
875
|
+
}
|
|
845
876
|
if (!prefix) {
|
|
846
|
-
const allTags = execOutput("git tag -l").split("\n").filter((tag) => tag &&
|
|
877
|
+
const allTags = execOutput("git tag -l").split("\n").filter((tag) => tag && isValidVersionTag(tag));
|
|
847
878
|
if (allTags.length === 0) {
|
|
848
879
|
prefix = await input2({
|
|
849
880
|
message: "\u5F53\u524D\u4ED3\u5E93\u6CA1\u6709 tag\uFF0C\u8BF7\u8F93\u5165\u524D\u7F00 (\u5982 v):",
|
|
@@ -889,9 +920,7 @@ async function createTag(inputPrefix) {
|
|
|
889
920
|
}
|
|
890
921
|
return;
|
|
891
922
|
}
|
|
892
|
-
const prefixes = [
|
|
893
|
-
...new Set(allTags.map((t) => t.replace(/\d.*/, "")).filter(Boolean))
|
|
894
|
-
];
|
|
923
|
+
const prefixes = [...new Set(allTags.map(extractTagPrefix).filter(Boolean))];
|
|
895
924
|
if (prefixes.length === 0) {
|
|
896
925
|
prefix = await input2({
|
|
897
926
|
message: "\u8BF7\u8F93\u5165 tag \u524D\u7F00 (\u5982 v):",
|
|
@@ -904,7 +933,7 @@ async function createTag(inputPrefix) {
|
|
|
904
933
|
}
|
|
905
934
|
} else {
|
|
906
935
|
const prefixWithDate = prefixes.map((p) => {
|
|
907
|
-
const latest = getLatestTag(p);
|
|
936
|
+
const latest = getLatestTag(p, tagLookupStrategy);
|
|
908
937
|
const date = latest ? execOutput(`git log -1 --format=%ct "${latest}" 2>/dev/null`) : "0";
|
|
909
938
|
return { prefix: p, latest, date: parseInt(date) || 0 };
|
|
910
939
|
});
|
|
@@ -929,7 +958,13 @@ async function createTag(inputPrefix) {
|
|
|
929
958
|
}
|
|
930
959
|
}
|
|
931
960
|
}
|
|
932
|
-
|
|
961
|
+
let latestTag = getLatestTag(prefix, tagLookupStrategy);
|
|
962
|
+
if (!latestTag && tagLookupStrategy === "latest") {
|
|
963
|
+
const fetchSpinner = ora2("\u672C\u5730\u672A\u627E\u5230\u5BF9\u5E94 tag\uFF0C\u6B63\u5728\u5168\u91CF\u540C\u6B65\u4E00\u6B21...").start();
|
|
964
|
+
exec("git fetch --tags", true);
|
|
965
|
+
fetchSpinner.stop();
|
|
966
|
+
latestTag = getLatestTag(prefix, tagLookupStrategy);
|
|
967
|
+
}
|
|
933
968
|
if (!latestTag) {
|
|
934
969
|
const newTag = `${prefix}1.0.0`;
|
|
935
970
|
console.log(
|
|
@@ -948,7 +983,8 @@ async function createTag(inputPrefix) {
|
|
|
948
983
|
}
|
|
949
984
|
return;
|
|
950
985
|
}
|
|
951
|
-
|
|
986
|
+
const strategyText = tagLookupStrategy === "latest" ? "\u6700\u65B0\u521B\u5EFA" : "\u7248\u672C\u6392\u5E8F";
|
|
987
|
+
console.log(colors.yellow(`\u5F53\u524D\u57FA\u51C6 tag (${strategyText}): ${latestTag}`));
|
|
952
988
|
divider();
|
|
953
989
|
const version2 = latestTag.slice(prefix.length);
|
|
954
990
|
const preReleaseMatch = version2.match(
|
|
@@ -1588,6 +1624,23 @@ async function init() {
|
|
|
1588
1624
|
theme
|
|
1589
1625
|
});
|
|
1590
1626
|
if (defaultTagPrefix) config2.defaultTagPrefix = defaultTagPrefix;
|
|
1627
|
+
const tagLookupStrategy = await select4({
|
|
1628
|
+
message: "Tag \u9012\u589E\u57FA\u51C6\u7B56\u7565:",
|
|
1629
|
+
choices: [
|
|
1630
|
+
{
|
|
1631
|
+
name: "\u4EC5\u57FA\u4E8E\u6700\u65B0\u521B\u5EFA\u7684 Tag\uFF08\u9ED8\u8BA4\uFF09",
|
|
1632
|
+
value: "latest",
|
|
1633
|
+
description: "\u907F\u514D\u5386\u53F2\u8BEF\u6253\u7684\u9AD8\u7248\u672C tag \u5E72\u6270\u540E\u7EED\u9012\u589E"
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
name: "\u5168\u91CF\u6392\u5E8F",
|
|
1637
|
+
value: "all",
|
|
1638
|
+
description: "\u5168\u91CF\u62C9\u53D6 tags\uFF0C\u5E76\u6309\u7248\u672C\u53F7\u6392\u5E8F\u540E\u53D6\u6700\u65B0\u503C"
|
|
1639
|
+
}
|
|
1640
|
+
],
|
|
1641
|
+
theme
|
|
1642
|
+
});
|
|
1643
|
+
config2.tagLookupStrategy = tagLookupStrategy;
|
|
1591
1644
|
const autoPushChoice = await select4({
|
|
1592
1645
|
message: "\u521B\u5EFA\u5206\u652F\u540E\u662F\u5426\u81EA\u52A8\u63A8\u9001?",
|
|
1593
1646
|
choices: [
|
|
@@ -4232,7 +4285,7 @@ process.on("SIGTERM", () => {
|
|
|
4232
4285
|
console.log("");
|
|
4233
4286
|
process.exit(0);
|
|
4234
4287
|
});
|
|
4235
|
-
var version = true ? "0.
|
|
4288
|
+
var version = true ? "0.6.0" : "0.0.0-dev";
|
|
4236
4289
|
async function mainMenu() {
|
|
4237
4290
|
console.log(
|
|
4238
4291
|
colors.green(`
|
package/docs/commands/config.md
CHANGED
|
@@ -89,6 +89,9 @@ $ gw init
|
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
91
|
? 默认 Tag 前缀 (留空则每次选择): v
|
|
92
|
+
? Tag 递增基准策略:
|
|
93
|
+
❯ 仅基于最新创建的 Tag(默认)
|
|
94
|
+
全量排序
|
|
92
95
|
```
|
|
93
96
|
|
|
94
97
|
### 推送配置
|
|
@@ -343,4 +346,4 @@ vim .gwrc.json
|
|
|
343
346
|
|
|
344
347
|
1. **定期更新** - 根据需求更新配置
|
|
345
348
|
2. **备份配置** - 备份重要的配置文件
|
|
346
|
-
3. **版本控制** - 跟踪配置文件的变更历史
|
|
349
|
+
3. **版本控制** - 跟踪配置文件的变更历史
|
|
@@ -42,6 +42,7 @@ gw.config.json # 明确的配置文件名
|
|
|
42
42
|
"featureIdLabel": "Story ID",
|
|
43
43
|
"hotfixIdLabel": "Issue ID",
|
|
44
44
|
"defaultTagPrefix": "v",
|
|
45
|
+
"tagLookupStrategy": "latest",
|
|
45
46
|
"autoPush": true,
|
|
46
47
|
"autoStage": true,
|
|
47
48
|
"useEmoji": true
|
|
@@ -59,6 +60,7 @@ gw.config.json # 明确的配置文件名
|
|
|
59
60
|
"featureIdLabel": "Story ID",
|
|
60
61
|
"hotfixIdLabel": "Issue ID",
|
|
61
62
|
"defaultTagPrefix": "v",
|
|
63
|
+
"tagLookupStrategy": "latest",
|
|
62
64
|
"autoPush": true,
|
|
63
65
|
"autoStage": true,
|
|
64
66
|
"useEmoji": true,
|
|
@@ -232,6 +234,22 @@ gw.config.json # 明确的配置文件名
|
|
|
232
234
|
- 创建 tag 时直接使用 `v` 前缀
|
|
233
235
|
- 跳过前缀选择界面
|
|
234
236
|
|
|
237
|
+
#### tagLookupStrategy
|
|
238
|
+
|
|
239
|
+
**类型:** `"all" | "latest"`
|
|
240
|
+
**默认值:** `"latest"`
|
|
241
|
+
**说明:** 创建 tag 时如何确定递增基准
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"tagLookupStrategy": "latest"
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**选项说明:**
|
|
250
|
+
- `"latest"` - 默认行为。优先基于本地最新创建的 tag 递增,避免历史误打的高版本 tag 干扰排序;如果当前前缀在本地不存在,会自动回退到一次全量同步
|
|
251
|
+
- `"all"` - 全量拉取 tags,并按版本号排序后取最新值
|
|
252
|
+
|
|
235
253
|
### 提交配置
|
|
236
254
|
|
|
237
255
|
#### autoStage
|
|
@@ -773,4 +791,4 @@ cp ../other-project/.gwrc.json .
|
|
|
773
791
|
|
|
774
792
|
---
|
|
775
793
|
|
|
776
|
-
通过合理配置,Git Workflow 可以完美适应你的工作流程。配置文件是工具的核心,掌握配置文件的使用是高效使用 Git Workflow 的关键。
|
|
794
|
+
通过合理配置,Git Workflow 可以完美适应你的工作流程。配置文件是工具的核心,掌握配置文件的使用是高效使用 Git Workflow 的关键。
|
package/docs/config/index.md
CHANGED
|
@@ -144,6 +144,7 @@ gw init
|
|
|
144
144
|
"featureIdLabel": "Jira ID",
|
|
145
145
|
"hotfixIdLabel": "Bug ID",
|
|
146
146
|
"defaultTagPrefix": "v",
|
|
147
|
+
"tagLookupStrategy": "latest",
|
|
147
148
|
"autoPush": true,
|
|
148
149
|
"autoStage": true,
|
|
149
150
|
"useEmoji": true,
|
|
@@ -186,6 +187,7 @@ gw init
|
|
|
186
187
|
| 配置项 | 类型 | 默认值 | 说明 |
|
|
187
188
|
| ------------------ | -------- | ------ | --------------------------------- |
|
|
188
189
|
| `defaultTagPrefix` | `string` | - | 默认 tag 前缀,设置后跳过选择步骤 |
|
|
190
|
+
| `tagLookupStrategy` | `"all" \| "latest"` | `"latest"` | tag 递增基准策略:`latest` 优先基于最新创建的 tag,`all` 按版本全量排序 |
|
|
189
191
|
|
|
190
192
|
### 提交配置
|
|
191
193
|
|
|
@@ -475,4 +477,4 @@ cp project-a/.gwrc.json project-b/.gwrc.json
|
|
|
475
477
|
|
|
476
478
|
---
|
|
477
479
|
|
|
478
|
-
通过合理的配置,Git Workflow 可以完美适应你的工作流程。从简单的个人项目到复杂的企业级应用,都能找到合适的配置方案。
|
|
480
|
+
通过合理的配置,Git Workflow 可以完美适应你的工作流程。从简单的个人项目到复杂的企业级应用,都能找到合适的配置方案。
|
|
@@ -436,6 +436,19 @@ jobs:
|
|
|
436
436
|
|
|
437
437
|
设置后,创建标签时会跳过前缀选择步骤。
|
|
438
438
|
|
|
439
|
+
### Tag 基准策略
|
|
440
|
+
|
|
441
|
+
```json
|
|
442
|
+
{
|
|
443
|
+
"defaultTagPrefix": "v",
|
|
444
|
+
"tagLookupStrategy": "latest"
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
`tagLookupStrategy` 支持两种模式:
|
|
449
|
+
- `all`:全量拉取并按版本号排序,兼容当前默认行为
|
|
450
|
+
- `latest`:优先基于最新创建的 tag 递增,适合历史上出现过误打高版本 tag 的仓库;如果本地没有该前缀的 tag,会自动回退一次全量同步
|
|
451
|
+
|
|
439
452
|
### 版本格式配置
|
|
440
453
|
|
|
441
454
|
```json
|
|
@@ -611,4 +624,4 @@ jobs:
|
|
|
611
624
|
|
|
612
625
|
---
|
|
613
626
|
|
|
614
|
-
通过系统化的 Tag 管理,你可以建立清晰的版本发布流程。Git Workflow 的智能版本递增和前缀检测功能,让版本管理变得简单而规范。
|
|
627
|
+
通过系统化的 Tag 管理,你可以建立清晰的版本发布流程。Git Workflow 的智能版本递增和前缀检测功能,让版本管理变得简单而规范。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zjex/git-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "🚀 极简的 Git 工作流 CLI 工具,让分支管理和版本发布变得轻松愉快",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"author": "zjex",
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
43
|
+
"node": ">=21.3.0",
|
|
44
44
|
"npm": ">=9.0.0"
|
|
45
45
|
},
|
|
46
46
|
"repository": {
|
package/src/commands/init.ts
CHANGED
|
@@ -148,6 +148,24 @@ export async function init(): Promise<void> {
|
|
|
148
148
|
});
|
|
149
149
|
if (defaultTagPrefix) config.defaultTagPrefix = defaultTagPrefix;
|
|
150
150
|
|
|
151
|
+
const tagLookupStrategy = await select({
|
|
152
|
+
message: "Tag 递增基准策略:",
|
|
153
|
+
choices: [
|
|
154
|
+
{
|
|
155
|
+
name: "仅基于最新创建的 Tag(默认)",
|
|
156
|
+
value: "latest",
|
|
157
|
+
description: "避免历史误打的高版本 tag 干扰后续递增",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "全量排序",
|
|
161
|
+
value: "all",
|
|
162
|
+
description: "全量拉取 tags,并按版本号排序后取最新值",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
theme,
|
|
166
|
+
});
|
|
167
|
+
config.tagLookupStrategy = tagLookupStrategy as "all" | "latest";
|
|
168
|
+
|
|
151
169
|
// 自动推送
|
|
152
170
|
const autoPushChoice = await select({
|
|
153
171
|
message: "创建分支后是否自动推送?",
|
package/src/commands/tag.ts
CHANGED
|
@@ -10,6 +10,14 @@ import {
|
|
|
10
10
|
divider,
|
|
11
11
|
} from "../utils.js";
|
|
12
12
|
import { getConfig } from "../config.js";
|
|
13
|
+
import {
|
|
14
|
+
extractTagPrefix,
|
|
15
|
+
getLatestTagCommand,
|
|
16
|
+
isValidVersionTag,
|
|
17
|
+
normalizeTagLookupStrategy,
|
|
18
|
+
shouldFetchAllTagsForCreateTag,
|
|
19
|
+
type TagLookupStrategy,
|
|
20
|
+
} from "../tag-utils.js";
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* 列出 tags(最新的显示在最下面,多个前缀分列展示)
|
|
@@ -29,7 +37,7 @@ export async function listTags(prefix?: string): Promise<void> {
|
|
|
29
37
|
|
|
30
38
|
// 3. 过滤无效 tag(如 vnull、vundefined 等误操作产生的 tag)
|
|
31
39
|
// 有效 tag 必须包含数字(版本号)
|
|
32
|
-
const tags = allTags.filter(
|
|
40
|
+
const tags = allTags.filter(isValidVersionTag);
|
|
33
41
|
|
|
34
42
|
// 4. 如果没有 tags,提示并返回
|
|
35
43
|
if (tags.length === 0) {
|
|
@@ -57,7 +65,7 @@ export async function listTags(prefix?: string): Promise<void> {
|
|
|
57
65
|
const grouped = new Map<string, string[]>();
|
|
58
66
|
tags.forEach((tag) => {
|
|
59
67
|
// 提取数字之前的字母部分作为前缀(如 "v0.1.0" -> "v")
|
|
60
|
-
const prefix = tag
|
|
68
|
+
const prefix = extractTagPrefix(tag) || "(无前缀)";
|
|
61
69
|
if (!grouped.has(prefix)) {
|
|
62
70
|
grouped.set(prefix, []);
|
|
63
71
|
}
|
|
@@ -147,18 +155,21 @@ interface TagChoice {
|
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
// 获取指定前缀的最新有效 tag(必须包含数字)
|
|
150
|
-
function getLatestTag(
|
|
151
|
-
|
|
158
|
+
function getLatestTag(
|
|
159
|
+
prefix: string,
|
|
160
|
+
strategy: TagLookupStrategy = "latest",
|
|
161
|
+
): string {
|
|
162
|
+
const tags = execOutput(getLatestTagCommand(prefix, strategy))
|
|
152
163
|
.split("\n")
|
|
153
|
-
.filter((tag) => tag &&
|
|
164
|
+
.filter((tag) => tag && isValidVersionTag(tag)); // 过滤无效 tag
|
|
154
165
|
return tags[0] || "";
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
export async function createTag(inputPrefix?: string): Promise<void> {
|
|
158
169
|
const config = getConfig();
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
const tagLookupStrategy = normalizeTagLookupStrategy(
|
|
171
|
+
config.tagLookupStrategy,
|
|
172
|
+
);
|
|
162
173
|
|
|
163
174
|
divider();
|
|
164
175
|
|
|
@@ -170,11 +181,17 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
170
181
|
console.log(colors.dim(`(使用配置的默认前缀: ${prefix})`));
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
if (shouldFetchAllTagsForCreateTag(tagLookupStrategy, prefix)) {
|
|
185
|
+
const fetchSpinner = ora("正在获取 tags...").start();
|
|
186
|
+
exec("git fetch --tags", true);
|
|
187
|
+
fetchSpinner.stop();
|
|
188
|
+
}
|
|
189
|
+
|
|
173
190
|
if (!prefix) {
|
|
174
191
|
// 过滤无效 tag(如 vnull、vundefined 等误操作产生的 tag)
|
|
175
192
|
const allTags = execOutput("git tag -l")
|
|
176
193
|
.split("\n")
|
|
177
|
-
.filter((tag) => tag &&
|
|
194
|
+
.filter((tag) => tag && isValidVersionTag(tag));
|
|
178
195
|
|
|
179
196
|
// 仓库没有任何 tag 的情况
|
|
180
197
|
if (allTags.length === 0) {
|
|
@@ -228,9 +245,7 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
228
245
|
}
|
|
229
246
|
|
|
230
247
|
// 从现有 tag 中提取前缀(数字之前的字母部分)
|
|
231
|
-
const prefixes = [
|
|
232
|
-
...new Set(allTags.map((t) => t.replace(/\d.*/, "")).filter(Boolean)),
|
|
233
|
-
];
|
|
248
|
+
const prefixes = [...new Set(allTags.map(extractTagPrefix).filter(Boolean))];
|
|
234
249
|
|
|
235
250
|
if (prefixes.length === 0) {
|
|
236
251
|
// 有 tag 但无法提取前缀(比如纯数字 tag)
|
|
@@ -245,7 +260,7 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
245
260
|
}
|
|
246
261
|
} else {
|
|
247
262
|
const prefixWithDate: PrefixInfo[] = prefixes.map((p) => {
|
|
248
|
-
const latest = getLatestTag(p);
|
|
263
|
+
const latest = getLatestTag(p, tagLookupStrategy);
|
|
249
264
|
const date = latest
|
|
250
265
|
? execOutput(`git log -1 --format=%ct "${latest}" 2>/dev/null`)
|
|
251
266
|
: "0";
|
|
@@ -276,7 +291,14 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
276
291
|
}
|
|
277
292
|
}
|
|
278
293
|
|
|
279
|
-
|
|
294
|
+
let latestTag = getLatestTag(prefix, tagLookupStrategy);
|
|
295
|
+
|
|
296
|
+
if (!latestTag && tagLookupStrategy === "latest") {
|
|
297
|
+
const fetchSpinner = ora("本地未找到对应 tag,正在全量同步一次...").start();
|
|
298
|
+
exec("git fetch --tags", true);
|
|
299
|
+
fetchSpinner.stop();
|
|
300
|
+
latestTag = getLatestTag(prefix, tagLookupStrategy);
|
|
301
|
+
}
|
|
280
302
|
|
|
281
303
|
if (!latestTag) {
|
|
282
304
|
const newTag = `${prefix}1.0.0`;
|
|
@@ -297,7 +319,9 @@ export async function createTag(inputPrefix?: string): Promise<void> {
|
|
|
297
319
|
return;
|
|
298
320
|
}
|
|
299
321
|
|
|
300
|
-
|
|
322
|
+
const strategyText =
|
|
323
|
+
tagLookupStrategy === "latest" ? "最新创建" : "版本排序";
|
|
324
|
+
console.log(colors.yellow(`当前基准 tag (${strategyText}): ${latestTag}`));
|
|
301
325
|
|
|
302
326
|
divider();
|
|
303
327
|
|
package/src/config.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface GwConfig {
|
|
|
21
21
|
hotfixIdLabel: string;
|
|
22
22
|
// 默认 tag 前缀
|
|
23
23
|
defaultTagPrefix?: string;
|
|
24
|
+
// tag 递增基准策略,all=全量按版本排序,latest=按最新创建的 tag 递增
|
|
25
|
+
tagLookupStrategy?: "all" | "latest";
|
|
24
26
|
// 创建分支后是否自动推送,默认询问
|
|
25
27
|
autoPush?: boolean;
|
|
26
28
|
// commit 时是否自动暂存所有更改,默认 true
|
|
@@ -60,6 +62,7 @@ const defaultConfig: GwConfig = {
|
|
|
60
62
|
requireId: false,
|
|
61
63
|
featureIdLabel: "Story ID",
|
|
62
64
|
hotfixIdLabel: "Issue ID",
|
|
65
|
+
tagLookupStrategy: "latest",
|
|
63
66
|
autoStage: true,
|
|
64
67
|
useEmoji: true,
|
|
65
68
|
};
|
package/src/tag-utils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type TagLookupStrategy = "all" | "latest";
|
|
2
|
+
|
|
3
|
+
export function isValidVersionTag(tag: string): boolean {
|
|
4
|
+
return /\d/.test(tag);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function extractTagPrefix(tag: string): string {
|
|
8
|
+
return tag.replace(/\d.*/, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeTagLookupStrategy(
|
|
12
|
+
value?: string,
|
|
13
|
+
): TagLookupStrategy {
|
|
14
|
+
return value === "all" ? "all" : "latest";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getLatestTagCommand(
|
|
18
|
+
prefix: string,
|
|
19
|
+
strategy: TagLookupStrategy,
|
|
20
|
+
): string {
|
|
21
|
+
if (strategy === "latest") {
|
|
22
|
+
return `git for-each-ref --sort=-creatordate --format="%(refname:short)" "refs/tags/${prefix}*"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return `git tag -l "${prefix}*" --sort=-v:refname`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function shouldFetchAllTagsForCreateTag(
|
|
29
|
+
strategy: TagLookupStrategy,
|
|
30
|
+
prefix?: string,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (strategy === "all") {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return !prefix;
|
|
37
|
+
}
|
package/tests/config.test.ts
CHANGED
|
@@ -35,6 +35,7 @@ describe("Config 模块测试", () => {
|
|
|
35
35
|
hotfixIdLabel: "Issue ID",
|
|
36
36
|
autoStage: true,
|
|
37
37
|
useEmoji: true,
|
|
38
|
+
tagLookupStrategy: "latest",
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
});
|
|
@@ -92,6 +93,23 @@ describe("Config 模块测试", () => {
|
|
|
92
93
|
|
|
93
94
|
expect(config.autoPush).toBe(true);
|
|
94
95
|
});
|
|
96
|
+
|
|
97
|
+
it("应该加载 tagLookupStrategy 配置", () => {
|
|
98
|
+
const mockConfig = {
|
|
99
|
+
tagLookupStrategy: "latest" as const,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
vi.mocked(existsSync).mockImplementation((path) => {
|
|
103
|
+
return path === ".gwrc.json";
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfig));
|
|
107
|
+
vi.mocked(execOutput).mockReturnValue("");
|
|
108
|
+
|
|
109
|
+
const config = loadConfig();
|
|
110
|
+
|
|
111
|
+
expect(config.tagLookupStrategy).toBe("latest");
|
|
112
|
+
});
|
|
95
113
|
});
|
|
96
114
|
|
|
97
115
|
describe("全局配置", () => {
|
package/tests/init.test.ts
CHANGED
|
@@ -40,6 +40,74 @@ describe("Init 模块测试", () => {
|
|
|
40
40
|
const mockJoin = vi.mocked(join);
|
|
41
41
|
const mockHomedir = vi.mocked(homedir);
|
|
42
42
|
|
|
43
|
+
async function mockBasicInitFlow(options?: {
|
|
44
|
+
scope?: "global" | "project";
|
|
45
|
+
exists?: boolean;
|
|
46
|
+
overwrite?: boolean;
|
|
47
|
+
requireId?: boolean;
|
|
48
|
+
featureRequireDescription?: boolean;
|
|
49
|
+
hotfixRequireDescription?: boolean;
|
|
50
|
+
tagLookupStrategy?: "all" | "latest";
|
|
51
|
+
autoPushChoice?: "ask" | "yes" | "no";
|
|
52
|
+
autoStage?: boolean;
|
|
53
|
+
useEmoji?: boolean;
|
|
54
|
+
enableAI?: boolean;
|
|
55
|
+
inputs?: {
|
|
56
|
+
baseBranch?: string;
|
|
57
|
+
featurePrefix?: string;
|
|
58
|
+
hotfixPrefix?: string;
|
|
59
|
+
featureIdLabel?: string;
|
|
60
|
+
hotfixIdLabel?: string;
|
|
61
|
+
defaultTagPrefix?: string;
|
|
62
|
+
};
|
|
63
|
+
}) {
|
|
64
|
+
const {
|
|
65
|
+
scope = "project",
|
|
66
|
+
exists = false,
|
|
67
|
+
overwrite,
|
|
68
|
+
requireId = false,
|
|
69
|
+
featureRequireDescription = false,
|
|
70
|
+
hotfixRequireDescription = false,
|
|
71
|
+
tagLookupStrategy = "latest",
|
|
72
|
+
autoPushChoice = "ask",
|
|
73
|
+
autoStage = true,
|
|
74
|
+
useEmoji = true,
|
|
75
|
+
enableAI = false,
|
|
76
|
+
inputs = {},
|
|
77
|
+
} = options ?? {};
|
|
78
|
+
|
|
79
|
+
mockExistsSync.mockReturnValue(exists);
|
|
80
|
+
|
|
81
|
+
const { select, input } = await import("@inquirer/prompts");
|
|
82
|
+
const selectMock = vi.mocked(select);
|
|
83
|
+
const inputMock = vi.mocked(input);
|
|
84
|
+
|
|
85
|
+
selectMock.mockResolvedValueOnce(scope);
|
|
86
|
+
if (exists) {
|
|
87
|
+
selectMock.mockResolvedValueOnce(overwrite ?? true);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
selectMock
|
|
91
|
+
.mockResolvedValueOnce(requireId)
|
|
92
|
+
.mockResolvedValueOnce(featureRequireDescription)
|
|
93
|
+
.mockResolvedValueOnce(hotfixRequireDescription)
|
|
94
|
+
.mockResolvedValueOnce(tagLookupStrategy)
|
|
95
|
+
.mockResolvedValueOnce(autoPushChoice)
|
|
96
|
+
.mockResolvedValueOnce(autoStage)
|
|
97
|
+
.mockResolvedValueOnce(useEmoji)
|
|
98
|
+
.mockResolvedValueOnce(enableAI);
|
|
99
|
+
|
|
100
|
+
inputMock
|
|
101
|
+
.mockResolvedValueOnce(inputs.baseBranch ?? "")
|
|
102
|
+
.mockResolvedValueOnce(inputs.featurePrefix ?? "feature")
|
|
103
|
+
.mockResolvedValueOnce(inputs.hotfixPrefix ?? "hotfix")
|
|
104
|
+
.mockResolvedValueOnce(inputs.featureIdLabel ?? "Story ID")
|
|
105
|
+
.mockResolvedValueOnce(inputs.hotfixIdLabel ?? "Issue ID")
|
|
106
|
+
.mockResolvedValueOnce(inputs.defaultTagPrefix ?? "");
|
|
107
|
+
|
|
108
|
+
return { select: selectMock, input: inputMock };
|
|
109
|
+
}
|
|
110
|
+
|
|
43
111
|
beforeEach(() => {
|
|
44
112
|
vi.clearAllMocks();
|
|
45
113
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
@@ -53,24 +121,7 @@ describe("Init 模块测试", () => {
|
|
|
53
121
|
|
|
54
122
|
describe("配置范围选择", () => {
|
|
55
123
|
it("应该支持全局配置", async () => {
|
|
56
|
-
|
|
57
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
58
|
-
vi.mocked(select)
|
|
59
|
-
.mockResolvedValueOnce("global")
|
|
60
|
-
.mockResolvedValueOnce(false)
|
|
61
|
-
.mockResolvedValueOnce(false)
|
|
62
|
-
.mockResolvedValueOnce(false)
|
|
63
|
-
.mockResolvedValueOnce("ask")
|
|
64
|
-
.mockResolvedValueOnce(true)
|
|
65
|
-
.mockResolvedValueOnce(true)
|
|
66
|
-
.mockResolvedValueOnce(false);
|
|
67
|
-
vi.mocked(input)
|
|
68
|
-
.mockResolvedValueOnce("")
|
|
69
|
-
.mockResolvedValueOnce("feature")
|
|
70
|
-
.mockResolvedValueOnce("hotfix")
|
|
71
|
-
.mockResolvedValueOnce("Story ID")
|
|
72
|
-
.mockResolvedValueOnce("Issue ID")
|
|
73
|
-
.mockResolvedValueOnce("");
|
|
124
|
+
await mockBasicInitFlow({ scope: "global" });
|
|
74
125
|
const { init } = await import("../src/commands/init.js");
|
|
75
126
|
await init();
|
|
76
127
|
expect(mockJoin).toHaveBeenCalledWith("/home/user", ".gwrc.json");
|
|
@@ -81,24 +132,7 @@ describe("Init 模块测试", () => {
|
|
|
81
132
|
});
|
|
82
133
|
|
|
83
134
|
it("应该支持项目配置", async () => {
|
|
84
|
-
|
|
85
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
86
|
-
vi.mocked(select)
|
|
87
|
-
.mockResolvedValueOnce("project")
|
|
88
|
-
.mockResolvedValueOnce(false)
|
|
89
|
-
.mockResolvedValueOnce(false)
|
|
90
|
-
.mockResolvedValueOnce(false)
|
|
91
|
-
.mockResolvedValueOnce("ask")
|
|
92
|
-
.mockResolvedValueOnce(true)
|
|
93
|
-
.mockResolvedValueOnce(true)
|
|
94
|
-
.mockResolvedValueOnce(false);
|
|
95
|
-
vi.mocked(input)
|
|
96
|
-
.mockResolvedValueOnce("")
|
|
97
|
-
.mockResolvedValueOnce("feature")
|
|
98
|
-
.mockResolvedValueOnce("hotfix")
|
|
99
|
-
.mockResolvedValueOnce("Story ID")
|
|
100
|
-
.mockResolvedValueOnce("Issue ID")
|
|
101
|
-
.mockResolvedValueOnce("");
|
|
135
|
+
await mockBasicInitFlow({ scope: "project" });
|
|
102
136
|
const { init } = await import("../src/commands/init.js");
|
|
103
137
|
await init();
|
|
104
138
|
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
@@ -110,25 +144,7 @@ describe("Init 模块测试", () => {
|
|
|
110
144
|
|
|
111
145
|
describe("配置文件覆盖", () => {
|
|
112
146
|
it("应该处理配置文件已存在的情况", async () => {
|
|
113
|
-
|
|
114
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
115
|
-
vi.mocked(select)
|
|
116
|
-
.mockResolvedValueOnce("global")
|
|
117
|
-
.mockResolvedValueOnce(true)
|
|
118
|
-
.mockResolvedValueOnce(false)
|
|
119
|
-
.mockResolvedValueOnce(false)
|
|
120
|
-
.mockResolvedValueOnce(false)
|
|
121
|
-
.mockResolvedValueOnce("ask")
|
|
122
|
-
.mockResolvedValueOnce(true)
|
|
123
|
-
.mockResolvedValueOnce(true)
|
|
124
|
-
.mockResolvedValueOnce(false);
|
|
125
|
-
vi.mocked(input)
|
|
126
|
-
.mockResolvedValueOnce("")
|
|
127
|
-
.mockResolvedValueOnce("feature")
|
|
128
|
-
.mockResolvedValueOnce("hotfix")
|
|
129
|
-
.mockResolvedValueOnce("Story ID")
|
|
130
|
-
.mockResolvedValueOnce("Issue ID")
|
|
131
|
-
.mockResolvedValueOnce("");
|
|
147
|
+
await mockBasicInitFlow({ scope: "global", exists: true, overwrite: true });
|
|
132
148
|
const { init } = await import("../src/commands/init.js");
|
|
133
149
|
await init();
|
|
134
150
|
expect(mockWriteFileSync).toHaveBeenCalled();
|
|
@@ -149,24 +165,16 @@ describe("Init 模块测试", () => {
|
|
|
149
165
|
|
|
150
166
|
describe("基础配置", () => {
|
|
151
167
|
it("应该正确配置分支前缀", async () => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.mockResolvedValueOnce(false);
|
|
163
|
-
vi.mocked(input)
|
|
164
|
-
.mockResolvedValueOnce("develop")
|
|
165
|
-
.mockResolvedValueOnce("feat")
|
|
166
|
-
.mockResolvedValueOnce("fix")
|
|
167
|
-
.mockResolvedValueOnce("Jira ID")
|
|
168
|
-
.mockResolvedValueOnce("Bug ID")
|
|
169
|
-
.mockResolvedValueOnce("v");
|
|
168
|
+
await mockBasicInitFlow({
|
|
169
|
+
inputs: {
|
|
170
|
+
baseBranch: "develop",
|
|
171
|
+
featurePrefix: "feat",
|
|
172
|
+
hotfixPrefix: "fix",
|
|
173
|
+
featureIdLabel: "Jira ID",
|
|
174
|
+
hotfixIdLabel: "Bug ID",
|
|
175
|
+
defaultTagPrefix: "v",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
170
178
|
const { init } = await import("../src/commands/init.js");
|
|
171
179
|
await init();
|
|
172
180
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -177,27 +185,11 @@ describe("Init 模块测试", () => {
|
|
|
177
185
|
expect(config.featureIdLabel).toBe("Jira ID");
|
|
178
186
|
expect(config.hotfixIdLabel).toBe("Bug ID");
|
|
179
187
|
expect(config.defaultTagPrefix).toBe("v");
|
|
188
|
+
expect(config.tagLookupStrategy).toBe("latest");
|
|
180
189
|
});
|
|
181
190
|
|
|
182
191
|
it("应该正确配置 ID 要求", async () => {
|
|
183
|
-
|
|
184
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
185
|
-
vi.mocked(select)
|
|
186
|
-
.mockResolvedValueOnce("project")
|
|
187
|
-
.mockResolvedValueOnce(true)
|
|
188
|
-
.mockResolvedValueOnce(false)
|
|
189
|
-
.mockResolvedValueOnce(false)
|
|
190
|
-
.mockResolvedValueOnce("ask")
|
|
191
|
-
.mockResolvedValueOnce(true)
|
|
192
|
-
.mockResolvedValueOnce(true)
|
|
193
|
-
.mockResolvedValueOnce(false);
|
|
194
|
-
vi.mocked(input)
|
|
195
|
-
.mockResolvedValueOnce("")
|
|
196
|
-
.mockResolvedValueOnce("feature")
|
|
197
|
-
.mockResolvedValueOnce("hotfix")
|
|
198
|
-
.mockResolvedValueOnce("Story ID")
|
|
199
|
-
.mockResolvedValueOnce("Issue ID")
|
|
200
|
-
.mockResolvedValueOnce("");
|
|
192
|
+
await mockBasicInitFlow({ requireId: true });
|
|
201
193
|
const { init } = await import("../src/commands/init.js");
|
|
202
194
|
await init();
|
|
203
195
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -206,57 +198,33 @@ describe("Init 模块测试", () => {
|
|
|
206
198
|
});
|
|
207
199
|
|
|
208
200
|
it("应该正确配置自动推送选项", async () => {
|
|
209
|
-
|
|
210
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
211
|
-
vi.mocked(select)
|
|
212
|
-
.mockResolvedValueOnce("project")
|
|
213
|
-
.mockResolvedValueOnce(false)
|
|
214
|
-
.mockResolvedValueOnce(false)
|
|
215
|
-
.mockResolvedValueOnce(false)
|
|
216
|
-
.mockResolvedValueOnce("yes")
|
|
217
|
-
.mockResolvedValueOnce(true)
|
|
218
|
-
.mockResolvedValueOnce(true)
|
|
219
|
-
.mockResolvedValueOnce(false);
|
|
220
|
-
vi.mocked(input)
|
|
221
|
-
.mockResolvedValueOnce("")
|
|
222
|
-
.mockResolvedValueOnce("feature")
|
|
223
|
-
.mockResolvedValueOnce("hotfix")
|
|
224
|
-
.mockResolvedValueOnce("Story ID")
|
|
225
|
-
.mockResolvedValueOnce("Issue ID")
|
|
226
|
-
.mockResolvedValueOnce("");
|
|
201
|
+
await mockBasicInitFlow({ autoPushChoice: "yes" });
|
|
227
202
|
const { init } = await import("../src/commands/init.js");
|
|
228
203
|
await init();
|
|
229
204
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
230
205
|
const config = JSON.parse(writtenConfig);
|
|
231
206
|
expect(config.autoPush).toBe(true);
|
|
232
207
|
});
|
|
208
|
+
|
|
209
|
+
it("应该支持配置 tagLookupStrategy 为 latest", async () => {
|
|
210
|
+
await mockBasicInitFlow({ tagLookupStrategy: "latest" });
|
|
211
|
+
const { init } = await import("../src/commands/init.js");
|
|
212
|
+
await init();
|
|
213
|
+
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
214
|
+
const config = JSON.parse(writtenConfig);
|
|
215
|
+
expect(config.tagLookupStrategy).toBe("latest");
|
|
216
|
+
});
|
|
233
217
|
});
|
|
234
218
|
|
|
235
219
|
describe("AI 配置", () => {
|
|
236
220
|
it("应该正确配置 GitHub Models", async () => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
vi.mocked(select)
|
|
240
|
-
.mockResolvedValueOnce("project")
|
|
241
|
-
.mockResolvedValueOnce(false)
|
|
242
|
-
.mockResolvedValueOnce(false)
|
|
243
|
-
.mockResolvedValueOnce(false)
|
|
244
|
-
.mockResolvedValueOnce("ask")
|
|
245
|
-
.mockResolvedValueOnce(true)
|
|
246
|
-
.mockResolvedValueOnce(true)
|
|
247
|
-
.mockResolvedValueOnce(true)
|
|
221
|
+
const { select, input } = await mockBasicInitFlow({ enableAI: true });
|
|
222
|
+
select
|
|
248
223
|
.mockResolvedValueOnce("github")
|
|
249
224
|
.mockResolvedValueOnce("zh-CN")
|
|
250
225
|
.mockResolvedValueOnce(true)
|
|
251
226
|
.mockResolvedValueOnce(true);
|
|
252
|
-
|
|
253
|
-
.mockResolvedValueOnce("")
|
|
254
|
-
.mockResolvedValueOnce("feature")
|
|
255
|
-
.mockResolvedValueOnce("hotfix")
|
|
256
|
-
.mockResolvedValueOnce("Story ID")
|
|
257
|
-
.mockResolvedValueOnce("Issue ID")
|
|
258
|
-
.mockResolvedValueOnce("")
|
|
259
|
-
.mockResolvedValueOnce("ghp_test_token");
|
|
227
|
+
input.mockResolvedValueOnce("ghp_test_token");
|
|
260
228
|
const { init } = await import("../src/commands/init.js");
|
|
261
229
|
await init();
|
|
262
230
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -269,29 +237,13 @@ describe("Init 模块测试", () => {
|
|
|
269
237
|
});
|
|
270
238
|
|
|
271
239
|
it("应该正确配置 OpenAI", async () => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
vi.mocked(select)
|
|
275
|
-
.mockResolvedValueOnce("project")
|
|
276
|
-
.mockResolvedValueOnce(false)
|
|
277
|
-
.mockResolvedValueOnce(false)
|
|
278
|
-
.mockResolvedValueOnce(false)
|
|
279
|
-
.mockResolvedValueOnce("ask")
|
|
280
|
-
.mockResolvedValueOnce(true)
|
|
281
|
-
.mockResolvedValueOnce(true)
|
|
282
|
-
.mockResolvedValueOnce(true)
|
|
240
|
+
const { select, input } = await mockBasicInitFlow({ enableAI: true });
|
|
241
|
+
select
|
|
283
242
|
.mockResolvedValueOnce("openai")
|
|
284
243
|
.mockResolvedValueOnce("en-US")
|
|
285
244
|
.mockResolvedValueOnce(true)
|
|
286
245
|
.mockResolvedValueOnce(true);
|
|
287
|
-
|
|
288
|
-
.mockResolvedValueOnce("")
|
|
289
|
-
.mockResolvedValueOnce("feature")
|
|
290
|
-
.mockResolvedValueOnce("hotfix")
|
|
291
|
-
.mockResolvedValueOnce("Story ID")
|
|
292
|
-
.mockResolvedValueOnce("Issue ID")
|
|
293
|
-
.mockResolvedValueOnce("")
|
|
294
|
-
.mockResolvedValueOnce("sk-test-key");
|
|
246
|
+
input.mockResolvedValueOnce("sk-test-key");
|
|
295
247
|
const { init } = await import("../src/commands/init.js");
|
|
296
248
|
await init();
|
|
297
249
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -304,28 +256,12 @@ describe("Init 模块测试", () => {
|
|
|
304
256
|
});
|
|
305
257
|
|
|
306
258
|
it("应该正确配置 Ollama", async () => {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
vi.mocked(select)
|
|
310
|
-
.mockResolvedValueOnce("project")
|
|
311
|
-
.mockResolvedValueOnce(false)
|
|
312
|
-
.mockResolvedValueOnce(false)
|
|
313
|
-
.mockResolvedValueOnce(false)
|
|
314
|
-
.mockResolvedValueOnce("ask")
|
|
315
|
-
.mockResolvedValueOnce(true)
|
|
316
|
-
.mockResolvedValueOnce(true)
|
|
317
|
-
.mockResolvedValueOnce(true)
|
|
259
|
+
const { select } = await mockBasicInitFlow({ enableAI: true });
|
|
260
|
+
select
|
|
318
261
|
.mockResolvedValueOnce("ollama")
|
|
319
262
|
.mockResolvedValueOnce("zh-CN")
|
|
320
263
|
.mockResolvedValueOnce(true)
|
|
321
264
|
.mockResolvedValueOnce(true);
|
|
322
|
-
vi.mocked(input)
|
|
323
|
-
.mockResolvedValueOnce("")
|
|
324
|
-
.mockResolvedValueOnce("feature")
|
|
325
|
-
.mockResolvedValueOnce("hotfix")
|
|
326
|
-
.mockResolvedValueOnce("Story ID")
|
|
327
|
-
.mockResolvedValueOnce("Issue ID")
|
|
328
|
-
.mockResolvedValueOnce("");
|
|
329
265
|
const { init } = await import("../src/commands/init.js");
|
|
330
266
|
await init();
|
|
331
267
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -338,24 +274,7 @@ describe("Init 模块测试", () => {
|
|
|
338
274
|
});
|
|
339
275
|
|
|
340
276
|
it("应该正确配置禁用 AI", async () => {
|
|
341
|
-
|
|
342
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
343
|
-
vi.mocked(select)
|
|
344
|
-
.mockResolvedValueOnce("project")
|
|
345
|
-
.mockResolvedValueOnce(false)
|
|
346
|
-
.mockResolvedValueOnce(false)
|
|
347
|
-
.mockResolvedValueOnce(false)
|
|
348
|
-
.mockResolvedValueOnce("ask")
|
|
349
|
-
.mockResolvedValueOnce(true)
|
|
350
|
-
.mockResolvedValueOnce(true)
|
|
351
|
-
.mockResolvedValueOnce(false);
|
|
352
|
-
vi.mocked(input)
|
|
353
|
-
.mockResolvedValueOnce("")
|
|
354
|
-
.mockResolvedValueOnce("feature")
|
|
355
|
-
.mockResolvedValueOnce("hotfix")
|
|
356
|
-
.mockResolvedValueOnce("Story ID")
|
|
357
|
-
.mockResolvedValueOnce("Issue ID")
|
|
358
|
-
.mockResolvedValueOnce("");
|
|
277
|
+
await mockBasicInitFlow({ enableAI: false });
|
|
359
278
|
const { init } = await import("../src/commands/init.js");
|
|
360
279
|
await init();
|
|
361
280
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -366,29 +285,13 @@ describe("Init 模块测试", () => {
|
|
|
366
285
|
|
|
367
286
|
describe("配置验证", () => {
|
|
368
287
|
it("应该验证 GitHub Token 不为空", async () => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
vi.mocked(select)
|
|
372
|
-
.mockResolvedValueOnce("project")
|
|
373
|
-
.mockResolvedValueOnce(false)
|
|
374
|
-
.mockResolvedValueOnce(false)
|
|
375
|
-
.mockResolvedValueOnce(false)
|
|
376
|
-
.mockResolvedValueOnce("ask")
|
|
377
|
-
.mockResolvedValueOnce(true)
|
|
378
|
-
.mockResolvedValueOnce(true)
|
|
379
|
-
.mockResolvedValueOnce(true)
|
|
288
|
+
const { select, input } = await mockBasicInitFlow({ enableAI: true });
|
|
289
|
+
select
|
|
380
290
|
.mockResolvedValueOnce("github")
|
|
381
291
|
.mockResolvedValueOnce("zh-CN")
|
|
382
292
|
.mockResolvedValueOnce(true)
|
|
383
293
|
.mockResolvedValueOnce(true);
|
|
384
|
-
|
|
385
|
-
.mockResolvedValueOnce("")
|
|
386
|
-
.mockResolvedValueOnce("feature")
|
|
387
|
-
.mockResolvedValueOnce("hotfix")
|
|
388
|
-
.mockResolvedValueOnce("Story ID")
|
|
389
|
-
.mockResolvedValueOnce("Issue ID")
|
|
390
|
-
.mockResolvedValueOnce("")
|
|
391
|
-
.mockResolvedValueOnce("ghp_valid_token");
|
|
294
|
+
input.mockResolvedValueOnce("ghp_valid_token");
|
|
392
295
|
const { init } = await import("../src/commands/init.js");
|
|
393
296
|
await init();
|
|
394
297
|
const inputCalls = vi.mocked(input).mock.calls;
|
|
@@ -403,29 +306,13 @@ describe("Init 模块测试", () => {
|
|
|
403
306
|
});
|
|
404
307
|
|
|
405
308
|
it("应该验证 OpenAI API Key 不为空", async () => {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
vi.mocked(select)
|
|
409
|
-
.mockResolvedValueOnce("project")
|
|
410
|
-
.mockResolvedValueOnce(false)
|
|
411
|
-
.mockResolvedValueOnce(false)
|
|
412
|
-
.mockResolvedValueOnce(false)
|
|
413
|
-
.mockResolvedValueOnce("ask")
|
|
414
|
-
.mockResolvedValueOnce(true)
|
|
415
|
-
.mockResolvedValueOnce(true)
|
|
416
|
-
.mockResolvedValueOnce(true)
|
|
309
|
+
const { select, input } = await mockBasicInitFlow({ enableAI: true });
|
|
310
|
+
select
|
|
417
311
|
.mockResolvedValueOnce("openai")
|
|
418
312
|
.mockResolvedValueOnce("en-US")
|
|
419
313
|
.mockResolvedValueOnce(true)
|
|
420
314
|
.mockResolvedValueOnce(true);
|
|
421
|
-
|
|
422
|
-
.mockResolvedValueOnce("")
|
|
423
|
-
.mockResolvedValueOnce("feature")
|
|
424
|
-
.mockResolvedValueOnce("hotfix")
|
|
425
|
-
.mockResolvedValueOnce("Story ID")
|
|
426
|
-
.mockResolvedValueOnce("Issue ID")
|
|
427
|
-
.mockResolvedValueOnce("")
|
|
428
|
-
.mockResolvedValueOnce("sk-valid-key");
|
|
315
|
+
input.mockResolvedValueOnce("sk-valid-key");
|
|
429
316
|
const { init } = await import("../src/commands/init.js");
|
|
430
317
|
await init();
|
|
431
318
|
const inputCalls = vi.mocked(input).mock.calls;
|
|
@@ -442,24 +329,7 @@ describe("Init 模块测试", () => {
|
|
|
442
329
|
|
|
443
330
|
describe("配置输出", () => {
|
|
444
331
|
it("应该包含默认的 commit emojis", async () => {
|
|
445
|
-
|
|
446
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
447
|
-
vi.mocked(select)
|
|
448
|
-
.mockResolvedValueOnce("project")
|
|
449
|
-
.mockResolvedValueOnce(false)
|
|
450
|
-
.mockResolvedValueOnce(false)
|
|
451
|
-
.mockResolvedValueOnce(false)
|
|
452
|
-
.mockResolvedValueOnce("ask")
|
|
453
|
-
.mockResolvedValueOnce(true)
|
|
454
|
-
.mockResolvedValueOnce(true)
|
|
455
|
-
.mockResolvedValueOnce(false);
|
|
456
|
-
vi.mocked(input)
|
|
457
|
-
.mockResolvedValueOnce("")
|
|
458
|
-
.mockResolvedValueOnce("feature")
|
|
459
|
-
.mockResolvedValueOnce("hotfix")
|
|
460
|
-
.mockResolvedValueOnce("Story ID")
|
|
461
|
-
.mockResolvedValueOnce("Issue ID")
|
|
462
|
-
.mockResolvedValueOnce("");
|
|
332
|
+
await mockBasicInitFlow();
|
|
463
333
|
const { init } = await import("../src/commands/init.js");
|
|
464
334
|
await init();
|
|
465
335
|
const writtenConfig = mockWriteFileSync.mock.calls[0][1] as string;
|
|
@@ -471,24 +341,7 @@ describe("Init 模块测试", () => {
|
|
|
471
341
|
});
|
|
472
342
|
|
|
473
343
|
it("应该显示成功消息", async () => {
|
|
474
|
-
|
|
475
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
476
|
-
vi.mocked(select)
|
|
477
|
-
.mockResolvedValueOnce("global")
|
|
478
|
-
.mockResolvedValueOnce(false)
|
|
479
|
-
.mockResolvedValueOnce(false)
|
|
480
|
-
.mockResolvedValueOnce(false)
|
|
481
|
-
.mockResolvedValueOnce("ask")
|
|
482
|
-
.mockResolvedValueOnce(true)
|
|
483
|
-
.mockResolvedValueOnce(true)
|
|
484
|
-
.mockResolvedValueOnce(false);
|
|
485
|
-
vi.mocked(input)
|
|
486
|
-
.mockResolvedValueOnce("")
|
|
487
|
-
.mockResolvedValueOnce("feature")
|
|
488
|
-
.mockResolvedValueOnce("hotfix")
|
|
489
|
-
.mockResolvedValueOnce("Story ID")
|
|
490
|
-
.mockResolvedValueOnce("Issue ID")
|
|
491
|
-
.mockResolvedValueOnce("");
|
|
344
|
+
await mockBasicInitFlow({ scope: "global" });
|
|
492
345
|
const { init } = await import("../src/commands/init.js");
|
|
493
346
|
await init();
|
|
494
347
|
expect(console.log).toHaveBeenCalledWith(
|
|
@@ -497,24 +350,7 @@ describe("Init 模块测试", () => {
|
|
|
497
350
|
});
|
|
498
351
|
|
|
499
352
|
it("应该显示全局配置的提示信息", async () => {
|
|
500
|
-
|
|
501
|
-
const { select, input } = await import("@inquirer/prompts");
|
|
502
|
-
vi.mocked(select)
|
|
503
|
-
.mockResolvedValueOnce("global")
|
|
504
|
-
.mockResolvedValueOnce(false)
|
|
505
|
-
.mockResolvedValueOnce(false)
|
|
506
|
-
.mockResolvedValueOnce(false)
|
|
507
|
-
.mockResolvedValueOnce("ask")
|
|
508
|
-
.mockResolvedValueOnce(true)
|
|
509
|
-
.mockResolvedValueOnce(true)
|
|
510
|
-
.mockResolvedValueOnce(false);
|
|
511
|
-
vi.mocked(input)
|
|
512
|
-
.mockResolvedValueOnce("")
|
|
513
|
-
.mockResolvedValueOnce("feature")
|
|
514
|
-
.mockResolvedValueOnce("hotfix")
|
|
515
|
-
.mockResolvedValueOnce("Story ID")
|
|
516
|
-
.mockResolvedValueOnce("Issue ID")
|
|
517
|
-
.mockResolvedValueOnce("");
|
|
353
|
+
await mockBasicInitFlow({ scope: "global" });
|
|
518
354
|
const { init } = await import("../src/commands/init.js");
|
|
519
355
|
await init();
|
|
520
356
|
expect(console.log).toHaveBeenCalledWith(
|
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);
|