@zjex/git-workflow 0.4.4 → 0.4.6
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 +29 -0
- package/README.md +1 -1
- package/dist/index.js +87 -15
- package/docs/commands/amend-date.md +75 -13
- package/docs/guide/command-quotes-handling.md +279 -0
- package/docs/guide/debug-mode.md +384 -0
- package/package.json +1 -1
- package/src/commands/amend-date.ts +25 -5
- package/src/commands/amend.ts +0 -1
- package/src/index.ts +11 -3
- package/src/utils.ts +91 -10
- package/tests/amend-date.test.ts +59 -4
- package/tests/command-with-quotes.test.ts +378 -0
- package/tests/debug-mode.test.ts +503 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v1.0.0](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.5...v1.0.0) (2026-01-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## [v0.4.5](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.4...v0.4.5) (2026-01-19)
|
|
7
|
+
|
|
8
|
+
### ✨ Features
|
|
9
|
+
|
|
10
|
+
- 更新日期格式说明,支持完整时间输入 ([25f2b39](https://github.com/iamzjt-front-end/git-workflow/commit/25f2b39))
|
|
11
|
+
|
|
12
|
+
### 📖 Documentation
|
|
13
|
+
|
|
14
|
+
- 📝 docs: 自动更新测试数量徽章 [skip ci] ([c49a4bd](https://github.com/iamzjt-front-end/git-workflow/commit/c49a4bd))
|
|
15
|
+
|
|
16
|
+
### 🔧 Chore
|
|
17
|
+
|
|
18
|
+
- 🔖 chore(release): 发布 v0.4.5 ([8abd7b4](https://github.com/iamzjt-front-end/git-workflow/commit/8abd7b4))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## [v0.4.4](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.3...v0.4.4) (2026-01-19)
|
|
22
|
+
|
|
23
|
+
### ✨ Features
|
|
24
|
+
|
|
25
|
+
- 优化分支创建和删除功能的用户体验 ([88dec43](https://github.com/iamzjt-front-end/git-workflow/commit/88dec43))
|
|
26
|
+
|
|
27
|
+
### 🔧 Chore
|
|
28
|
+
|
|
29
|
+
- 🔖 chore(release): 发布 v0.4.4 ([d673bfb](https://github.com/iamzjt-front-end/git-workflow/commit/d673bfb))
|
|
30
|
+
|
|
31
|
+
|
|
3
32
|
## [v0.4.3](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.2...v0.4.3) (2026-01-19)
|
|
4
33
|
|
|
5
34
|
### ✨ Features
|
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,57 @@ 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
|
+
console.log(colors.dim(`[DEBUG] \u9519\u8BEF\u8F93\u51FA:
|
|
88
|
+
${errorOutput}`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (code === 0) {
|
|
92
|
+
resolve({ success: true });
|
|
93
|
+
} else {
|
|
94
|
+
resolve({ success: false, error: errorOutput.trim() });
|
|
95
|
+
}
|
|
62
96
|
});
|
|
63
|
-
process2.on("error", () => {
|
|
64
|
-
|
|
97
|
+
process2.on("error", (err) => {
|
|
98
|
+
if (debugMode) {
|
|
99
|
+
console.log(colors.dim(`[DEBUG] \u8FDB\u7A0B\u9519\u8BEF: ${err.message}`));
|
|
100
|
+
console.log(colors.dim(`[DEBUG] \u9519\u8BEF\u5806\u6808:
|
|
101
|
+
${err.stack}`));
|
|
102
|
+
}
|
|
103
|
+
resolve({ success: false, error: err.message });
|
|
65
104
|
});
|
|
66
105
|
});
|
|
67
106
|
}
|
|
68
107
|
async function execWithSpinner(command, spinner, successMessage, errorMessage) {
|
|
69
|
-
const
|
|
70
|
-
if (success) {
|
|
108
|
+
const result = await execAsync(command, spinner);
|
|
109
|
+
if (result.success) {
|
|
71
110
|
if (successMessage) {
|
|
72
111
|
spinner.succeed(successMessage);
|
|
73
112
|
} else {
|
|
@@ -79,13 +118,27 @@ async function execWithSpinner(command, spinner, successMessage, errorMessage) {
|
|
|
79
118
|
} else {
|
|
80
119
|
spinner.fail();
|
|
81
120
|
}
|
|
121
|
+
if (result.error) {
|
|
122
|
+
console.log(colors.dim(` ${result.error}`));
|
|
123
|
+
}
|
|
124
|
+
if (debugMode) {
|
|
125
|
+
console.log(colors.yellow("\n[DEBUG] \u6545\u969C\u6392\u67E5\u4FE1\u606F:"));
|
|
126
|
+
console.log(colors.dim(` \u547D\u4EE4: ${command}`));
|
|
127
|
+
console.log(colors.dim(` \u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}`));
|
|
128
|
+
console.log(colors.dim(` Shell: ${process.env.SHELL || "unknown"}`));
|
|
129
|
+
console.log(
|
|
130
|
+
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
|
|
131
|
+
`)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
82
134
|
}
|
|
83
|
-
return success;
|
|
135
|
+
return result.success;
|
|
84
136
|
}
|
|
85
|
-
var colors, TODAY, theme;
|
|
137
|
+
var debugMode, colors, TODAY, theme;
|
|
86
138
|
var init_utils = __esm({
|
|
87
139
|
"src/utils.ts"() {
|
|
88
140
|
"use strict";
|
|
141
|
+
debugMode = false;
|
|
89
142
|
colors = {
|
|
90
143
|
red: (s) => `\x1B[31m${s}\x1B[0m`,
|
|
91
144
|
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
@@ -3223,6 +3276,20 @@ function formatGitDate(date) {
|
|
|
3223
3276
|
}
|
|
3224
3277
|
function parseDate(input8) {
|
|
3225
3278
|
const trimmed = input8.trim();
|
|
3279
|
+
const fullMatch = trimmed.match(
|
|
3280
|
+
/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/
|
|
3281
|
+
);
|
|
3282
|
+
if (fullMatch) {
|
|
3283
|
+
const [, year, month, day, hours, minutes, seconds] = fullMatch;
|
|
3284
|
+
return new Date(
|
|
3285
|
+
parseInt(year),
|
|
3286
|
+
parseInt(month) - 1,
|
|
3287
|
+
parseInt(day),
|
|
3288
|
+
parseInt(hours),
|
|
3289
|
+
parseInt(minutes),
|
|
3290
|
+
parseInt(seconds)
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3226
3293
|
const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
3227
3294
|
if (dateMatch) {
|
|
3228
3295
|
const [, year, month, day] = dateMatch;
|
|
@@ -3279,7 +3346,6 @@ async function amendDate(commitHash) {
|
|
|
3279
3346
|
value: c,
|
|
3280
3347
|
description: c.date
|
|
3281
3348
|
})),
|
|
3282
|
-
pageSize: 15,
|
|
3283
3349
|
theme
|
|
3284
3350
|
});
|
|
3285
3351
|
}
|
|
@@ -3289,14 +3355,16 @@ async function amendDate(commitHash) {
|
|
|
3289
3355
|
console.log(` Message: ${selectedCommit.message}`);
|
|
3290
3356
|
console.log(` Date: ${colors.cyan(selectedCommit.date)}`);
|
|
3291
3357
|
divider();
|
|
3292
|
-
console.log(colors.dim("\
|
|
3358
|
+
console.log(colors.dim("\u652F\u6301\u683C\u5F0F:"));
|
|
3359
|
+
console.log(colors.dim(" YYYY-MM-DD (\u5982: 2026-01-19\uFF0C\u9ED8\u8BA4 00:00:00)"));
|
|
3360
|
+
console.log(colors.dim(" YYYY-MM-DD HH:mm:ss (\u5982: 2026-01-19 14:30:00)"));
|
|
3293
3361
|
console.log("");
|
|
3294
3362
|
const dateInput = await input6({
|
|
3295
3363
|
message: "\u8F93\u5165\u65B0\u7684\u65E5\u671F:",
|
|
3296
3364
|
validate: (value) => {
|
|
3297
3365
|
const parsed = parseDate(value);
|
|
3298
3366
|
if (!parsed) {
|
|
3299
|
-
return "\u65E5\u671F\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528 YYYY-MM-DD \u683C\u5F0F";
|
|
3367
|
+
return "\u65E5\u671F\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u8BF7\u4F7F\u7528 YYYY-MM-DD \u6216 YYYY-MM-DD HH:mm:ss \u683C\u5F0F";
|
|
3300
3368
|
}
|
|
3301
3369
|
return true;
|
|
3302
3370
|
},
|
|
@@ -3409,7 +3477,6 @@ async function amend(commitHash) {
|
|
|
3409
3477
|
value: c,
|
|
3410
3478
|
description: c.message
|
|
3411
3479
|
})),
|
|
3412
|
-
pageSize: 15,
|
|
3413
3480
|
theme
|
|
3414
3481
|
});
|
|
3415
3482
|
}
|
|
@@ -3519,7 +3586,7 @@ process.on("SIGTERM", () => {
|
|
|
3519
3586
|
console.log("");
|
|
3520
3587
|
process.exit(0);
|
|
3521
3588
|
});
|
|
3522
|
-
var version = true ? "0.4.
|
|
3589
|
+
var version = true ? "0.4.6" : "0.0.0-dev";
|
|
3523
3590
|
async function mainMenu() {
|
|
3524
3591
|
console.log(
|
|
3525
3592
|
colors.green(`
|
|
@@ -3799,7 +3866,12 @@ cli.command("clean", "\u6E05\u7406\u7F13\u5B58\u548C\u4E34\u65F6\u6587\u4EF6").a
|
|
|
3799
3866
|
});
|
|
3800
3867
|
cli.option("-v, --version", "\u663E\u793A\u7248\u672C\u53F7");
|
|
3801
3868
|
cli.option("-h, --help", "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F");
|
|
3869
|
+
cli.option("-d, --debug", "\u542F\u7528\u8C03\u8BD5\u6A21\u5F0F\uFF0C\u663E\u793A\u8BE6\u7EC6\u7684\u547D\u4EE4\u548C\u9519\u8BEF\u4FE1\u606F");
|
|
3802
3870
|
var processArgs = process.argv.slice(2);
|
|
3871
|
+
if (processArgs.includes("-d") || processArgs.includes("--debug")) {
|
|
3872
|
+
setDebugMode(true);
|
|
3873
|
+
console.log(colors.yellow("\u{1F41B} Debug \u6A21\u5F0F\u5DF2\u542F\u7528\n"));
|
|
3874
|
+
}
|
|
3803
3875
|
if (processArgs.includes("-v") || processArgs.includes("--version")) {
|
|
3804
3876
|
console.log(colors.yellow(`v${version}`));
|
|
3805
3877
|
process.exit(0);
|
|
@@ -66,12 +66,17 @@ gw ad HEAD~2 # 上上个 commit
|
|
|
66
66
|
### 步骤 3: 输入新日期
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
|
|
69
|
+
支持格式:
|
|
70
|
+
YYYY-MM-DD (如: 2026-01-19,默认 00:00:00)
|
|
71
|
+
YYYY-MM-DD HH:mm:ss (如: 2026-01-19 14:30:00)
|
|
70
72
|
|
|
71
|
-
? 输入新的日期: 2026-01-15
|
|
73
|
+
? 输入新的日期: 2026-01-15 14:30:00
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
|
|
76
|
+
**支持的日期格式**:
|
|
77
|
+
|
|
78
|
+
- `YYYY-MM-DD` - 只指定日期,时间默认为 `00:00:00`
|
|
79
|
+
- `YYYY-MM-DD HH:mm:ss` - 指定完整的日期和时间
|
|
75
80
|
|
|
76
81
|
### 步骤 4: 确认修改
|
|
77
82
|
|
|
@@ -79,7 +84,7 @@ gw ad HEAD~2 # 上上个 commit
|
|
|
79
84
|
修改预览:
|
|
80
85
|
Commit: a1b2c3d
|
|
81
86
|
旧时间: 2026-01-19 14:30:00 +0800
|
|
82
|
-
新时间: 2026-01-15
|
|
87
|
+
新时间: 2026-01-15 14:30:00
|
|
83
88
|
修改类型: Author + Committer (两者都修改)
|
|
84
89
|
────────────────────────────────────────
|
|
85
90
|
|
|
@@ -164,9 +169,37 @@ git push --force
|
|
|
164
169
|
|
|
165
170
|
### 📅 时间格式
|
|
166
171
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
-
|
|
172
|
+
支持两种输入格式:
|
|
173
|
+
|
|
174
|
+
1. **仅日期**: `YYYY-MM-DD`(如 `2026-01-19`)
|
|
175
|
+
- 时间默认为 `00:00:00`
|
|
176
|
+
- 适合只关心日期的场景
|
|
177
|
+
|
|
178
|
+
2. **完整时间**: `YYYY-MM-DD HH:mm:ss`(如 `2026-01-19 14:30:00`)
|
|
179
|
+
- 可以精确指定时分秒
|
|
180
|
+
- 适合需要精确时间的场景
|
|
181
|
+
|
|
182
|
+
**示例**:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# 只指定日期
|
|
186
|
+
输入: 2026-01-19
|
|
187
|
+
结果: 2026-01-19 00:00:00
|
|
188
|
+
|
|
189
|
+
# 指定完整时间
|
|
190
|
+
输入: 2026-01-19 14:30:00
|
|
191
|
+
结果: 2026-01-19 14:30:00
|
|
192
|
+
|
|
193
|
+
# 指定午夜
|
|
194
|
+
输入: 2026-01-19 00:00:00
|
|
195
|
+
结果: 2026-01-19 00:00:00
|
|
196
|
+
|
|
197
|
+
# 指定一天结束
|
|
198
|
+
输入: 2026-01-19 23:59:59
|
|
199
|
+
结果: 2026-01-19 23:59:59
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**时区**: 使用系统当前时区
|
|
170
203
|
|
|
171
204
|
### 🔄 修改范围
|
|
172
205
|
|
|
@@ -187,11 +220,21 @@ $ gw ad HEAD
|
|
|
187
220
|
✔ 修改成功
|
|
188
221
|
```
|
|
189
222
|
|
|
190
|
-
### 示例 2:
|
|
223
|
+
### 示例 2: 修改为指定的完整时间
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
$ gw ad HEAD
|
|
227
|
+
输入新的日期: 2026-01-15 14:30:00
|
|
228
|
+
确认修改? y
|
|
229
|
+
|
|
230
|
+
✔ 修改成功
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 示例 3: 修改指定 hash 的 commit
|
|
191
234
|
|
|
192
235
|
```bash
|
|
193
236
|
$ gw ad a1b2c3d
|
|
194
|
-
输入新的日期: 2026-01-10
|
|
237
|
+
输入新的日期: 2026-01-10 09:00:00
|
|
195
238
|
确认修改? y
|
|
196
239
|
|
|
197
240
|
⚠️ 警告: 修改非最新 commit 需要使用 rebase,可能会改变 commit hash
|
|
@@ -199,7 +242,7 @@ $ gw ad a1b2c3d
|
|
|
199
242
|
✔ 修改成功
|
|
200
243
|
```
|
|
201
244
|
|
|
202
|
-
### 示例
|
|
245
|
+
### 示例 4: 交互式选择并修改
|
|
203
246
|
|
|
204
247
|
```bash
|
|
205
248
|
$ gw ad
|
|
@@ -208,7 +251,7 @@ $ gw ad
|
|
|
208
251
|
❯ a1b2c3d ✨ feat: 添加登录功能 2026-01-19 14:30:00
|
|
209
252
|
d4e5f6g 🐛 fix: 修复bug 2026-01-18 10:20:00
|
|
210
253
|
|
|
211
|
-
输入新的日期: 2026-01-15
|
|
254
|
+
输入新的日期: 2026-01-15 16:45:30
|
|
212
255
|
确认修改? y
|
|
213
256
|
|
|
214
257
|
✔ 修改成功
|
|
@@ -363,11 +406,30 @@ git am < patch.diff
|
|
|
363
406
|
|
|
364
407
|
### Q: 为什么时间是 00:00:00?
|
|
365
408
|
|
|
366
|
-
A:
|
|
409
|
+
A: 当你只输入日期(YYYY-MM-DD)时,时间会默认为 00:00:00。这是为了简化操作,大多数情况下我们只关心日期。
|
|
410
|
+
|
|
411
|
+
如果需要指定精确时间,可以使用完整格式:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# 只输入日期
|
|
415
|
+
输入: 2026-01-15
|
|
416
|
+
结果: 2026-01-15 00:00:00
|
|
417
|
+
|
|
418
|
+
# 输入完整时间
|
|
419
|
+
输入: 2026-01-15 14:30:00
|
|
420
|
+
结果: 2026-01-15 14:30:00
|
|
421
|
+
```
|
|
367
422
|
|
|
368
423
|
### Q: 可以修改时间部分吗?
|
|
369
424
|
|
|
370
|
-
A:
|
|
425
|
+
A: 可以!使用完整格式 `YYYY-MM-DD HH:mm:ss` 即可指定精确时间:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
$ gw ad
|
|
429
|
+
输入新的日期: 2026-01-15 14:30:00
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
如果需要更灵活的控制,也可以手动使用 git 命令:
|
|
371
433
|
|
|
372
434
|
```bash
|
|
373
435
|
# 修改最新 commit 的时间为指定的精确时间
|
|
@@ -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 函数
|