@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.
@@ -96,8 +96,25 @@ describe("amend-date command", () => {
96
96
  describe("日期解析", () => {
97
97
  const parseDate = (input: string): Date | null => {
98
98
  const trimmed = input.trim();
99
- const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
100
99
 
100
+ // 尝试匹配完整格式: YYYY-MM-DD HH:mm:ss
101
+ const fullMatch = trimmed.match(
102
+ /^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})$/,
103
+ );
104
+ if (fullMatch) {
105
+ const [, year, month, day, hours, minutes, seconds] = fullMatch;
106
+ return new Date(
107
+ parseInt(year),
108
+ parseInt(month) - 1,
109
+ parseInt(day),
110
+ parseInt(hours),
111
+ parseInt(minutes),
112
+ parseInt(seconds),
113
+ );
114
+ }
115
+
116
+ // 尝试匹配简化格式: YYYY-MM-DD (默认 00:00:00)
117
+ const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
101
118
  if (dateMatch) {
102
119
  const [, year, month, day] = dateMatch;
103
120
  return new Date(
@@ -131,6 +148,33 @@ describe("amend-date command", () => {
131
148
  expect(date?.getDate()).toBe(5);
132
149
  });
133
150
 
151
+ it("should parse full datetime format", () => {
152
+ const date = parseDate("2026-01-19 14:30:45");
153
+ expect(date).toBeTruthy();
154
+ expect(date?.getFullYear()).toBe(2026);
155
+ expect(date?.getMonth()).toBe(0);
156
+ expect(date?.getDate()).toBe(19);
157
+ expect(date?.getHours()).toBe(14);
158
+ expect(date?.getMinutes()).toBe(30);
159
+ expect(date?.getSeconds()).toBe(45);
160
+ });
161
+
162
+ it("should parse midnight time", () => {
163
+ const date = parseDate("2026-01-19 00:00:00");
164
+ expect(date).toBeTruthy();
165
+ expect(date?.getHours()).toBe(0);
166
+ expect(date?.getMinutes()).toBe(0);
167
+ expect(date?.getSeconds()).toBe(0);
168
+ });
169
+
170
+ it("should parse end of day time", () => {
171
+ const date = parseDate("2026-01-19 23:59:59");
172
+ expect(date).toBeTruthy();
173
+ expect(date?.getHours()).toBe(23);
174
+ expect(date?.getMinutes()).toBe(59);
175
+ expect(date?.getSeconds()).toBe(59);
176
+ });
177
+
134
178
  it("should return null for invalid format", () => {
135
179
  expect(parseDate("invalid")).toBeNull();
136
180
  expect(parseDate("2026-1-19")).toBeNull(); // 缺少前导零
@@ -138,9 +182,10 @@ describe("amend-date command", () => {
138
182
  expect(parseDate("19-01-2026")).toBeNull(); // 错误的顺序
139
183
  });
140
184
 
141
- it("should return null for date with time", () => {
142
- expect(parseDate("2026-01-19 14:30:00")).toBeNull();
143
- expect(parseDate("2026-01-19 14:30")).toBeNull();
185
+ it("should return null for incomplete datetime", () => {
186
+ expect(parseDate("2026-01-19 14:30")).toBeNull(); // 缺少秒
187
+ expect(parseDate("2026-01-19 14")).toBeNull(); // 只有小时
188
+ expect(parseDate("2026-01-19 14:30:45:00")).toBeNull(); // 多余部分
144
189
  });
145
190
 
146
191
  it("should handle edge cases", () => {
@@ -149,6 +194,16 @@ describe("amend-date command", () => {
149
194
  expect(parseDate("2026-13-01")).not.toBeNull(); // 月份超出范围,但格式正确
150
195
  expect(parseDate("2026-00-01")).not.toBeNull(); // 月份为0,但格式正确
151
196
  });
197
+
198
+ it("should handle whitespace", () => {
199
+ const date1 = parseDate(" 2026-01-19 ");
200
+ expect(date1).toBeTruthy();
201
+ expect(date1?.getFullYear()).toBe(2026);
202
+
203
+ const date2 = parseDate(" 2026-01-19 14:30:45 ");
204
+ expect(date2).toBeTruthy();
205
+ expect(date2?.getHours()).toBe(14);
206
+ });
152
207
  });
153
208
 
154
209
  describe("修改最新 commit", () => {
@@ -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
+ });