@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/src/utils.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { execSync, spawn, type ExecSyncOptions } from "child_process";
|
|
2
2
|
import type { Ora } from "ora";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 全局 debug 模式标志
|
|
6
|
+
*/
|
|
7
|
+
let debugMode = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 设置 debug 模式(由主入口调用)
|
|
11
|
+
*/
|
|
12
|
+
export function setDebugMode(enabled: boolean): void {
|
|
13
|
+
debugMode = enabled;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 获取当前 debug 模式状态
|
|
18
|
+
*/
|
|
19
|
+
export function isDebugMode(): boolean {
|
|
20
|
+
return debugMode;
|
|
21
|
+
}
|
|
22
|
+
|
|
4
23
|
export interface Colors {
|
|
5
24
|
red: (s: string) => string;
|
|
6
25
|
green: (s: string) => string;
|
|
@@ -104,22 +123,70 @@ export function divider(): void {
|
|
|
104
123
|
* 使用 spawn 异步执行命令,避免阻塞 spinner
|
|
105
124
|
* @param command 命令字符串
|
|
106
125
|
* @param spinner 可选的 ora spinner 实例
|
|
107
|
-
* @returns Promise<boolean
|
|
126
|
+
* @returns Promise<{success: boolean, error?: string}> 返回执行结果和错误信息
|
|
108
127
|
*/
|
|
109
|
-
export function execAsync(
|
|
128
|
+
export function execAsync(
|
|
129
|
+
command: string,
|
|
130
|
+
spinner?: Ora,
|
|
131
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
110
132
|
return new Promise((resolve) => {
|
|
111
|
-
|
|
133
|
+
// Debug 模式:显示执行的命令
|
|
134
|
+
if (debugMode) {
|
|
135
|
+
console.log(colors.dim(`\n[DEBUG] 执行命令: ${colors.cyan(command)}`));
|
|
136
|
+
}
|
|
112
137
|
|
|
113
|
-
|
|
138
|
+
// 使用 shell 模式执行命令,这样可以正确处理引号
|
|
139
|
+
const process = spawn(command, {
|
|
114
140
|
stdio: spinner ? "pipe" : "inherit",
|
|
141
|
+
shell: true,
|
|
115
142
|
});
|
|
116
143
|
|
|
144
|
+
let errorOutput = "";
|
|
145
|
+
let stdoutOutput = "";
|
|
146
|
+
|
|
147
|
+
// 捕获标准输出(debug 模式)
|
|
148
|
+
if (debugMode && process.stdout) {
|
|
149
|
+
process.stdout.on("data", (data) => {
|
|
150
|
+
stdoutOutput += data.toString();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 捕获错误输出
|
|
155
|
+
if (process.stderr) {
|
|
156
|
+
process.stderr.on("data", (data) => {
|
|
157
|
+
errorOutput += data.toString();
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
117
161
|
process.on("close", (code) => {
|
|
118
|
-
|
|
162
|
+
// Debug 模式:显示退出码和输出
|
|
163
|
+
if (debugMode) {
|
|
164
|
+
console.log(colors.dim(`[DEBUG] 退出码: ${code}`));
|
|
165
|
+
if (stdoutOutput) {
|
|
166
|
+
console.log(colors.dim(`[DEBUG] 标准输出:\n${stdoutOutput}`));
|
|
167
|
+
}
|
|
168
|
+
if (errorOutput) {
|
|
169
|
+
// 根据退出码决定标签:成功时显示"输出信息",失败时显示"错误输出"
|
|
170
|
+
const label = code === 0 ? "输出信息" : "错误输出";
|
|
171
|
+
console.log(colors.dim(`[DEBUG] ${label}:\n${errorOutput}`));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (code === 0) {
|
|
176
|
+
resolve({ success: true });
|
|
177
|
+
} else {
|
|
178
|
+
resolve({ success: false, error: errorOutput.trim() });
|
|
179
|
+
}
|
|
119
180
|
});
|
|
120
181
|
|
|
121
|
-
process.on("error", () => {
|
|
122
|
-
|
|
182
|
+
process.on("error", (err) => {
|
|
183
|
+
// Debug 模式:显示进程错误
|
|
184
|
+
if (debugMode) {
|
|
185
|
+
console.log(colors.dim(`[DEBUG] 进程错误: ${err.message}`));
|
|
186
|
+
console.log(colors.dim(`[DEBUG] 错误堆栈:\n${err.stack}`));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
resolve({ success: false, error: err.message });
|
|
123
190
|
});
|
|
124
191
|
});
|
|
125
192
|
}
|
|
@@ -138,9 +205,9 @@ export async function execWithSpinner(
|
|
|
138
205
|
successMessage?: string,
|
|
139
206
|
errorMessage?: string,
|
|
140
207
|
): Promise<boolean> {
|
|
141
|
-
const
|
|
208
|
+
const result = await execAsync(command, spinner);
|
|
142
209
|
|
|
143
|
-
if (success) {
|
|
210
|
+
if (result.success) {
|
|
144
211
|
if (successMessage) {
|
|
145
212
|
spinner.succeed(successMessage);
|
|
146
213
|
} else {
|
|
@@ -152,7 +219,23 @@ export async function execWithSpinner(
|
|
|
152
219
|
} else {
|
|
153
220
|
spinner.fail();
|
|
154
221
|
}
|
|
222
|
+
|
|
223
|
+
// 显示具体的错误信息
|
|
224
|
+
if (result.error) {
|
|
225
|
+
console.log(colors.dim(` ${result.error}`));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Debug 模式:显示完整的命令和建议
|
|
229
|
+
if (debugMode) {
|
|
230
|
+
console.log(colors.yellow("\n[DEBUG] 故障排查信息:"));
|
|
231
|
+
console.log(colors.dim(` 命令: ${command}`));
|
|
232
|
+
console.log(colors.dim(` 工作目录: ${process.cwd()}`));
|
|
233
|
+
console.log(colors.dim(` Shell: ${process.env.SHELL || "unknown"}`));
|
|
234
|
+
console.log(
|
|
235
|
+
colors.dim(` 建议: 尝试在终端中直接运行上述命令以获取更多信息\n`),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
155
238
|
}
|
|
156
239
|
|
|
157
|
-
return success;
|
|
240
|
+
return result.success;
|
|
158
241
|
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
|
|
4
|
+
// Mock child_process
|
|
5
|
+
vi.mock("child_process", () => ({
|
|
6
|
+
spawn: vi.fn(),
|
|
7
|
+
execSync: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("ora", () => ({
|
|
11
|
+
default: vi.fn(() => ({
|
|
12
|
+
start: vi.fn().mockReturnThis(),
|
|
13
|
+
stop: vi.fn().mockReturnThis(),
|
|
14
|
+
succeed: vi.fn().mockReturnThis(),
|
|
15
|
+
fail: vi.fn().mockReturnThis(),
|
|
16
|
+
warn: vi.fn().mockReturnThis(),
|
|
17
|
+
})),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe("带引号的命令参数测试", () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("execAsync 函数", () => {
|
|
30
|
+
it("应该正确处理带引号的 tag 名称", async () => {
|
|
31
|
+
const mockSpawn = vi.mocked(spawn);
|
|
32
|
+
mockSpawn.mockImplementation(((command: string, options: any) => {
|
|
33
|
+
// 验证使用了 shell 模式
|
|
34
|
+
expect(options.shell).toBe(true);
|
|
35
|
+
|
|
36
|
+
// 验证命令字符串包含正确的引号
|
|
37
|
+
expect(command).toContain('"v1.5.3"');
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
stderr: {
|
|
41
|
+
on: vi.fn(),
|
|
42
|
+
},
|
|
43
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
44
|
+
if (event === "close") {
|
|
45
|
+
setTimeout(() => callback(0), 0);
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
} as any;
|
|
49
|
+
}) as any);
|
|
50
|
+
|
|
51
|
+
const { execAsync } = await import("../src/utils.js");
|
|
52
|
+
const result = await execAsync('git tag -a "v1.5.3" -m "Release v1.5.3"');
|
|
53
|
+
|
|
54
|
+
expect(result.success).toBe(true);
|
|
55
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
56
|
+
'git tag -a "v1.5.3" -m "Release v1.5.3"',
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
shell: true,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("应该正确处理带空格的分支名称", async () => {
|
|
64
|
+
const mockSpawn = vi.mocked(spawn);
|
|
65
|
+
mockSpawn.mockImplementation(((command: string, options: any) => {
|
|
66
|
+
expect(options.shell).toBe(true);
|
|
67
|
+
expect(command).toContain('"feature/my branch"');
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
stderr: {
|
|
71
|
+
on: vi.fn(),
|
|
72
|
+
},
|
|
73
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
74
|
+
if (event === "close") {
|
|
75
|
+
setTimeout(() => callback(0), 0);
|
|
76
|
+
}
|
|
77
|
+
}),
|
|
78
|
+
} as any;
|
|
79
|
+
}) as any);
|
|
80
|
+
|
|
81
|
+
const { execAsync } = await import("../src/utils.js");
|
|
82
|
+
const result = await execAsync('git push -u origin "feature/my branch"');
|
|
83
|
+
|
|
84
|
+
expect(result.success).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("应该正确处理带特殊字符的 commit message", async () => {
|
|
88
|
+
const mockSpawn = vi.mocked(spawn);
|
|
89
|
+
mockSpawn.mockImplementation(((command: string, options: any) => {
|
|
90
|
+
expect(options.shell).toBe(true);
|
|
91
|
+
// 验证特殊字符被正确转义(在命令字符串中是转义的)
|
|
92
|
+
expect(command).toContain('feat: add \\"quotes\\" support');
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
stderr: {
|
|
96
|
+
on: vi.fn(),
|
|
97
|
+
},
|
|
98
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
99
|
+
if (event === "close") {
|
|
100
|
+
setTimeout(() => callback(0), 0);
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
} as any;
|
|
104
|
+
}) as any);
|
|
105
|
+
|
|
106
|
+
const { execAsync } = await import("../src/utils.js");
|
|
107
|
+
const result = await execAsync(
|
|
108
|
+
'git commit -m "feat: add \\"quotes\\" support"',
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("应该正确处理带 emoji 的 tag 名称", async () => {
|
|
115
|
+
const mockSpawn = vi.mocked(spawn);
|
|
116
|
+
mockSpawn.mockImplementation(((command: string, options: any) => {
|
|
117
|
+
expect(options.shell).toBe(true);
|
|
118
|
+
expect(command).toContain('"v1.0.0-🎉"');
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
stderr: {
|
|
122
|
+
on: vi.fn(),
|
|
123
|
+
},
|
|
124
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
125
|
+
if (event === "close") {
|
|
126
|
+
setTimeout(() => callback(0), 0);
|
|
127
|
+
}
|
|
128
|
+
}),
|
|
129
|
+
} as any;
|
|
130
|
+
}) as any);
|
|
131
|
+
|
|
132
|
+
const { execAsync } = await import("../src/utils.js");
|
|
133
|
+
const result = await execAsync('git tag -a "v1.0.0-🎉" -m "Release 🎉"');
|
|
134
|
+
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("应该正确处理带中文的 stash message", async () => {
|
|
139
|
+
const mockSpawn = vi.mocked(spawn);
|
|
140
|
+
mockSpawn.mockImplementation(((command: string, options: any) => {
|
|
141
|
+
expect(options.shell).toBe(true);
|
|
142
|
+
expect(command).toContain('"临时保存:修复bug"');
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
stderr: {
|
|
146
|
+
on: vi.fn(),
|
|
147
|
+
},
|
|
148
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
149
|
+
if (event === "close") {
|
|
150
|
+
setTimeout(() => callback(0), 0);
|
|
151
|
+
}
|
|
152
|
+
}),
|
|
153
|
+
} as any;
|
|
154
|
+
}) as any);
|
|
155
|
+
|
|
156
|
+
const { execAsync } = await import("../src/utils.js");
|
|
157
|
+
const result = await execAsync('git stash push -m "临时保存:修复bug"');
|
|
158
|
+
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("应该正确捕获错误信息", async () => {
|
|
163
|
+
const mockSpawn = vi.mocked(spawn);
|
|
164
|
+
const errorMessage = "fatal: tag 'v1.5.3' already exists";
|
|
165
|
+
|
|
166
|
+
mockSpawn.mockImplementation((() => {
|
|
167
|
+
return {
|
|
168
|
+
stderr: {
|
|
169
|
+
on: vi.fn((event: string, callback: (data: Buffer) => void) => {
|
|
170
|
+
if (event === "data") {
|
|
171
|
+
setTimeout(() => callback(Buffer.from(errorMessage)), 0);
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
176
|
+
if (event === "close") {
|
|
177
|
+
setTimeout(() => callback(1), 10);
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
} as any;
|
|
181
|
+
}) as any);
|
|
182
|
+
|
|
183
|
+
const { execAsync } = await import("../src/utils.js");
|
|
184
|
+
const result = await execAsync('git tag -a "v1.5.3" -m "Release v1.5.3"');
|
|
185
|
+
|
|
186
|
+
expect(result.success).toBe(false);
|
|
187
|
+
expect(result.error).toBe(errorMessage);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("应该处理命令执行错误", async () => {
|
|
191
|
+
const mockSpawn = vi.mocked(spawn);
|
|
192
|
+
|
|
193
|
+
mockSpawn.mockImplementation((() => {
|
|
194
|
+
return {
|
|
195
|
+
stderr: {
|
|
196
|
+
on: vi.fn(),
|
|
197
|
+
},
|
|
198
|
+
on: vi.fn((event: string, callback: (error?: Error) => void) => {
|
|
199
|
+
if (event === "error") {
|
|
200
|
+
setTimeout(() => callback(new Error("Command not found")), 0);
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
} as any;
|
|
204
|
+
}) as any);
|
|
205
|
+
|
|
206
|
+
const { execAsync } = await import("../src/utils.js");
|
|
207
|
+
const result = await execAsync("invalid-command");
|
|
208
|
+
|
|
209
|
+
expect(result.success).toBe(false);
|
|
210
|
+
expect(result.error).toBe("Command not found");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("execWithSpinner 函数", () => {
|
|
215
|
+
it("应该在成功时显示成功消息", async () => {
|
|
216
|
+
const mockSpawn = vi.mocked(spawn);
|
|
217
|
+
mockSpawn.mockImplementation((() => {
|
|
218
|
+
return {
|
|
219
|
+
stderr: {
|
|
220
|
+
on: vi.fn(),
|
|
221
|
+
},
|
|
222
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
223
|
+
if (event === "close") {
|
|
224
|
+
setTimeout(() => callback(0), 0);
|
|
225
|
+
}
|
|
226
|
+
}),
|
|
227
|
+
} as any;
|
|
228
|
+
}) as any);
|
|
229
|
+
|
|
230
|
+
const ora = await import("ora");
|
|
231
|
+
const mockSpinner = {
|
|
232
|
+
start: vi.fn().mockReturnThis(),
|
|
233
|
+
succeed: vi.fn().mockReturnThis(),
|
|
234
|
+
fail: vi.fn().mockReturnThis(),
|
|
235
|
+
};
|
|
236
|
+
vi.mocked(ora.default).mockReturnValue(mockSpinner as any);
|
|
237
|
+
|
|
238
|
+
const { execWithSpinner } = await import("../src/utils.js");
|
|
239
|
+
const result = await execWithSpinner(
|
|
240
|
+
'git tag -a "v1.0.0" -m "Release"',
|
|
241
|
+
mockSpinner as any,
|
|
242
|
+
"Tag 创建成功",
|
|
243
|
+
"Tag 创建失败",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(result).toBe(true);
|
|
247
|
+
expect(mockSpinner.succeed).toHaveBeenCalledWith("Tag 创建成功");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("应该在失败时显示错误消息和详细信息", async () => {
|
|
251
|
+
const mockSpawn = vi.mocked(spawn);
|
|
252
|
+
const errorMessage = "fatal: Failed to resolve 'HEAD' as a valid ref.";
|
|
253
|
+
|
|
254
|
+
mockSpawn.mockImplementation((() => {
|
|
255
|
+
return {
|
|
256
|
+
stderr: {
|
|
257
|
+
on: vi.fn((event: string, callback: (data: Buffer) => void) => {
|
|
258
|
+
if (event === "data") {
|
|
259
|
+
setTimeout(() => callback(Buffer.from(errorMessage)), 0);
|
|
260
|
+
}
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
264
|
+
if (event === "close") {
|
|
265
|
+
setTimeout(() => callback(1), 10);
|
|
266
|
+
}
|
|
267
|
+
}),
|
|
268
|
+
} as any;
|
|
269
|
+
}) as any);
|
|
270
|
+
|
|
271
|
+
const ora = await import("ora");
|
|
272
|
+
const mockSpinner = {
|
|
273
|
+
start: vi.fn().mockReturnThis(),
|
|
274
|
+
succeed: vi.fn().mockReturnThis(),
|
|
275
|
+
fail: vi.fn().mockReturnThis(),
|
|
276
|
+
};
|
|
277
|
+
vi.mocked(ora.default).mockReturnValue(mockSpinner as any);
|
|
278
|
+
|
|
279
|
+
// Mock console.log
|
|
280
|
+
const consoleLogSpy = vi
|
|
281
|
+
.spyOn(console, "log")
|
|
282
|
+
.mockImplementation(() => {});
|
|
283
|
+
|
|
284
|
+
const { execWithSpinner } = await import("../src/utils.js");
|
|
285
|
+
const result = await execWithSpinner(
|
|
286
|
+
'git tag -a "v1.0.0" -m "Release"',
|
|
287
|
+
mockSpinner as any,
|
|
288
|
+
"Tag 创建成功",
|
|
289
|
+
"Tag 创建失败",
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(result).toBe(false);
|
|
293
|
+
expect(mockSpinner.fail).toHaveBeenCalledWith("Tag 创建失败");
|
|
294
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
295
|
+
expect.stringContaining(errorMessage),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
consoleLogSpy.mockRestore();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("实际命令场景测试", () => {
|
|
303
|
+
it("tag 命令:创建带特殊字符的 tag", async () => {
|
|
304
|
+
const mockSpawn = vi.mocked(spawn);
|
|
305
|
+
mockSpawn.mockImplementation((() => {
|
|
306
|
+
return {
|
|
307
|
+
stderr: { on: vi.fn() },
|
|
308
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
309
|
+
if (event === "close") setTimeout(() => callback(0), 0);
|
|
310
|
+
}),
|
|
311
|
+
} as any;
|
|
312
|
+
}) as any);
|
|
313
|
+
|
|
314
|
+
const { execAsync } = await import("../src/utils.js");
|
|
315
|
+
|
|
316
|
+
// 测试各种特殊情况
|
|
317
|
+
const testCases = [
|
|
318
|
+
'git tag -a "v1.0.0-beta.1" -m "Release v1.0.0-beta.1"',
|
|
319
|
+
'git tag -a "v1.0.0-rc.1" -m "Release v1.0.0-rc.1"',
|
|
320
|
+
'git tag -a "release/2024-01-20" -m "Release 2024-01-20"',
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const cmd of testCases) {
|
|
324
|
+
const result = await execAsync(cmd);
|
|
325
|
+
expect(result.success).toBe(true);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("branch 命令:删除带特殊字符的分支", async () => {
|
|
330
|
+
const mockSpawn = vi.mocked(spawn);
|
|
331
|
+
mockSpawn.mockImplementation((() => {
|
|
332
|
+
return {
|
|
333
|
+
stderr: { on: vi.fn() },
|
|
334
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
335
|
+
if (event === "close") setTimeout(() => callback(0), 0);
|
|
336
|
+
}),
|
|
337
|
+
} as any;
|
|
338
|
+
}) as any);
|
|
339
|
+
|
|
340
|
+
const { execAsync } = await import("../src/utils.js");
|
|
341
|
+
|
|
342
|
+
const testCases = [
|
|
343
|
+
'git branch -D "feature/20240120-123-add-feature"',
|
|
344
|
+
'git push origin --delete "feature/20240120-123-add-feature"',
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
for (const cmd of testCases) {
|
|
348
|
+
const result = await execAsync(cmd);
|
|
349
|
+
expect(result.success).toBe(true);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("stash 命令:创建带特殊字符的 stash", async () => {
|
|
354
|
+
const mockSpawn = vi.mocked(spawn);
|
|
355
|
+
mockSpawn.mockImplementation((() => {
|
|
356
|
+
return {
|
|
357
|
+
stderr: { on: vi.fn() },
|
|
358
|
+
on: vi.fn((event: string, callback: (code: number) => void) => {
|
|
359
|
+
if (event === "close") setTimeout(() => callback(0), 0);
|
|
360
|
+
}),
|
|
361
|
+
} as any;
|
|
362
|
+
}) as any);
|
|
363
|
+
|
|
364
|
+
const { execAsync } = await import("../src/utils.js");
|
|
365
|
+
|
|
366
|
+
const testCases = [
|
|
367
|
+
'git stash push -m "临时保存:修复登录bug"',
|
|
368
|
+
'git stash push -m "WIP: 添加\\"新功能\\""',
|
|
369
|
+
'git stash branch "feature/from-stash" stash@{0}',
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
for (const cmd of testCases) {
|
|
373
|
+
const result = await execAsync(cmd);
|
|
374
|
+
expect(result.success).toBe(true);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|