@zjex/git-workflow 0.4.5 → 0.4.7
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 +135 -364
- package/README.md +1 -1
- package/dist/index.js +129 -30
- package/docs/.vitepress/config.ts +2 -0
- package/docs/guide/command-quotes-handling.md +279 -0
- package/docs/guide/debug-mode.md +384 -0
- package/package.json +1 -1
- package/scripts/generate-changelog-manual.js +15 -64
- package/src/commands/tag.ts +42 -10
- package/src/commands/update.ts +25 -9
- package/src/index.ts +11 -3
- package/src/utils.ts +93 -10
- package/tests/command-with-quotes.test.ts +378 -0
- package/tests/debug-mode.test.ts +503 -0
- package/tests/update.test.ts +85 -69
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-474%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
|
@@ -11,6 +11,9 @@ var __export = (target, all) => {
|
|
|
11
11
|
|
|
12
12
|
// src/utils.ts
|
|
13
13
|
import { execSync, spawn } from "child_process";
|
|
14
|
+
function setDebugMode(enabled) {
|
|
15
|
+
debugMode = enabled;
|
|
16
|
+
}
|
|
14
17
|
function exec(cmd, silent = false) {
|
|
15
18
|
try {
|
|
16
19
|
const options = {
|
|
@@ -53,21 +56,58 @@ function divider() {
|
|
|
53
56
|
}
|
|
54
57
|
function execAsync(command, spinner) {
|
|
55
58
|
return new Promise((resolve) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
if (debugMode) {
|
|
60
|
+
console.log(colors.dim(`
|
|
61
|
+
[DEBUG] \u6267\u884C\u547D\u4EE4: ${colors.cyan(command)}`));
|
|
62
|
+
}
|
|
63
|
+
const process2 = spawn(command, {
|
|
64
|
+
stdio: spinner ? "pipe" : "inherit",
|
|
65
|
+
shell: true
|
|
59
66
|
});
|
|
67
|
+
let errorOutput = "";
|
|
68
|
+
let stdoutOutput = "";
|
|
69
|
+
if (debugMode && process2.stdout) {
|
|
70
|
+
process2.stdout.on("data", (data) => {
|
|
71
|
+
stdoutOutput += data.toString();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (process2.stderr) {
|
|
75
|
+
process2.stderr.on("data", (data) => {
|
|
76
|
+
errorOutput += data.toString();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
60
79
|
process2.on("close", (code) => {
|
|
61
|
-
|
|
80
|
+
if (debugMode) {
|
|
81
|
+
console.log(colors.dim(`[DEBUG] \u9000\u51FA\u7801: ${code}`));
|
|
82
|
+
if (stdoutOutput) {
|
|
83
|
+
console.log(colors.dim(`[DEBUG] \u6807\u51C6\u8F93\u51FA:
|
|
84
|
+
${stdoutOutput}`));
|
|
85
|
+
}
|
|
86
|
+
if (errorOutput) {
|
|
87
|
+
const label = code === 0 ? "\u8F93\u51FA\u4FE1\u606F" : "\u9519\u8BEF\u8F93\u51FA";
|
|
88
|
+
console.log(colors.dim(`[DEBUG] ${label}:
|
|
89
|
+
${errorOutput}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (code === 0) {
|
|
93
|
+
resolve({ success: true });
|
|
94
|
+
} else {
|
|
95
|
+
resolve({ success: false, error: errorOutput.trim() });
|
|
96
|
+
}
|
|
62
97
|
});
|
|
63
|
-
process2.on("error", () => {
|
|
64
|
-
|
|
98
|
+
process2.on("error", (err) => {
|
|
99
|
+
if (debugMode) {
|
|
100
|
+
console.log(colors.dim(`[DEBUG] \u8FDB\u7A0B\u9519\u8BEF: ${err.message}`));
|
|
101
|
+
console.log(colors.dim(`[DEBUG] \u9519\u8BEF\u5806\u6808:
|
|
102
|
+
${err.stack}`));
|
|
103
|
+
}
|
|
104
|
+
resolve({ success: false, error: err.message });
|
|
65
105
|
});
|
|
66
106
|
});
|
|
67
107
|
}
|
|
68
108
|
async function execWithSpinner(command, spinner, successMessage, errorMessage) {
|
|
69
|
-
const
|
|
70
|
-
if (success) {
|
|
109
|
+
const result = await execAsync(command, spinner);
|
|
110
|
+
if (result.success) {
|
|
71
111
|
if (successMessage) {
|
|
72
112
|
spinner.succeed(successMessage);
|
|
73
113
|
} else {
|
|
@@ -79,13 +119,27 @@ async function execWithSpinner(command, spinner, successMessage, errorMessage) {
|
|
|
79
119
|
} else {
|
|
80
120
|
spinner.fail();
|
|
81
121
|
}
|
|
122
|
+
if (result.error) {
|
|
123
|
+
console.log(colors.dim(` ${result.error}`));
|
|
124
|
+
}
|
|
125
|
+
if (debugMode) {
|
|
126
|
+
console.log(colors.yellow("\n[DEBUG] \u6545\u969C\u6392\u67E5\u4FE1\u606F:"));
|
|
127
|
+
console.log(colors.dim(` \u547D\u4EE4: ${command}`));
|
|
128
|
+
console.log(colors.dim(` \u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}`));
|
|
129
|
+
console.log(colors.dim(` Shell: ${process.env.SHELL || "unknown"}`));
|
|
130
|
+
console.log(
|
|
131
|
+
colors.dim(` \u5EFA\u8BAE: \u5C1D\u8BD5\u5728\u7EC8\u7AEF\u4E2D\u76F4\u63A5\u8FD0\u884C\u4E0A\u8FF0\u547D\u4EE4\u4EE5\u83B7\u53D6\u66F4\u591A\u4FE1\u606F
|
|
132
|
+
`)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
82
135
|
}
|
|
83
|
-
return success;
|
|
136
|
+
return result.success;
|
|
84
137
|
}
|
|
85
|
-
var colors, TODAY, theme;
|
|
138
|
+
var debugMode, colors, TODAY, theme;
|
|
86
139
|
var init_utils = __esm({
|
|
87
140
|
"src/utils.ts"() {
|
|
88
141
|
"use strict";
|
|
142
|
+
debugMode = false;
|
|
89
143
|
colors = {
|
|
90
144
|
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
91
145
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -1006,6 +1060,25 @@ async function createTag(inputPrefix) {
|
|
|
1006
1060
|
}
|
|
1007
1061
|
async function doCreateTag(tagName) {
|
|
1008
1062
|
divider();
|
|
1063
|
+
const hasCommits = execOutput("git rev-parse HEAD 2>/dev/null");
|
|
1064
|
+
if (!hasCommits) {
|
|
1065
|
+
console.log(colors.red("\u5F53\u524D\u4ED3\u5E93\u6CA1\u6709\u4EFB\u4F55\u63D0\u4EA4"));
|
|
1066
|
+
console.log("");
|
|
1067
|
+
console.log(colors.dim(" \u63D0\u793A: \u9700\u8981\u5148\u521B\u5EFA\u81F3\u5C11\u4E00\u4E2A\u63D0\u4EA4\u624D\u80FD\u6253 tag:"));
|
|
1068
|
+
console.log(colors.cyan(" git add ."));
|
|
1069
|
+
console.log(colors.cyan(' git commit -m "Initial commit"'));
|
|
1070
|
+
console.log(colors.cyan(" gw tag"));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const existingTags = execOutput("git tag -l").split("\n").filter(Boolean);
|
|
1074
|
+
if (existingTags.includes(tagName)) {
|
|
1075
|
+
console.log(colors.red(`Tag ${tagName} \u5DF2\u5B58\u5728`));
|
|
1076
|
+
console.log("");
|
|
1077
|
+
console.log(colors.dim(" \u63D0\u793A: \u5982\u9700\u91CD\u65B0\u521B\u5EFA\uFF0C\u8BF7\u5148\u5220\u9664\u65E7 tag:"));
|
|
1078
|
+
console.log(colors.cyan(` git tag -d ${tagName}`));
|
|
1079
|
+
console.log(colors.cyan(` git push origin --delete ${tagName}`));
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1009
1082
|
const spinner = ora2(`\u6B63\u5728\u521B\u5EFA tag: ${tagName}`).start();
|
|
1010
1083
|
const success = await execWithSpinner(
|
|
1011
1084
|
`git tag -a "${tagName}" -m "Release ${tagName}"`,
|
|
@@ -1153,9 +1226,12 @@ async function updateTag() {
|
|
|
1153
1226
|
spinner.fail("tag \u91CD\u547D\u540D\u5931\u8D25");
|
|
1154
1227
|
return;
|
|
1155
1228
|
}
|
|
1156
|
-
const
|
|
1157
|
-
if (!
|
|
1229
|
+
const deleteResult = await execAsync(`git tag -d "${oldTag}"`, spinner);
|
|
1230
|
+
if (!deleteResult.success) {
|
|
1158
1231
|
spinner.fail("\u5220\u9664\u65E7 tag \u5931\u8D25");
|
|
1232
|
+
if (deleteResult.error) {
|
|
1233
|
+
console.log(colors.dim(` ${deleteResult.error}`));
|
|
1234
|
+
}
|
|
1159
1235
|
return;
|
|
1160
1236
|
}
|
|
1161
1237
|
spinner.succeed(`Tag \u5DF2\u91CD\u547D\u540D: ${oldTag} \u2192 ${newTag}`);
|
|
@@ -1169,26 +1245,32 @@ async function updateTag() {
|
|
|
1169
1245
|
});
|
|
1170
1246
|
if (pushRemote) {
|
|
1171
1247
|
const pushSpinner = ora2("\u6B63\u5728\u540C\u6B65\u5230\u8FDC\u7A0B...").start();
|
|
1172
|
-
const
|
|
1248
|
+
const pushNewResult = await execAsync(
|
|
1173
1249
|
`git push origin "${newTag}"`,
|
|
1174
1250
|
pushSpinner
|
|
1175
1251
|
);
|
|
1176
|
-
if (!
|
|
1252
|
+
if (!pushNewResult.success) {
|
|
1177
1253
|
pushSpinner.warn(
|
|
1178
1254
|
`\u8FDC\u7A0B\u540C\u6B65\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C:
|
|
1179
1255
|
git push origin ${newTag}
|
|
1180
1256
|
git push origin --delete ${oldTag}`
|
|
1181
1257
|
);
|
|
1258
|
+
if (pushNewResult.error) {
|
|
1259
|
+
console.log(colors.dim(` ${pushNewResult.error}`));
|
|
1260
|
+
}
|
|
1182
1261
|
return;
|
|
1183
1262
|
}
|
|
1184
|
-
const
|
|
1263
|
+
const deleteOldResult = await execAsync(
|
|
1185
1264
|
`git push origin --delete "${oldTag}"`,
|
|
1186
1265
|
pushSpinner
|
|
1187
1266
|
);
|
|
1188
|
-
if (!
|
|
1267
|
+
if (!deleteOldResult.success) {
|
|
1189
1268
|
pushSpinner.warn(
|
|
1190
1269
|
`\u8FDC\u7A0B\u65E7 tag \u5220\u9664\u5931\u8D25\uFF0C\u53EF\u7A0D\u540E\u624B\u52A8\u6267\u884C: git push origin --delete ${oldTag}`
|
|
1191
1270
|
);
|
|
1271
|
+
if (deleteOldResult.error) {
|
|
1272
|
+
console.log(colors.dim(` ${deleteOldResult.error}`));
|
|
1273
|
+
}
|
|
1192
1274
|
return;
|
|
1193
1275
|
}
|
|
1194
1276
|
pushSpinner.succeed(`\u8FDC\u7A0B tag \u5DF2\u540C\u6B65: ${oldTag} \u2192 ${newTag}`);
|
|
@@ -1241,8 +1323,8 @@ async function cleanInvalidTags() {
|
|
|
1241
1323
|
let localSuccess = 0;
|
|
1242
1324
|
let localFailed = 0;
|
|
1243
1325
|
for (const tag of invalidTags) {
|
|
1244
|
-
const
|
|
1245
|
-
if (success) {
|
|
1326
|
+
const result = await execAsync(`git tag -d "${tag}"`, localSpinner);
|
|
1327
|
+
if (result.success) {
|
|
1246
1328
|
localSuccess++;
|
|
1247
1329
|
} else {
|
|
1248
1330
|
localFailed++;
|
|
@@ -1268,11 +1350,11 @@ async function cleanInvalidTags() {
|
|
|
1268
1350
|
let remoteSuccess = 0;
|
|
1269
1351
|
let remoteFailed = 0;
|
|
1270
1352
|
for (const tag of invalidTags) {
|
|
1271
|
-
const
|
|
1353
|
+
const result = await execAsync(
|
|
1272
1354
|
`git push origin --delete "${tag}"`,
|
|
1273
1355
|
remoteSpinner
|
|
1274
1356
|
);
|
|
1275
|
-
if (success) {
|
|
1357
|
+
if (result.success) {
|
|
1276
1358
|
remoteSuccess++;
|
|
1277
1359
|
} else {
|
|
1278
1360
|
remoteFailed++;
|
|
@@ -2753,16 +2835,28 @@ function clearUpdateCache2() {
|
|
|
2753
2835
|
}
|
|
2754
2836
|
}
|
|
2755
2837
|
async function getLatestVersion2(packageName) {
|
|
2756
|
-
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
timeout:
|
|
2760
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
2838
|
+
return new Promise((resolve) => {
|
|
2839
|
+
const npmView = spawn3("npm", ["view", packageName, "version"], {
|
|
2840
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2841
|
+
timeout: 5e3
|
|
2761
2842
|
});
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2843
|
+
let output = "";
|
|
2844
|
+
if (npmView.stdout) {
|
|
2845
|
+
npmView.stdout.on("data", (data) => {
|
|
2846
|
+
output += data.toString();
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
npmView.on("close", (code) => {
|
|
2850
|
+
if (code === 0 && output.trim()) {
|
|
2851
|
+
resolve(output.trim());
|
|
2852
|
+
} else {
|
|
2853
|
+
resolve(null);
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
npmView.on("error", () => {
|
|
2857
|
+
resolve(null);
|
|
2858
|
+
});
|
|
2859
|
+
});
|
|
2766
2860
|
}
|
|
2767
2861
|
function isUsingVolta2() {
|
|
2768
2862
|
try {
|
|
@@ -3533,7 +3627,7 @@ process.on("SIGTERM", () => {
|
|
|
3533
3627
|
console.log("");
|
|
3534
3628
|
process.exit(0);
|
|
3535
3629
|
});
|
|
3536
|
-
var version = true ? "0.4.
|
|
3630
|
+
var version = true ? "0.4.7" : "0.0.0-dev";
|
|
3537
3631
|
async function mainMenu() {
|
|
3538
3632
|
console.log(
|
|
3539
3633
|
colors.green(`
|
|
@@ -3813,7 +3907,12 @@ cli.command("clean", "\u6E05\u7406\u7F13\u5B58\u548C\u4E34\u65F6\u6587\u4EF6").a
|
|
|
3813
3907
|
});
|
|
3814
3908
|
cli.option("-v, --version", "\u663E\u793A\u7248\u672C\u53F7");
|
|
3815
3909
|
cli.option("-h, --help", "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F");
|
|
3910
|
+
cli.option("-d, --debug", "\u542F\u7528\u8C03\u8BD5\u6A21\u5F0F\uFF0C\u663E\u793A\u8BE6\u7EC6\u7684\u547D\u4EE4\u548C\u9519\u8BEF\u4FE1\u606F");
|
|
3816
3911
|
var processArgs = process.argv.slice(2);
|
|
3912
|
+
if (processArgs.includes("-d") || processArgs.includes("--debug")) {
|
|
3913
|
+
setDebugMode(true);
|
|
3914
|
+
console.log(colors.yellow("\u{1F41B} Debug \u6A21\u5F0F\u5DF2\u542F\u7528\n"));
|
|
3915
|
+
}
|
|
3817
3916
|
if (processArgs.includes("-v") || processArgs.includes("--version")) {
|
|
3818
3917
|
console.log(colors.yellow(`v${version}`));
|
|
3819
3918
|
process.exit(0);
|
|
@@ -54,6 +54,8 @@ export default defineConfig({
|
|
|
54
54
|
items: [
|
|
55
55
|
{ text: "开发指南", link: "/guide/development" },
|
|
56
56
|
{ text: "测试指南", link: "/guide/testing" },
|
|
57
|
+
{ text: "Debug 模式", link: "/guide/debug-mode" },
|
|
58
|
+
{ text: "命令引号处理", link: "/guide/command-quotes-handling" },
|
|
57
59
|
{ text: "API 文档", link: "/guide/api" },
|
|
58
60
|
{ text: "贡献指南", link: "/guide/contributing" },
|
|
59
61
|
],
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# 命令参数引号处理
|
|
2
|
+
|
|
3
|
+
本文档说明了 git-workflow 如何正确处理带引号和特殊字符的命令参数。
|
|
4
|
+
|
|
5
|
+
## 问题背景
|
|
6
|
+
|
|
7
|
+
在早期版本中,使用简单的字符串分割方式处理命令参数:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// ❌ 错误的方式
|
|
11
|
+
const [cmd, ...args] = command.split(" ");
|
|
12
|
+
spawn(cmd, args);
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
这种方式无法正确处理引号:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
'git tag -a "v1.5.3" -m "Release v1.5.3"'.split(" ");
|
|
19
|
+
// 结果: ["git", "tag", "-a", '"v1.5.3"', "-m", '"Release', 'v1.5.3"']
|
|
20
|
+
// 引号被当作参数的一部分!
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
导致的问题:
|
|
24
|
+
|
|
25
|
+
- Tag 名称包含引号:`"v1.5.3"` 而不是 `v1.5.3`
|
|
26
|
+
- Git 报错:`fatal: Failed to resolve '"v1.5.3"' as a valid ref.`
|
|
27
|
+
|
|
28
|
+
## 解决方案
|
|
29
|
+
|
|
30
|
+
使用 `shell: true` 选项让 spawn 通过 shell 执行命令:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// ✅ 正确的方式
|
|
34
|
+
spawn(command, {
|
|
35
|
+
stdio: spinner ? "pipe" : "inherit",
|
|
36
|
+
shell: true, // 使用 shell 模式
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 优势
|
|
41
|
+
|
|
42
|
+
1. **正确处理引号**:Shell 会自动解析和移除引号
|
|
43
|
+
2. **支持特殊字符**:emoji、中文、空格等都能正确处理
|
|
44
|
+
3. **支持转义**:`\"` 等转义字符正常工作
|
|
45
|
+
|
|
46
|
+
## 支持的场景
|
|
47
|
+
|
|
48
|
+
### 1. Tag 命令
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 基本版本号
|
|
52
|
+
gw tag # 创建 v1.5.3
|
|
53
|
+
|
|
54
|
+
# 预发布版本
|
|
55
|
+
gw tag # 创建 v1.0.0-beta.1
|
|
56
|
+
|
|
57
|
+
# 带特殊字符
|
|
58
|
+
git tag -a "v1.0.0-🎉" -m "Release 🎉"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Branch 命令
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 带日期和描述的分支
|
|
65
|
+
gw b feature # 创建 feature/20240120-123-add-feature
|
|
66
|
+
|
|
67
|
+
# 删除带特殊字符的分支
|
|
68
|
+
git branch -D "feature/20240120-123-add-feature"
|
|
69
|
+
git push origin --delete "feature/20240120-123-add-feature"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Stash 命令
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 带中文的 stash 消息
|
|
76
|
+
git stash push -m "临时保存:修复登录bug"
|
|
77
|
+
|
|
78
|
+
# 带引号的消息
|
|
79
|
+
git stash push -m "WIP: 添加\"新功能\""
|
|
80
|
+
|
|
81
|
+
# 从 stash 创建分支
|
|
82
|
+
git stash branch "feature/from-stash" stash@{0}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. Commit 命令
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# 带特殊字符的提交消息
|
|
89
|
+
git commit -m "feat: add \"quotes\" support"
|
|
90
|
+
git commit -m "fix: 修复登录问题 🐛"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 测试覆盖
|
|
94
|
+
|
|
95
|
+
我们创建了全面的测试用例来确保引号处理的正确性:
|
|
96
|
+
|
|
97
|
+
### 测试场景
|
|
98
|
+
|
|
99
|
+
1. **基本引号处理**
|
|
100
|
+
- 带引号的 tag 名称
|
|
101
|
+
- 带空格的分支名称
|
|
102
|
+
- 带特殊字符的 commit message
|
|
103
|
+
|
|
104
|
+
2. **特殊字符支持**
|
|
105
|
+
- Emoji:`v1.0.0-🎉`
|
|
106
|
+
- 中文:`临时保存:修复bug`
|
|
107
|
+
- 转义引号:`feat: add \"quotes\" support`
|
|
108
|
+
|
|
109
|
+
3. **错误处理**
|
|
110
|
+
- 捕获 stderr 错误信息
|
|
111
|
+
- 显示详细的失败原因
|
|
112
|
+
- 提供解决建议
|
|
113
|
+
|
|
114
|
+
### 运行测试
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 运行引号处理测试
|
|
118
|
+
npm test -- tests/command-with-quotes.test.ts
|
|
119
|
+
|
|
120
|
+
# 运行所有测试
|
|
121
|
+
npm test
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 错误信息改进
|
|
125
|
+
|
|
126
|
+
现在当命令失败时,会显示详细的错误信息:
|
|
127
|
+
|
|
128
|
+
### Tag 已存在
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
✗ Tag v1.5.3 已存在
|
|
132
|
+
|
|
133
|
+
提示: 如需重新创建,请先删除旧 tag:
|
|
134
|
+
git tag -d v1.5.3
|
|
135
|
+
git push origin --delete v1.5.3
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 没有提交
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
✗ 当前仓库没有任何提交
|
|
142
|
+
|
|
143
|
+
提示: 需要先创建至少一个提交才能打 tag:
|
|
144
|
+
git add .
|
|
145
|
+
git commit -m "Initial commit"
|
|
146
|
+
gw tag
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Git 命令错误
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
✗ tag 创建失败
|
|
153
|
+
fatal: Failed to resolve 'HEAD' as a valid ref.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 实现细节
|
|
157
|
+
|
|
158
|
+
### execAsync 函数
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
export function execAsync(
|
|
162
|
+
command: string,
|
|
163
|
+
spinner?: Ora,
|
|
164
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const process = spawn(command, {
|
|
167
|
+
stdio: spinner ? "pipe" : "inherit",
|
|
168
|
+
shell: true, // 关键:使用 shell 模式
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let errorOutput = "";
|
|
172
|
+
|
|
173
|
+
// 捕获错误输出
|
|
174
|
+
if (process.stderr) {
|
|
175
|
+
process.stderr.on("data", (data) => {
|
|
176
|
+
errorOutput += data.toString();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
process.on("close", (code) => {
|
|
181
|
+
if (code === 0) {
|
|
182
|
+
resolve({ success: true });
|
|
183
|
+
} else {
|
|
184
|
+
resolve({ success: false, error: errorOutput.trim() });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
process.on("error", (err) => {
|
|
189
|
+
resolve({ success: false, error: err.message });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### execWithSpinner 函数
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
export async function execWithSpinner(
|
|
199
|
+
command: string,
|
|
200
|
+
spinner: Ora,
|
|
201
|
+
successMessage?: string,
|
|
202
|
+
errorMessage?: string,
|
|
203
|
+
): Promise<boolean> {
|
|
204
|
+
const result = await execAsync(command, spinner);
|
|
205
|
+
|
|
206
|
+
if (result.success) {
|
|
207
|
+
if (successMessage) {
|
|
208
|
+
spinner.succeed(successMessage);
|
|
209
|
+
} else {
|
|
210
|
+
spinner.succeed();
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
if (errorMessage) {
|
|
214
|
+
spinner.fail(errorMessage);
|
|
215
|
+
} else {
|
|
216
|
+
spinner.fail();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 显示具体的错误信息
|
|
220
|
+
if (result.error) {
|
|
221
|
+
console.log(colors.dim(` ${result.error}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return result.success;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## 最佳实践
|
|
230
|
+
|
|
231
|
+
### 1. 始终使用引号包裹可能包含特殊字符的参数
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// ✅ 推荐
|
|
235
|
+
await execAsync(`git tag -a "${tagName}" -m "Release ${tagName}"`);
|
|
236
|
+
|
|
237
|
+
// ❌ 不推荐(如果 tagName 包含空格会失败)
|
|
238
|
+
await execAsync(`git tag -a ${tagName} -m Release ${tagName}`);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 2. 转义用户输入中的引号
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// ✅ 正确处理用户输入
|
|
245
|
+
const message = userInput.replace(/"/g, '\\"');
|
|
246
|
+
await execAsync(`git commit -m "${message}"`);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 3. 使用 execWithSpinner 显示进度
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ✅ 推荐:显示进度和错误信息
|
|
253
|
+
const spinner = ora("正在创建 tag...").start();
|
|
254
|
+
const success = await execWithSpinner(
|
|
255
|
+
`git tag -a "${tagName}" -m "Release ${tagName}"`,
|
|
256
|
+
spinner,
|
|
257
|
+
"Tag 创建成功",
|
|
258
|
+
"Tag 创建失败",
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!success) {
|
|
262
|
+
// 错误信息已自动显示
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## 相关文件
|
|
268
|
+
|
|
269
|
+
- `src/utils.ts` - execAsync 和 execWithSpinner 实现
|
|
270
|
+
- `tests/command-with-quotes.test.ts` - 引号处理测试
|
|
271
|
+
- `src/commands/tag.ts` - Tag 命令实现
|
|
272
|
+
- `src/commands/branch.ts` - Branch 命令实现
|
|
273
|
+
- `src/commands/stash.ts` - Stash 命令实现
|
|
274
|
+
|
|
275
|
+
## 版本历史
|
|
276
|
+
|
|
277
|
+
- **v0.4.5** - 修复引号处理问题,添加详细错误信息
|
|
278
|
+
- **v0.4.4** - 修复 spinner 阻塞问题
|
|
279
|
+
- **v0.4.3** - 添加 execAsync 和 execWithSpinner 函数
|