@zjex/git-workflow 0.3.8 → 0.3.10

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.
@@ -253,3 +253,119 @@ describe("Branch 功能测试", () => {
253
253
  });
254
254
  });
255
255
  });
256
+
257
+ describe("描述必填配置", () => {
258
+ it("featureRequireDescription 为 true 时描述不能为空", () => {
259
+ const featureRequireDescription = true;
260
+ const description = "";
261
+
262
+ if (featureRequireDescription && !description) {
263
+ expect(description).toBe("");
264
+ }
265
+ });
266
+
267
+ it("featureRequireDescription 为 false 时描述可以为空", () => {
268
+ const featureRequireDescription = false;
269
+ const description = "";
270
+
271
+ if (!featureRequireDescription) {
272
+ expect(description).toBe("");
273
+ }
274
+ });
275
+
276
+ it("hotfixRequireDescription 为 true 时描述不能为空", () => {
277
+ const hotfixRequireDescription = true;
278
+ const description = "";
279
+
280
+ if (hotfixRequireDescription && !description) {
281
+ expect(description).toBe("");
282
+ }
283
+ });
284
+
285
+ it("hotfixRequireDescription 为 false 时描述可以为空", () => {
286
+ const hotfixRequireDescription = false;
287
+ const description = "";
288
+
289
+ if (!hotfixRequireDescription) {
290
+ expect(description).toBe("");
291
+ }
292
+ });
293
+
294
+ it("应该生成只有 ID 的 feature 分支名(描述为空)", () => {
295
+ const prefix = "feature";
296
+ const id = "PROJ-123";
297
+ const description = "";
298
+ const branchName = id ? `${prefix}/${TODAY}-${id}` : `${prefix}/${TODAY}`;
299
+
300
+ expect(branchName).toMatch(/^feature\/\d{8}-PROJ-123$/);
301
+ });
302
+
303
+ it("应该生成只有描述的 feature 分支名(ID 为空)", () => {
304
+ const prefix = "feature";
305
+ const id = "";
306
+ const description = "add-login";
307
+ const branchName = description
308
+ ? `${prefix}/${TODAY}-${description}`
309
+ : `${prefix}/${TODAY}`;
310
+
311
+ expect(branchName).toMatch(/^feature\/\d{8}-add-login$/);
312
+ });
313
+
314
+ it("应该生成只有日期的 feature 分支名(ID 和描述都为空)", () => {
315
+ const prefix = "feature";
316
+ const id = "";
317
+ const description = "";
318
+ const branchName = `${prefix}/${TODAY}`;
319
+
320
+ expect(branchName).toMatch(/^feature\/\d{8}$/);
321
+ });
322
+
323
+ it("应该生成只有 ID 的 hotfix 分支名(描述为空)", () => {
324
+ const prefix = "hotfix";
325
+ const id = "BUG-456";
326
+ const description = "";
327
+ const branchName = id ? `${prefix}/${TODAY}-${id}` : `${prefix}/${TODAY}`;
328
+
329
+ expect(branchName).toMatch(/^hotfix\/\d{8}-BUG-456$/);
330
+ });
331
+
332
+ it("应该生成只有描述的 hotfix 分支名(ID 为空)", () => {
333
+ const prefix = "hotfix";
334
+ const id = "";
335
+ const description = "fix-crash";
336
+ const branchName = description
337
+ ? `${prefix}/${TODAY}-${description}`
338
+ : `${prefix}/${TODAY}`;
339
+
340
+ expect(branchName).toMatch(/^hotfix\/\d{8}-fix-crash$/);
341
+ });
342
+
343
+ it("应该生成只有日期的 hotfix 分支名(ID 和描述都为空)", () => {
344
+ const prefix = "hotfix";
345
+ const id = "";
346
+ const description = "";
347
+ const branchName = `${prefix}/${TODAY}`;
348
+
349
+ expect(branchName).toMatch(/^hotfix\/\d{8}$/);
350
+ });
351
+
352
+ it("feature 和 hotfix 可以有不同的描述必填配置", () => {
353
+ const featureRequireDescription = true;
354
+ const hotfixRequireDescription = false;
355
+
356
+ expect(featureRequireDescription).toBe(true);
357
+ expect(hotfixRequireDescription).toBe(false);
358
+ expect(featureRequireDescription).not.toBe(hotfixRequireDescription);
359
+ });
360
+
361
+ it("描述必填配置默认应该为 false", () => {
362
+ const featureRequireDescription = undefined;
363
+ const hotfixRequireDescription = undefined;
364
+
365
+ const featureRequired = featureRequireDescription ?? false;
366
+ const hotfixRequired = hotfixRequireDescription ?? false;
367
+
368
+ expect(featureRequired).toBe(false);
369
+ expect(hotfixRequired).toBe(false);
370
+ });
371
+ });
@@ -0,0 +1,293 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { existsSync, unlinkSync, readdirSync, writeFileSync } from "fs";
3
+ import { homedir, tmpdir } from "os";
4
+ import { join } from "path";
5
+
6
+ // Mock 所有外部依赖
7
+ vi.mock("fs", () => ({
8
+ existsSync: vi.fn(),
9
+ unlinkSync: vi.fn(),
10
+ readdirSync: vi.fn(),
11
+ writeFileSync: vi.fn(),
12
+ readFileSync: vi.fn(),
13
+ }));
14
+
15
+ vi.mock("os", () => ({
16
+ homedir: vi.fn(),
17
+ tmpdir: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("path", () => ({
21
+ join: vi.fn((...args) => args.join("/")),
22
+ }));
23
+
24
+ vi.mock("@inquirer/prompts", () => ({
25
+ select: vi.fn(),
26
+ }));
27
+
28
+ vi.mock("../src/update-notifier.js", () => ({
29
+ clearUpdateCache: vi.fn(),
30
+ }));
31
+
32
+ describe("Clean 命令测试", () => {
33
+ const mockExistsSync = vi.mocked(existsSync);
34
+ const mockUnlinkSync = vi.mocked(unlinkSync);
35
+ const mockReaddirSync = vi.mocked(readdirSync);
36
+ const mockHomedir = vi.mocked(homedir);
37
+ const mockTmpdir = vi.mocked(tmpdir);
38
+
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ mockHomedir.mockReturnValue("/home/user");
42
+ mockTmpdir.mockReturnValue("/tmp");
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ describe("清理更新缓存", () => {
50
+ it("应该清理更新缓存文件", async () => {
51
+ const { clearUpdateCache } = await import("../src/update-notifier.js");
52
+
53
+ clearUpdateCache();
54
+
55
+ expect(clearUpdateCache).toHaveBeenCalled();
56
+ });
57
+ });
58
+
59
+ describe("清理全局配置文件", () => {
60
+ it("没有全局配置文件时不应该询问", async () => {
61
+ mockExistsSync.mockReturnValue(false);
62
+ mockReaddirSync.mockReturnValue([]);
63
+
64
+ const { select } = await import("@inquirer/prompts");
65
+
66
+ expect(mockExistsSync).not.toHaveBeenCalledWith("/home/user/.gwrc.json");
67
+ });
68
+
69
+ it("有全局配置文件时应该询问是否删除", async () => {
70
+ const globalConfig = "/home/user/.gwrc.json";
71
+ mockExistsSync.mockReturnValue(true);
72
+ mockReaddirSync.mockReturnValue([]);
73
+
74
+ const { select } = await import("@inquirer/prompts");
75
+ vi.mocked(select).mockResolvedValue(false);
76
+
77
+ // 模拟检查全局配置文件
78
+ const hasGlobalConfig = mockExistsSync(globalConfig);
79
+
80
+ expect(hasGlobalConfig).toBe(true);
81
+ });
82
+
83
+ it("选择删除时应该删除全局配置文件", async () => {
84
+ const globalConfig = "/home/user/.gwrc.json";
85
+ mockExistsSync.mockReturnValue(true);
86
+ mockReaddirSync.mockReturnValue([]);
87
+
88
+ const { select } = await import("@inquirer/prompts");
89
+ vi.mocked(select).mockResolvedValue(true);
90
+
91
+ // 模拟删除操作
92
+ if (mockExistsSync(globalConfig)) {
93
+ mockUnlinkSync(globalConfig);
94
+ }
95
+
96
+ expect(mockUnlinkSync).toHaveBeenCalledWith(globalConfig);
97
+ });
98
+
99
+ it("选择不删除时应该保留全局配置文件", async () => {
100
+ const globalConfig = "/home/user/.gwrc.json";
101
+ mockExistsSync.mockReturnValue(true);
102
+ mockReaddirSync.mockReturnValue([]);
103
+
104
+ const { select } = await import("@inquirer/prompts");
105
+ vi.mocked(select).mockResolvedValue(false);
106
+
107
+ // 模拟不删除的情况
108
+ const shouldDelete = await vi.mocked(select)();
109
+
110
+ if (!shouldDelete) {
111
+ expect(mockUnlinkSync).not.toHaveBeenCalledWith(globalConfig);
112
+ }
113
+ });
114
+ });
115
+
116
+ describe("清理临时 commit 文件", () => {
117
+ it("应该清理所有临时 commit 文件", () => {
118
+ mockReaddirSync.mockReturnValue([
119
+ ".gw-commit-msg-123456",
120
+ ".gw-commit-msg-789012",
121
+ "other-file.txt",
122
+ ] as any);
123
+
124
+ const tmpDir = mockTmpdir();
125
+ const files = mockReaddirSync(tmpDir);
126
+ const gwTmpFiles = files.filter((f: string) =>
127
+ f.startsWith(".gw-commit-msg-")
128
+ );
129
+
130
+ expect(gwTmpFiles).toHaveLength(2);
131
+ expect(gwTmpFiles).toContain(".gw-commit-msg-123456");
132
+ expect(gwTmpFiles).toContain(".gw-commit-msg-789012");
133
+ });
134
+
135
+ it("应该忽略非 gw 临时文件", () => {
136
+ mockReaddirSync.mockReturnValue([
137
+ ".gw-commit-msg-123456",
138
+ "other-temp-file",
139
+ ".other-file",
140
+ ] as any);
141
+
142
+ const tmpDir = mockTmpdir();
143
+ const files = mockReaddirSync(tmpDir);
144
+ const gwTmpFiles = files.filter((f: string) =>
145
+ f.startsWith(".gw-commit-msg-")
146
+ );
147
+
148
+ expect(gwTmpFiles).toHaveLength(1);
149
+ expect(gwTmpFiles).toContain(".gw-commit-msg-123456");
150
+ });
151
+
152
+ it("没有临时文件时不应该报错", () => {
153
+ mockReaddirSync.mockReturnValue([]);
154
+
155
+ const tmpDir = mockTmpdir();
156
+ const files = mockReaddirSync(tmpDir);
157
+ const gwTmpFiles = files.filter((f: string) =>
158
+ f.startsWith(".gw-commit-msg-")
159
+ );
160
+
161
+ expect(gwTmpFiles).toHaveLength(0);
162
+ });
163
+
164
+ it("应该删除找到的临时文件", () => {
165
+ mockReaddirSync.mockReturnValue([
166
+ ".gw-commit-msg-123456",
167
+ ".gw-commit-msg-789012",
168
+ ] as any);
169
+
170
+ const tmpDir = mockTmpdir();
171
+ const files = mockReaddirSync(tmpDir);
172
+ const gwTmpFiles = files.filter((f: string) =>
173
+ f.startsWith(".gw-commit-msg-")
174
+ );
175
+
176
+ gwTmpFiles.forEach((file: string) => {
177
+ mockUnlinkSync(join(tmpDir, file));
178
+ });
179
+
180
+ expect(mockUnlinkSync).toHaveBeenCalledTimes(2);
181
+ expect(mockUnlinkSync).toHaveBeenCalledWith("/tmp/.gw-commit-msg-123456");
182
+ expect(mockUnlinkSync).toHaveBeenCalledWith("/tmp/.gw-commit-msg-789012");
183
+ });
184
+ });
185
+
186
+ describe("清理统计", () => {
187
+ it("应该正确统计清理的文件数量", () => {
188
+ let cleanedCount = 0;
189
+
190
+ // 清理更新缓存
191
+ cleanedCount++;
192
+
193
+ // 清理全局配置
194
+ mockExistsSync.mockReturnValue(true);
195
+ if (mockExistsSync("/home/user/.gwrc.json")) {
196
+ cleanedCount++;
197
+ }
198
+
199
+ // 清理临时文件
200
+ mockReaddirSync.mockReturnValue([
201
+ ".gw-commit-msg-123456",
202
+ ".gw-commit-msg-789012",
203
+ ] as any);
204
+ const gwTmpFiles = mockReaddirSync("/tmp").filter((f: string) =>
205
+ f.startsWith(".gw-commit-msg-")
206
+ );
207
+ cleanedCount += gwTmpFiles.length;
208
+
209
+ expect(cleanedCount).toBe(4); // 1 缓存 + 1 配置 + 2 临时文件
210
+ });
211
+
212
+ it("没有全局配置时应该统计正确", () => {
213
+ let cleanedCount = 0;
214
+
215
+ // 清理更新缓存
216
+ cleanedCount++;
217
+
218
+ // 没有全局配置
219
+ mockExistsSync.mockReturnValue(false);
220
+
221
+ // 清理临时文件
222
+ mockReaddirSync.mockReturnValue([".gw-commit-msg-123456"] as any);
223
+ const gwTmpFiles = mockReaddirSync("/tmp").filter((f: string) =>
224
+ f.startsWith(".gw-commit-msg-")
225
+ );
226
+ cleanedCount += gwTmpFiles.length;
227
+
228
+ expect(cleanedCount).toBe(2); // 1 缓存 + 1 临时文件
229
+ });
230
+
231
+ it("没有任何文件时应该只清理缓存", () => {
232
+ let cleanedCount = 0;
233
+
234
+ // 清理更新缓存
235
+ cleanedCount++;
236
+
237
+ // 没有全局配置
238
+ mockExistsSync.mockReturnValue(false);
239
+
240
+ // 没有临时文件
241
+ mockReaddirSync.mockReturnValue([]);
242
+
243
+ expect(cleanedCount).toBe(1); // 只有缓存
244
+ });
245
+ });
246
+
247
+ describe("错误处理", () => {
248
+ it("删除文件失败时应该静默处理", () => {
249
+ mockUnlinkSync.mockImplementation(() => {
250
+ throw new Error("Permission denied");
251
+ });
252
+
253
+ expect(() => {
254
+ try {
255
+ mockUnlinkSync("/home/user/.gwrc.json");
256
+ } catch {
257
+ // 静默失败
258
+ }
259
+ }).not.toThrow();
260
+ });
261
+
262
+ it("读取目录失败时应该静默处理", () => {
263
+ mockReaddirSync.mockImplementation(() => {
264
+ throw new Error("Directory not found");
265
+ });
266
+
267
+ expect(() => {
268
+ try {
269
+ mockReaddirSync("/tmp");
270
+ } catch {
271
+ // 静默失败
272
+ }
273
+ }).not.toThrow();
274
+ });
275
+ });
276
+
277
+ describe("文件路径", () => {
278
+ it("应该使用正确的全局配置路径", () => {
279
+ const globalConfig = join(mockHomedir(), ".gwrc.json");
280
+ expect(globalConfig).toBe("/home/user/.gwrc.json");
281
+ });
282
+
283
+ it("应该使用正确的临时目录路径", () => {
284
+ const tmpDir = mockTmpdir();
285
+ expect(tmpDir).toBe("/tmp");
286
+ });
287
+
288
+ it("应该正确拼接临时文件路径", () => {
289
+ const tmpFile = join(mockTmpdir(), ".gw-commit-msg-123456");
290
+ expect(tmpFile).toBe("/tmp/.gw-commit-msg-123456");
291
+ });
292
+ });
293
+ });
@@ -1,4 +1,10 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { execSync } from "child_process";
3
+
4
+ // Mock child_process
5
+ vi.mock("child_process", () => ({
6
+ execSync: vi.fn(),
7
+ }));
2
8
 
3
9
  describe("Commit 功能测试", () => {
4
10
  describe("提交类型", () => {
@@ -82,4 +88,183 @@ describe("Commit 功能测试", () => {
82
88
  expect(extraSpace).toBe("");
83
89
  });
84
90
  });
91
+
92
+ describe("自动暂存功能", () => {
93
+ beforeEach(() => {
94
+ vi.clearAllMocks();
95
+ });
96
+
97
+ afterEach(() => {
98
+ vi.restoreAllMocks();
99
+ });
100
+
101
+ it("autoStage 为 true 时应该执行 git add -A", () => {
102
+ const autoStage = true;
103
+
104
+ if (autoStage) {
105
+ execSync("git add -A", { stdio: "pipe" });
106
+ }
107
+
108
+ expect(execSync).toHaveBeenCalledWith("git add -A", { stdio: "pipe" });
109
+ });
110
+
111
+ it("autoStage 为 false 时不应该自动执行 git add -A", () => {
112
+ const autoStage = false;
113
+
114
+ if (autoStage) {
115
+ execSync("git add -A", { stdio: "pipe" });
116
+ }
117
+
118
+ expect(execSync).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("提交前应该再次执行 git add -A 确保文件被暂存", () => {
122
+ const autoStage = true;
123
+
124
+ // 模拟提交前的暂存操作
125
+ if (autoStage) {
126
+ execSync("git add -A", { stdio: "pipe" });
127
+ }
128
+
129
+ // 模拟提交前再次暂存
130
+ if (autoStage) {
131
+ execSync("git add -A", { stdio: "pipe" });
132
+ }
133
+
134
+ // 应该被调用两次
135
+ expect(execSync).toHaveBeenCalledTimes(2);
136
+ expect(execSync).toHaveBeenCalledWith("git add -A", { stdio: "pipe" });
137
+ });
138
+
139
+ it("默认 autoStage 应该为 true", () => {
140
+ const config = {};
141
+ const autoStage = (config as any).autoStage ?? true;
142
+
143
+ expect(autoStage).toBe(true);
144
+ });
145
+
146
+ it("配置 autoStage 为 false 时应该覆盖默认值", () => {
147
+ const config = { autoStage: false };
148
+ const autoStage = config.autoStage ?? true;
149
+
150
+ expect(autoStage).toBe(false);
151
+ });
152
+ });
153
+
154
+ describe("Git 状态解析", () => {
155
+ it("应该正确解析已暂存的文件", () => {
156
+ // 模拟 git status --porcelain 输出
157
+ // M = 已暂存的修改
158
+ // A = 已暂存的新文件
159
+ const output = "M src/index.ts\nA src/new.ts";
160
+ const lines = output.split("\n");
161
+
162
+ const staged: { status: string; file: string }[] = [];
163
+
164
+ for (const line of lines) {
165
+ if (!line) continue;
166
+ const indexStatus = line[0];
167
+ const file = line.slice(3);
168
+
169
+ if (indexStatus !== " " && indexStatus !== "?") {
170
+ staged.push({ status: indexStatus, file });
171
+ }
172
+ }
173
+
174
+ expect(staged).toHaveLength(2);
175
+ expect(staged[0]).toEqual({ status: "M", file: "src/index.ts" });
176
+ expect(staged[1]).toEqual({ status: "A", file: "src/new.ts" });
177
+ });
178
+
179
+ it("应该正确解析未暂存的文件", () => {
180
+ // 模拟 git status --porcelain 输出
181
+ // " M" = 未暂存的修改
182
+ // "??" = 未跟踪的文件
183
+ const output = " M src/modified.ts\n?? src/untracked.ts";
184
+ const lines = output.split("\n");
185
+
186
+ const unstaged: { status: string; file: string }[] = [];
187
+
188
+ for (const line of lines) {
189
+ if (!line) continue;
190
+ const indexStatus = line[0];
191
+ const workTreeStatus = line[1];
192
+ const file = line.slice(3);
193
+
194
+ if (workTreeStatus !== " " || indexStatus === "?") {
195
+ const status = indexStatus === "?" ? "?" : workTreeStatus;
196
+ unstaged.push({ status, file });
197
+ }
198
+ }
199
+
200
+ expect(unstaged).toHaveLength(2);
201
+ expect(unstaged[0]).toEqual({ status: "M", file: "src/modified.ts" });
202
+ expect(unstaged[1]).toEqual({ status: "?", file: "src/untracked.ts" });
203
+ });
204
+
205
+ it("空输出应该返回空数组", () => {
206
+ const output = "";
207
+ const staged: any[] = [];
208
+ const unstaged: any[] = [];
209
+
210
+ if (!output) {
211
+ expect(staged).toHaveLength(0);
212
+ expect(unstaged).toHaveLength(0);
213
+ }
214
+ });
215
+
216
+ it("应该正确处理同时有暂存和未暂存状态的文件", () => {
217
+ // "MM" = 已暂存且有新的未暂存修改
218
+ const output = "MM src/both.ts";
219
+ const lines = output.split("\n");
220
+
221
+ const staged: { status: string; file: string }[] = [];
222
+ const unstaged: { status: string; file: string }[] = [];
223
+
224
+ for (const line of lines) {
225
+ if (!line) continue;
226
+ const indexStatus = line[0];
227
+ const workTreeStatus = line[1];
228
+ const file = line.slice(3);
229
+
230
+ if (indexStatus !== " " && indexStatus !== "?") {
231
+ staged.push({ status: indexStatus, file });
232
+ }
233
+
234
+ if (workTreeStatus !== " " || indexStatus === "?") {
235
+ const status = indexStatus === "?" ? "?" : workTreeStatus;
236
+ unstaged.push({ status, file });
237
+ }
238
+ }
239
+
240
+ expect(staged).toHaveLength(1);
241
+ expect(unstaged).toHaveLength(1);
242
+ expect(staged[0]).toEqual({ status: "M", file: "src/both.ts" });
243
+ expect(unstaged[0]).toEqual({ status: "M", file: "src/both.ts" });
244
+ });
245
+ });
246
+
247
+ describe("临时文件提交", () => {
248
+ it("应该使用临时文件传递多行 commit message", () => {
249
+ const message = "feat: 新功能\n\n- 详细描述1\n- 详细描述2";
250
+ const lines = message.split("\n");
251
+
252
+ expect(lines).toHaveLength(4);
253
+ expect(lines[0]).toBe("feat: 新功能");
254
+ expect(lines[1]).toBe("");
255
+ expect(lines[2]).toBe("- 详细描述1");
256
+ expect(lines[3]).toBe("- 详细描述2");
257
+ });
258
+
259
+ it("临时文件名应该包含时间戳避免冲突", () => {
260
+ const timestamp1 = Date.now();
261
+ const tmpFile1 = `.gw-commit-msg-${timestamp1}`;
262
+
263
+ // 模拟短暂延迟
264
+ const timestamp2 = timestamp1 + 1;
265
+ const tmpFile2 = `.gw-commit-msg-${timestamp2}`;
266
+
267
+ expect(tmpFile1).not.toBe(tmpFile2);
268
+ });
269
+ });
85
270
  });