@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.
@@ -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
- execSync(`git stash show -p --color=always stash@{${index}}`, {
226
- stdio: "inherit",
227
- });
228
- console.log();
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: "选择分支类型:",
@@ -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 = 'stash@{0}|WIP on main: abc123 fix bug|2 hours ago\nstash@{1}|On feature: def456 add feature|1 day ago';
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 = 'stash@{0}|WIP on main: abc123 fix login bug|2 hours ago';
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 = 'stash@{0}|WIP on main: fix bug|2 hours ago';
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
- "git stash apply stash@{0}",
144
- { stdio: "pipe" }
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 = 'stash@{0}|WIP on main: fix bug|2 hours ago';
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
- "git stash pop stash@{0}",
167
- { stdio: "pipe" }
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 = 'stash@{0}|WIP on main: fix bug|2 hours ago';
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
- "git stash drop stash@{0}",
191
- { stdio: "pipe" }
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 = 'stash@{0}|WIP on main: fix bug|2 hours ago';
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('stash@{0}|WIP on main: fix login issue|just now') // 创建成功后的 stash 列表
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('stash@{0}|WIP on main: add new feature|just now') // 创建成功后的 stash 列表
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('stash@{0}|WIP on main: (no message)|just now') // 创建成功后的 stash 列表
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
- "git stash push",
296
- { stdio: "pipe" }
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("feature/20260111-PROJ-123-fix-login");
322
-
319
+ vi.mocked(getBranchName).mockResolvedValue(
320
+ "feature/20260111-PROJ-123-fix-login"
321
+ );
322
+
323
323
  // 验证 git stash branch 命令格式正确
324
- const expectedCmd = 'git stash branch "feature/20260111-PROJ-123-fix-login" stash@{0}';
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
+ });