@zjex/git-workflow 0.4.0 → 0.4.2
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 +488 -12
- package/README.md +8 -10
- package/ROADMAP.md +1 -1
- package/dist/index.js +124 -22
- package/docs/.vitepress/config.ts +111 -100
- package/docs/guide/api.md +607 -0
- package/docs/guide/contributing.md +441 -0
- package/docs/guide/development.md +295 -0
- package/docs/guide/team-collaboration.md +538 -0
- package/docs/guide/testing.md +461 -0
- package/package.json +3 -3
- package/scripts/generate-changelog-manual.js +135 -0
- package/src/commands/stash.ts +176 -7
- package/tests/stash.test.ts +161 -89
- package/CODE_DOCUMENTATION.md +0 -169
- package/TESTING.md +0 -436
- package/TEST_COVERAGE_SUMMARY.md +0 -264
package/src/commands/stash.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execSync, spawn } from "child_process";
|
|
2
2
|
import { select, input } from "@inquirer/prompts";
|
|
3
3
|
import ora from "ora";
|
|
4
|
+
import boxen from "boxen";
|
|
4
5
|
import {
|
|
5
6
|
colors,
|
|
6
7
|
theme,
|
|
@@ -222,19 +223,187 @@ function applyStash(index: number, pop: boolean): void {
|
|
|
222
223
|
|
|
223
224
|
async function showDiff(index: number): Promise<void> {
|
|
224
225
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
// 获取差异内容(不使用颜色,我们自己格式化)
|
|
227
|
+
const diffOutput = execOutput(
|
|
228
|
+
`git stash show -p --no-color stash@{${index}}`
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (!diffOutput) {
|
|
232
|
+
console.log(colors.yellow("没有差异内容"));
|
|
233
|
+
await input({
|
|
234
|
+
message: colors.dim("按 Enter 返回菜单..."),
|
|
235
|
+
theme,
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 获取统计信息
|
|
241
|
+
const statsOutput = execOutput(`git stash show --stat stash@{${index}}`);
|
|
242
|
+
|
|
243
|
+
// 解析差异内容,按文件分组
|
|
244
|
+
const files = parseDiffByFile(diffOutput);
|
|
245
|
+
|
|
246
|
+
// 构建完整输出
|
|
247
|
+
let fullOutput = "";
|
|
248
|
+
|
|
249
|
+
// 添加统计信息
|
|
250
|
+
if (statsOutput) {
|
|
251
|
+
const statsBox = boxen(statsOutput, {
|
|
252
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
253
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
254
|
+
borderStyle: "double",
|
|
255
|
+
borderColor: "yellow",
|
|
256
|
+
title: `📊 Stash #${index} 统计`,
|
|
257
|
+
titleAlignment: "center",
|
|
258
|
+
});
|
|
259
|
+
fullOutput += statsBox + "\n";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 为每个文件创建边框
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
const fileContent = formatFileDiff(file);
|
|
265
|
+
const fileBox = boxen(fileContent, {
|
|
266
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
267
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
268
|
+
borderStyle: "round",
|
|
269
|
+
borderColor: "cyan",
|
|
270
|
+
title: `📄 ${file.path}`,
|
|
271
|
+
titleAlignment: "left",
|
|
272
|
+
});
|
|
273
|
+
fullOutput += fileBox + "\n";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 使用 less 分页器显示,等待用户退出
|
|
277
|
+
await startPager(fullOutput);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.log(colors.red("无法显示差异"));
|
|
229
280
|
await input({
|
|
230
281
|
message: colors.dim("按 Enter 返回菜单..."),
|
|
231
282
|
theme,
|
|
232
283
|
});
|
|
233
|
-
} catch {
|
|
234
|
-
console.log(colors.red("无法显示差异"));
|
|
235
284
|
}
|
|
236
285
|
}
|
|
237
286
|
|
|
287
|
+
/**
|
|
288
|
+
* 解析差异内容,按文件分组
|
|
289
|
+
*/
|
|
290
|
+
interface FileDiff {
|
|
291
|
+
path: string;
|
|
292
|
+
lines: string[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseDiffByFile(diffOutput: string): FileDiff[] {
|
|
296
|
+
const files: FileDiff[] = [];
|
|
297
|
+
const lines = diffOutput.split("\n");
|
|
298
|
+
let currentFile: FileDiff | null = null;
|
|
299
|
+
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
// 检测文件头
|
|
302
|
+
if (line.startsWith("diff --git")) {
|
|
303
|
+
// 保存上一个文件
|
|
304
|
+
if (currentFile && currentFile.lines.length > 0) {
|
|
305
|
+
files.push(currentFile);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 提取文件路径
|
|
309
|
+
const match = line.match(/diff --git a\/(.*?) b\/(.*?)$/);
|
|
310
|
+
const path = match ? match[2] : "unknown";
|
|
311
|
+
|
|
312
|
+
currentFile = { path, lines: [] };
|
|
313
|
+
} else if (currentFile) {
|
|
314
|
+
// 跳过 index 和 --- +++ 行
|
|
315
|
+
if (
|
|
316
|
+
line.startsWith("index ") ||
|
|
317
|
+
line.startsWith("--- ") ||
|
|
318
|
+
line.startsWith("+++ ")
|
|
319
|
+
) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
currentFile.lines.push(line);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 保存最后一个文件
|
|
328
|
+
if (currentFile && currentFile.lines.length > 0) {
|
|
329
|
+
files.push(currentFile);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return files;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 格式化文件差异内容
|
|
337
|
+
*/
|
|
338
|
+
function formatFileDiff(file: FileDiff): string {
|
|
339
|
+
const formattedLines: string[] = [];
|
|
340
|
+
|
|
341
|
+
for (const line of file.lines) {
|
|
342
|
+
if (line.startsWith("@@")) {
|
|
343
|
+
// 位置信息 - 使用蓝色
|
|
344
|
+
formattedLines.push(colors.blue(line));
|
|
345
|
+
} else if (line.startsWith("+")) {
|
|
346
|
+
// 新增行 - 使用绿色
|
|
347
|
+
formattedLines.push(colors.green(line));
|
|
348
|
+
} else if (line.startsWith("-")) {
|
|
349
|
+
// 删除行 - 使用红色
|
|
350
|
+
formattedLines.push(colors.red(line));
|
|
351
|
+
} else {
|
|
352
|
+
// 上下文行 - 使用灰色
|
|
353
|
+
formattedLines.push(colors.dim(line));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return formattedLines.join("\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 启动分页器显示内容
|
|
362
|
+
*/
|
|
363
|
+
function startPager(content: string): Promise<void> {
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
const pager = process.env.PAGER || "less";
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
// -R: 支持ANSI颜色代码
|
|
369
|
+
// -S: 不换行长行
|
|
370
|
+
// -F: 如果内容少于一屏则直接退出
|
|
371
|
+
// -X: 不清屏
|
|
372
|
+
// -i: 忽略大小写搜索
|
|
373
|
+
const pagerProcess = spawn(pager, ["-R", "-S", "-F", "-X", "-i"], {
|
|
374
|
+
stdio: ["pipe", "inherit", "inherit"],
|
|
375
|
+
env: { ...process.env, LESS: "-R -S -F -X -i" },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// 处理 stdin 的 EPIPE 错误(当 less 提前退出时)
|
|
379
|
+
pagerProcess.stdin.on("error", (err: NodeJS.ErrnoException) => {
|
|
380
|
+
if (err.code !== "EPIPE") {
|
|
381
|
+
console.error(err);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// 将内容写入分页器
|
|
386
|
+
pagerProcess.stdin.write(content);
|
|
387
|
+
pagerProcess.stdin.end();
|
|
388
|
+
|
|
389
|
+
// 等待分页器退出后返回菜单
|
|
390
|
+
pagerProcess.on("exit", () => {
|
|
391
|
+
resolve();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// 处理错误
|
|
395
|
+
pagerProcess.on("error", () => {
|
|
396
|
+
console.log(content);
|
|
397
|
+
resolve();
|
|
398
|
+
});
|
|
399
|
+
} catch (error) {
|
|
400
|
+
// 如果出错,直接输出内容
|
|
401
|
+
console.log(content);
|
|
402
|
+
resolve();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
238
407
|
async function createBranchFromStash(index: number): Promise<void> {
|
|
239
408
|
const type = await select({
|
|
240
409
|
message: "选择分支类型:",
|
package/tests/stash.test.ts
CHANGED
|
@@ -37,7 +37,7 @@ vi.mock("../src/commands/branch.js", () => ({
|
|
|
37
37
|
|
|
38
38
|
describe("Stash 模块测试", () => {
|
|
39
39
|
const mockExecSync = vi.mocked(execSync);
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
beforeEach(() => {
|
|
42
42
|
vi.clearAllMocks();
|
|
43
43
|
vi.resetModules(); // Reset module cache to avoid state interference
|
|
@@ -52,70 +52,72 @@ describe("Stash 模块测试", () => {
|
|
|
52
52
|
it("应该正确解析空的 stash 列表", async () => {
|
|
53
53
|
const { execOutput } = await import("../src/utils.js");
|
|
54
54
|
vi.mocked(execOutput).mockReturnValue("");
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
const { stash } = await import("../src/commands/stash.js");
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
// Mock select 返回取消
|
|
59
59
|
const { select } = await import("@inquirer/prompts");
|
|
60
60
|
vi.mocked(select).mockResolvedValue(false);
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
await stash();
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
expect(console.log).toHaveBeenCalledWith("没有 stash 记录");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it("应该正确解析 stash 列表", async () => {
|
|
68
|
-
const stashOutput =
|
|
69
|
-
|
|
68
|
+
const stashOutput =
|
|
69
|
+
"stash@{0}|WIP on main: abc123 fix bug|2 hours ago\nstash@{1}|On feature: def456 add feature|1 day ago";
|
|
70
|
+
|
|
70
71
|
const { execOutput } = await import("../src/utils.js");
|
|
71
72
|
vi.mocked(execOutput)
|
|
72
73
|
.mockReturnValueOnce(stashOutput) // git stash list
|
|
73
74
|
.mockReturnValueOnce("file1.js\nfile2.js") // git stash show stash@{0}
|
|
74
75
|
.mockReturnValueOnce("file3.js"); // git stash show stash@{1}
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
const { stash } = await import("../src/commands/stash.js");
|
|
77
|
-
|
|
78
|
+
|
|
78
79
|
// Mock select 返回取消
|
|
79
80
|
const { select } = await import("@inquirer/prompts");
|
|
80
81
|
vi.mocked(select).mockResolvedValue("__cancel__");
|
|
81
|
-
|
|
82
|
+
|
|
82
83
|
await stash();
|
|
83
|
-
|
|
84
|
+
|
|
84
85
|
expect(console.log).toHaveBeenCalledWith("共 2 个 stash:\n");
|
|
85
86
|
});
|
|
86
87
|
|
|
87
88
|
it("应该正确格式化 stash 选项", async () => {
|
|
88
|
-
const stashOutput =
|
|
89
|
-
|
|
89
|
+
const stashOutput =
|
|
90
|
+
"stash@{0}|WIP on main: abc123 fix login bug|2 hours ago";
|
|
91
|
+
|
|
90
92
|
const { execOutput } = await import("../src/utils.js");
|
|
91
93
|
vi.mocked(execOutput)
|
|
92
94
|
.mockReturnValueOnce(stashOutput)
|
|
93
95
|
.mockReturnValueOnce("login.js\nauth.js");
|
|
94
|
-
|
|
96
|
+
|
|
95
97
|
const { stash } = await import("../src/commands/stash.js");
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
const { select } = await import("@inquirer/prompts");
|
|
98
100
|
vi.mocked(select).mockResolvedValue("__cancel__");
|
|
99
|
-
|
|
101
|
+
|
|
100
102
|
await stash();
|
|
101
|
-
|
|
103
|
+
|
|
102
104
|
// 验证 select 被调用时包含正确格式的选项
|
|
103
105
|
expect(select).toHaveBeenCalledWith(
|
|
104
106
|
expect.objectContaining({
|
|
105
107
|
choices: expect.arrayContaining([
|
|
106
108
|
expect.objectContaining({
|
|
107
109
|
name: expect.stringContaining("[0]"),
|
|
108
|
-
value: "0"
|
|
110
|
+
value: "0",
|
|
109
111
|
}),
|
|
110
112
|
expect.objectContaining({
|
|
111
113
|
name: expect.stringContaining("+ 创建新 stash"),
|
|
112
|
-
value: "__new__"
|
|
114
|
+
value: "__new__",
|
|
113
115
|
}),
|
|
114
116
|
expect.objectContaining({
|
|
115
117
|
name: expect.stringContaining("取消"),
|
|
116
|
-
value: "__cancel__"
|
|
117
|
-
})
|
|
118
|
-
])
|
|
118
|
+
value: "__cancel__",
|
|
119
|
+
}),
|
|
120
|
+
]),
|
|
119
121
|
})
|
|
120
122
|
);
|
|
121
123
|
});
|
|
@@ -123,93 +125,90 @@ describe("Stash 模块测试", () => {
|
|
|
123
125
|
|
|
124
126
|
describe("stash 操作", () => {
|
|
125
127
|
it("应该正确应用 stash", async () => {
|
|
126
|
-
const stashOutput =
|
|
127
|
-
|
|
128
|
+
const stashOutput = "stash@{0}|WIP on main: fix bug|2 hours ago";
|
|
129
|
+
|
|
128
130
|
const { execOutput } = await import("../src/utils.js");
|
|
129
131
|
vi.mocked(execOutput)
|
|
130
132
|
.mockReturnValueOnce(stashOutput)
|
|
131
133
|
.mockReturnValueOnce("file1.js");
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
const { stash } = await import("../src/commands/stash.js");
|
|
134
|
-
|
|
136
|
+
|
|
135
137
|
const { select } = await import("@inquirer/prompts");
|
|
136
138
|
vi.mocked(select)
|
|
137
139
|
.mockResolvedValueOnce("0") // 选择 stash
|
|
138
140
|
.mockResolvedValueOnce("apply"); // 选择应用
|
|
139
|
-
|
|
141
|
+
|
|
140
142
|
await stash();
|
|
141
|
-
|
|
142
|
-
expect(mockExecSync).toHaveBeenCalledWith(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
143
|
+
|
|
144
|
+
expect(mockExecSync).toHaveBeenCalledWith("git stash apply stash@{0}", {
|
|
145
|
+
stdio: "pipe",
|
|
146
|
+
});
|
|
146
147
|
});
|
|
147
148
|
|
|
148
149
|
it("应该正确弹出 stash", async () => {
|
|
149
|
-
const stashOutput =
|
|
150
|
-
|
|
150
|
+
const stashOutput = "stash@{0}|WIP on main: fix bug|2 hours ago";
|
|
151
|
+
|
|
151
152
|
const { execOutput } = await import("../src/utils.js");
|
|
152
153
|
vi.mocked(execOutput)
|
|
153
154
|
.mockReturnValueOnce(stashOutput)
|
|
154
155
|
.mockReturnValueOnce("file1.js");
|
|
155
|
-
|
|
156
|
+
|
|
156
157
|
const { stash } = await import("../src/commands/stash.js");
|
|
157
|
-
|
|
158
|
+
|
|
158
159
|
const { select } = await import("@inquirer/prompts");
|
|
159
160
|
vi.mocked(select)
|
|
160
161
|
.mockResolvedValueOnce("0") // 选择 stash
|
|
161
162
|
.mockResolvedValueOnce("pop"); // 选择弹出
|
|
162
|
-
|
|
163
|
+
|
|
163
164
|
await stash();
|
|
164
|
-
|
|
165
|
-
expect(mockExecSync).toHaveBeenCalledWith(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
165
|
+
|
|
166
|
+
expect(mockExecSync).toHaveBeenCalledWith("git stash pop stash@{0}", {
|
|
167
|
+
stdio: "pipe",
|
|
168
|
+
});
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
it("应该正确删除 stash", async () => {
|
|
172
|
-
const stashOutput =
|
|
173
|
-
|
|
172
|
+
const stashOutput = "stash@{0}|WIP on main: fix bug|2 hours ago";
|
|
173
|
+
|
|
174
174
|
const { execOutput } = await import("../src/utils.js");
|
|
175
175
|
vi.mocked(execOutput)
|
|
176
176
|
.mockReturnValueOnce(stashOutput)
|
|
177
177
|
.mockReturnValueOnce("file1.js");
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
const { stash } = await import("../src/commands/stash.js");
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
const { select } = await import("@inquirer/prompts");
|
|
182
182
|
vi.mocked(select)
|
|
183
183
|
.mockResolvedValueOnce("0") // 选择 stash
|
|
184
184
|
.mockResolvedValueOnce("drop") // 选择删除
|
|
185
185
|
.mockResolvedValueOnce(true); // 确认删除
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
await stash();
|
|
188
|
-
|
|
189
|
-
expect(mockExecSync).toHaveBeenCalledWith(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
188
|
+
|
|
189
|
+
expect(mockExecSync).toHaveBeenCalledWith("git stash drop stash@{0}", {
|
|
190
|
+
stdio: "pipe",
|
|
191
|
+
});
|
|
193
192
|
});
|
|
194
193
|
|
|
195
194
|
it("应该在删除时处理用户取消", async () => {
|
|
196
|
-
const stashOutput =
|
|
197
|
-
|
|
195
|
+
const stashOutput = "stash@{0}|WIP on main: fix bug|2 hours ago";
|
|
196
|
+
|
|
198
197
|
const { execOutput } = await import("../src/utils.js");
|
|
199
198
|
vi.mocked(execOutput)
|
|
200
199
|
.mockReturnValueOnce(stashOutput)
|
|
201
200
|
.mockReturnValueOnce("file1.js");
|
|
202
|
-
|
|
201
|
+
|
|
203
202
|
const { stash } = await import("../src/commands/stash.js");
|
|
204
|
-
|
|
203
|
+
|
|
205
204
|
const { select } = await import("@inquirer/prompts");
|
|
206
205
|
vi.mocked(select)
|
|
207
206
|
.mockResolvedValueOnce("0") // 选择 stash
|
|
208
207
|
.mockResolvedValueOnce("drop") // 选择删除
|
|
209
208
|
.mockResolvedValueOnce(false); // 取消删除
|
|
210
|
-
|
|
209
|
+
|
|
211
210
|
await stash();
|
|
212
|
-
|
|
211
|
+
|
|
213
212
|
expect(mockExecSync).not.toHaveBeenCalledWith(
|
|
214
213
|
expect.stringContaining("git stash drop"),
|
|
215
214
|
expect.any(Object)
|
|
@@ -225,20 +224,20 @@ describe("Stash 模块测试", () => {
|
|
|
225
224
|
.mockReturnValueOnce("") // 初始 stash 列表为空
|
|
226
225
|
.mockReturnValueOnce("M file1.js\nA file2.js") // git status (第一次调用)
|
|
227
226
|
.mockReturnValueOnce("M file1.js\nA file2.js") // git status (createStash 内部调用)
|
|
228
|
-
.mockReturnValueOnce(
|
|
227
|
+
.mockReturnValueOnce("stash@{0}|WIP on main: fix login issue|just now") // 创建成功后的 stash 列表
|
|
229
228
|
.mockReturnValueOnce("file1.js\nfile2.js"); // git stash show
|
|
230
|
-
|
|
229
|
+
|
|
231
230
|
const { stash } = await import("../src/commands/stash.js");
|
|
232
|
-
|
|
231
|
+
|
|
233
232
|
const { select, input } = await import("@inquirer/prompts");
|
|
234
233
|
vi.mocked(select)
|
|
235
234
|
.mockResolvedValueOnce(true) // 选择创建 stash
|
|
236
235
|
.mockResolvedValueOnce(false) // 不包含未跟踪文件
|
|
237
236
|
.mockResolvedValueOnce("__cancel__"); // 创建成功后取消
|
|
238
237
|
vi.mocked(input).mockResolvedValue("fix login issue");
|
|
239
|
-
|
|
238
|
+
|
|
240
239
|
await stash();
|
|
241
|
-
|
|
240
|
+
|
|
242
241
|
expect(mockExecSync).toHaveBeenCalledWith(
|
|
243
242
|
'git stash push -m "fix login issue"',
|
|
244
243
|
{ stdio: "pipe" }
|
|
@@ -251,20 +250,20 @@ describe("Stash 模块测试", () => {
|
|
|
251
250
|
.mockReturnValueOnce("") // 初始 stash 列表为空
|
|
252
251
|
.mockReturnValueOnce("M file1.js\n?? file2.js") // git status (第一次调用)
|
|
253
252
|
.mockReturnValueOnce("M file1.js\n?? file2.js") // git status (createStash 内部调用)
|
|
254
|
-
.mockReturnValueOnce(
|
|
253
|
+
.mockReturnValueOnce("stash@{0}|WIP on main: add new feature|just now") // 创建成功后的 stash 列表
|
|
255
254
|
.mockReturnValueOnce("file1.js\nfile2.js"); // git stash show
|
|
256
|
-
|
|
255
|
+
|
|
257
256
|
const { stash } = await import("../src/commands/stash.js");
|
|
258
|
-
|
|
257
|
+
|
|
259
258
|
const { select, input } = await import("@inquirer/prompts");
|
|
260
259
|
vi.mocked(select)
|
|
261
260
|
.mockResolvedValueOnce(true) // 选择创建 stash
|
|
262
261
|
.mockResolvedValueOnce(true) // 包含未跟踪文件
|
|
263
262
|
.mockResolvedValueOnce("__cancel__"); // 创建成功后取消
|
|
264
263
|
vi.mocked(input).mockResolvedValue("add new feature");
|
|
265
|
-
|
|
264
|
+
|
|
266
265
|
await stash();
|
|
267
|
-
|
|
266
|
+
|
|
268
267
|
expect(mockExecSync).toHaveBeenCalledWith(
|
|
269
268
|
'git stash push -u -m "add new feature"',
|
|
270
269
|
{ stdio: "pipe" }
|
|
@@ -277,24 +276,23 @@ describe("Stash 模块测试", () => {
|
|
|
277
276
|
.mockReturnValueOnce("") // 初始 stash 列表为空
|
|
278
277
|
.mockReturnValueOnce("M file1.js") // git status (第一次调用)
|
|
279
278
|
.mockReturnValueOnce("M file1.js") // git status (createStash 内部调用)
|
|
280
|
-
.mockReturnValueOnce(
|
|
279
|
+
.mockReturnValueOnce("stash@{0}|WIP on main: (no message)|just now") // 创建成功后的 stash 列表
|
|
281
280
|
.mockReturnValueOnce("file1.js"); // git stash show
|
|
282
|
-
|
|
281
|
+
|
|
283
282
|
const { stash } = await import("../src/commands/stash.js");
|
|
284
|
-
|
|
283
|
+
|
|
285
284
|
const { select, input } = await import("@inquirer/prompts");
|
|
286
285
|
vi.mocked(select)
|
|
287
286
|
.mockResolvedValueOnce(true) // 选择创建 stash
|
|
288
287
|
.mockResolvedValueOnce(false) // 不包含未跟踪文件
|
|
289
288
|
.mockResolvedValueOnce("__cancel__"); // 创建成功后取消
|
|
290
289
|
vi.mocked(input).mockResolvedValue(""); // 空消息
|
|
291
|
-
|
|
290
|
+
|
|
292
291
|
await stash();
|
|
293
|
-
|
|
294
|
-
expect(mockExecSync).toHaveBeenCalledWith(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
);
|
|
292
|
+
|
|
293
|
+
expect(mockExecSync).toHaveBeenCalledWith("git stash push", {
|
|
294
|
+
stdio: "pipe",
|
|
295
|
+
});
|
|
298
296
|
});
|
|
299
297
|
|
|
300
298
|
it("应该处理没有变更的情况", async () => {
|
|
@@ -302,14 +300,14 @@ describe("Stash 模块测试", () => {
|
|
|
302
300
|
vi.mocked(execOutput)
|
|
303
301
|
.mockReturnValueOnce("") // 初始 stash 列表为空
|
|
304
302
|
.mockReturnValueOnce(""); // git status 为空
|
|
305
|
-
|
|
303
|
+
|
|
306
304
|
const { stash } = await import("../src/commands/stash.js");
|
|
307
|
-
|
|
305
|
+
|
|
308
306
|
const { select } = await import("@inquirer/prompts");
|
|
309
307
|
vi.mocked(select).mockResolvedValueOnce(false); // 选择不创建 stash
|
|
310
|
-
|
|
308
|
+
|
|
311
309
|
await stash();
|
|
312
|
-
|
|
310
|
+
|
|
313
311
|
expect(console.log).toHaveBeenCalledWith("没有 stash 记录");
|
|
314
312
|
});
|
|
315
313
|
});
|
|
@@ -318,10 +316,13 @@ describe("Stash 模块测试", () => {
|
|
|
318
316
|
it("应该正确从 stash 创建 feature 分支", async () => {
|
|
319
317
|
// 简化测试:只验证核心逻辑
|
|
320
318
|
const { getBranchName } = await import("../src/commands/branch.js");
|
|
321
|
-
vi.mocked(getBranchName).mockResolvedValue(
|
|
322
|
-
|
|
319
|
+
vi.mocked(getBranchName).mockResolvedValue(
|
|
320
|
+
"feature/20260111-PROJ-123-fix-login"
|
|
321
|
+
);
|
|
322
|
+
|
|
323
323
|
// 验证 git stash branch 命令格式正确
|
|
324
|
-
const expectedCmd =
|
|
324
|
+
const expectedCmd =
|
|
325
|
+
'git stash branch "feature/20260111-PROJ-123-fix-login" stash@{0}';
|
|
325
326
|
expect(expectedCmd).toContain("git stash branch");
|
|
326
327
|
expect(expectedCmd).toContain("stash@{0}");
|
|
327
328
|
});
|
|
@@ -330,7 +331,7 @@ describe("Stash 模块测试", () => {
|
|
|
330
331
|
// 简化测试:测试取消逻辑
|
|
331
332
|
const { select } = await import("@inquirer/prompts");
|
|
332
333
|
vi.mocked(select).mockResolvedValue("__cancel__");
|
|
333
|
-
|
|
334
|
+
|
|
334
335
|
// 测试取消逻辑不会执行 git 命令
|
|
335
336
|
expect(mockExecSync).not.toHaveBeenCalledWith(
|
|
336
337
|
expect.stringContaining("git stash branch"),
|
|
@@ -348,29 +349,100 @@ describe("Stash 模块测试", () => {
|
|
|
348
349
|
const cmd = "git stash show -p --color=always stash@{0}";
|
|
349
350
|
mockExecSync(cmd, { stdio: "inherit" });
|
|
350
351
|
}).not.toThrow();
|
|
351
|
-
|
|
352
|
+
|
|
352
353
|
// 验证命令被正确调用
|
|
353
354
|
expect(mockExecSync).toHaveBeenCalledWith(
|
|
354
355
|
"git stash show -p --color=always stash@{0}",
|
|
355
356
|
{ stdio: "inherit" }
|
|
356
357
|
);
|
|
357
358
|
});
|
|
359
|
+
|
|
360
|
+
it("应该使用 boxen 格式化差异显示", async () => {
|
|
361
|
+
const diffOutput = `diff --git a/file1.js b/file1.js
|
|
362
|
+
index abc123..def456 100644
|
|
363
|
+
--- a/file1.js
|
|
364
|
+
+++ b/file1.js
|
|
365
|
+
@@ -1,3 +1,4 @@
|
|
366
|
+
function test() {
|
|
367
|
+
+ console.log('new line');
|
|
368
|
+
return true;
|
|
369
|
+
}`;
|
|
370
|
+
|
|
371
|
+
const statsOutput = ` file1.js | 1 +
|
|
372
|
+
1 file changed, 1 insertion(+)`;
|
|
373
|
+
|
|
374
|
+
const { execOutput } = await import("../src/utils.js");
|
|
375
|
+
vi.mocked(execOutput)
|
|
376
|
+
.mockReturnValueOnce(diffOutput) // git stash show -p
|
|
377
|
+
.mockReturnValueOnce(statsOutput); // git stash show --stat
|
|
378
|
+
|
|
379
|
+
// 验证 boxen 会被用于格式化输出
|
|
380
|
+
const boxen = await import("boxen");
|
|
381
|
+
expect(boxen).toBeDefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("应该正确解析多文件差异", async () => {
|
|
385
|
+
const diffOutput = `diff --git a/file1.js b/file1.js
|
|
386
|
+
index abc123..def456 100644
|
|
387
|
+
--- a/file1.js
|
|
388
|
+
+++ b/file1.js
|
|
389
|
+
@@ -1,3 +1,4 @@
|
|
390
|
+
function test() {
|
|
391
|
+
+ console.log('file1');
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
diff --git a/file2.js b/file2.js
|
|
395
|
+
index 111222..333444 100644
|
|
396
|
+
--- a/file2.js
|
|
397
|
+
+++ b/file2.js
|
|
398
|
+
@@ -1,2 +1,3 @@
|
|
399
|
+
function test2() {
|
|
400
|
+
+ console.log('file2');
|
|
401
|
+
return false;
|
|
402
|
+
}`;
|
|
403
|
+
|
|
404
|
+
// 验证差异解析逻辑能处理多个文件
|
|
405
|
+
const files = diffOutput.split("diff --git").filter(Boolean);
|
|
406
|
+
expect(files.length).toBe(2);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it("应该处理没有差异内容的情况", async () => {
|
|
410
|
+
const { execOutput } = await import("../src/utils.js");
|
|
411
|
+
vi.mocked(execOutput).mockReturnValueOnce(""); // 空差异
|
|
412
|
+
|
|
413
|
+
const { input } = await import("@inquirer/prompts");
|
|
414
|
+
vi.mocked(input).mockResolvedValue("");
|
|
415
|
+
|
|
416
|
+
// 验证会显示"没有差异内容"消息
|
|
417
|
+
expect(console.log).toBeDefined();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("应该正确格式化添加和删除的行", async () => {
|
|
421
|
+
const diffLine1 = "+ console.log('added');";
|
|
422
|
+
const diffLine2 = "- console.log('removed');";
|
|
423
|
+
const diffLine3 = " console.log('unchanged');";
|
|
424
|
+
|
|
425
|
+
// 验证颜色格式化逻辑
|
|
426
|
+
expect(diffLine1.startsWith("+")).toBe(true);
|
|
427
|
+
expect(diffLine2.startsWith("-")).toBe(true);
|
|
428
|
+
expect(diffLine3.startsWith(" ")).toBe(true);
|
|
429
|
+
});
|
|
358
430
|
});
|
|
359
431
|
|
|
360
432
|
describe("错误处理", () => {
|
|
361
433
|
it("应该处理 stash 创建失败", async () => {
|
|
362
434
|
// 简化测试:只验证错误处理逻辑存在
|
|
363
435
|
const errorMessage = "Stash failed";
|
|
364
|
-
|
|
436
|
+
|
|
365
437
|
// 验证错误处理机制
|
|
366
438
|
expect(() => {
|
|
367
439
|
throw new Error(errorMessage);
|
|
368
440
|
}).toThrow(errorMessage);
|
|
369
|
-
|
|
441
|
+
|
|
370
442
|
// 验证 git stash push 命令格式正确
|
|
371
443
|
const expectedCmd = 'git stash push -m "test message"';
|
|
372
444
|
expect(expectedCmd).toContain("git stash push");
|
|
373
445
|
expect(expectedCmd).toContain("test message");
|
|
374
446
|
});
|
|
375
447
|
});
|
|
376
|
-
});
|
|
448
|
+
});
|