@zjex/git-workflow 0.4.2 → 0.4.4
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 +26 -0
- package/README.md +1 -1
- package/dist/index.js +580 -178
- package/docs/.vitepress/config.ts +2 -0
- package/docs/commands/amend-date.md +425 -0
- package/docs/commands/amend.md +380 -0
- package/docs/commands/index.md +14 -10
- package/package.json +1 -1
- package/src/commands/amend-date.ts +228 -0
- package/src/commands/amend.ts +189 -0
- package/src/commands/branch.ts +47 -38
- package/src/commands/stash.ts +40 -33
- package/src/commands/tag.ts +113 -73
- package/src/commands/update.ts +77 -44
- package/src/index.ts +39 -4
- package/src/utils.ts +59 -1
- package/tests/amend-date.test.ts +364 -0
- package/tests/amend.test.ts +441 -0
- package/tests/stash.test.ts +70 -75
- package/tests/update.test.ts +125 -112
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
|
|
7
|
+
describe("amend-date command", () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
let originalCwd: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// 保存当前工作目录
|
|
13
|
+
originalCwd = process.cwd();
|
|
14
|
+
|
|
15
|
+
// 创建临时测试目录
|
|
16
|
+
testDir = mkdtempSync(join(tmpdir(), "gw-test-"));
|
|
17
|
+
process.chdir(testDir);
|
|
18
|
+
|
|
19
|
+
// 初始化 git 仓库
|
|
20
|
+
execSync("git init", { stdio: "pipe" });
|
|
21
|
+
execSync('git config user.email "test@example.com"', { stdio: "pipe" });
|
|
22
|
+
execSync('git config user.name "Test User"', { stdio: "pipe" });
|
|
23
|
+
|
|
24
|
+
// 创建初始提交
|
|
25
|
+
execSync("echo 'test' > test.txt", { stdio: "pipe" });
|
|
26
|
+
execSync("git add .", { stdio: "pipe" });
|
|
27
|
+
execSync('git commit -m "Initial commit"', { stdio: "pipe" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
// 恢复工作目录
|
|
32
|
+
process.chdir(originalCwd);
|
|
33
|
+
|
|
34
|
+
// 清理测试目录
|
|
35
|
+
try {
|
|
36
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
37
|
+
} catch {
|
|
38
|
+
// 忽略清理错误
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("基础功能", () => {
|
|
43
|
+
it("should have commits in test repo", () => {
|
|
44
|
+
const log = execSync("git log --oneline", { encoding: "utf-8" });
|
|
45
|
+
expect(log).toContain("Initial commit");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should be able to get commit date", () => {
|
|
49
|
+
const date = execSync('git log -1 --format="%ai"', { encoding: "utf-8" });
|
|
50
|
+
expect(date).toBeTruthy();
|
|
51
|
+
expect(date.trim()).toMatch(/^\d{4}-\d{2}-\d{2}/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should get both author and committer dates", () => {
|
|
55
|
+
const authorDate = execSync('git log -1 --format="%ai"', {
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
}).trim();
|
|
58
|
+
const committerDate = execSync('git log -1 --format="%ci"', {
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
}).trim();
|
|
61
|
+
|
|
62
|
+
expect(authorDate).toBeTruthy();
|
|
63
|
+
expect(committerDate).toBeTruthy();
|
|
64
|
+
expect(authorDate).toMatch(/^\d{4}-\d{2}-\d{2}/);
|
|
65
|
+
expect(committerDate).toMatch(/^\d{4}-\d{2}-\d{2}/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("日期格式化", () => {
|
|
70
|
+
it("should format date correctly", () => {
|
|
71
|
+
const date = new Date("2026-01-19T00:00:00");
|
|
72
|
+
const year = date.getFullYear();
|
|
73
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
74
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
75
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
76
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
77
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
78
|
+
const formatted = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
79
|
+
|
|
80
|
+
expect(formatted).toBe("2026-01-19 00:00:00");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should format date with leading zeros", () => {
|
|
84
|
+
const date = new Date("2026-01-05T00:00:00");
|
|
85
|
+
const year = date.getFullYear();
|
|
86
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
87
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
88
|
+
const formatted = `${year}-${month}-${day}`;
|
|
89
|
+
|
|
90
|
+
expect(formatted).toBe("2026-01-05");
|
|
91
|
+
expect(month).toBe("01");
|
|
92
|
+
expect(day).toBe("05");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("日期解析", () => {
|
|
97
|
+
const parseDate = (input: string): Date | null => {
|
|
98
|
+
const trimmed = input.trim();
|
|
99
|
+
const dateMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
100
|
+
|
|
101
|
+
if (dateMatch) {
|
|
102
|
+
const [, year, month, day] = dateMatch;
|
|
103
|
+
return new Date(
|
|
104
|
+
parseInt(year),
|
|
105
|
+
parseInt(month) - 1,
|
|
106
|
+
parseInt(day),
|
|
107
|
+
0,
|
|
108
|
+
0,
|
|
109
|
+
0,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
it("should parse valid date format", () => {
|
|
117
|
+
const date = parseDate("2026-01-19");
|
|
118
|
+
expect(date).toBeTruthy();
|
|
119
|
+
expect(date?.getFullYear()).toBe(2026);
|
|
120
|
+
expect(date?.getMonth()).toBe(0); // 0-based
|
|
121
|
+
expect(date?.getDate()).toBe(19);
|
|
122
|
+
expect(date?.getHours()).toBe(0);
|
|
123
|
+
expect(date?.getMinutes()).toBe(0);
|
|
124
|
+
expect(date?.getSeconds()).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should parse date with leading zeros", () => {
|
|
128
|
+
const date = parseDate("2026-01-05");
|
|
129
|
+
expect(date).toBeTruthy();
|
|
130
|
+
expect(date?.getMonth()).toBe(0);
|
|
131
|
+
expect(date?.getDate()).toBe(5);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should return null for invalid format", () => {
|
|
135
|
+
expect(parseDate("invalid")).toBeNull();
|
|
136
|
+
expect(parseDate("2026-1-19")).toBeNull(); // 缺少前导零
|
|
137
|
+
expect(parseDate("2026/01/19")).toBeNull(); // 错误的分隔符
|
|
138
|
+
expect(parseDate("19-01-2026")).toBeNull(); // 错误的顺序
|
|
139
|
+
});
|
|
140
|
+
|
|
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();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should handle edge cases", () => {
|
|
147
|
+
expect(parseDate("")).toBeNull();
|
|
148
|
+
expect(parseDate(" ")).toBeNull();
|
|
149
|
+
expect(parseDate("2026-13-01")).not.toBeNull(); // 月份超出范围,但格式正确
|
|
150
|
+
expect(parseDate("2026-00-01")).not.toBeNull(); // 月份为0,但格式正确
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("修改最新 commit", () => {
|
|
155
|
+
it("should be able to amend latest commit date", () => {
|
|
156
|
+
const newDate = "2026-01-15 00:00:00";
|
|
157
|
+
|
|
158
|
+
// 修改最新 commit 的时间
|
|
159
|
+
execSync(
|
|
160
|
+
`GIT_AUTHOR_DATE="${newDate}" GIT_COMMITTER_DATE="${newDate}" git commit --amend --no-edit --reset-author`,
|
|
161
|
+
{ stdio: "pipe" },
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// 验证时间已修改
|
|
165
|
+
const authorDate = execSync(
|
|
166
|
+
'git log -1 --format="%ad" --date=format:"%Y-%m-%d"',
|
|
167
|
+
{ encoding: "utf-8" },
|
|
168
|
+
).trim();
|
|
169
|
+
|
|
170
|
+
expect(authorDate).toBe("2026-01-15");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("should modify both author and committer dates", () => {
|
|
174
|
+
const newDate = "2026-01-15 00:00:00";
|
|
175
|
+
|
|
176
|
+
execSync(
|
|
177
|
+
`GIT_AUTHOR_DATE="${newDate}" GIT_COMMITTER_DATE="${newDate}" git commit --amend --no-edit --reset-author`,
|
|
178
|
+
{ stdio: "pipe" },
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const authorDate = execSync(
|
|
182
|
+
'git log -1 --format="%ad" --date=format:"%Y-%m-%d"',
|
|
183
|
+
{ encoding: "utf-8" },
|
|
184
|
+
).trim();
|
|
185
|
+
|
|
186
|
+
const committerDate = execSync(
|
|
187
|
+
'git log -1 --format="%cd" --date=format:"%Y-%m-%d"',
|
|
188
|
+
{ encoding: "utf-8" },
|
|
189
|
+
).trim();
|
|
190
|
+
|
|
191
|
+
expect(authorDate).toBe("2026-01-15");
|
|
192
|
+
expect(committerDate).toBe("2026-01-15");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should change commit hash when amending", () => {
|
|
196
|
+
const beforeHash = execSync("git rev-parse HEAD", {
|
|
197
|
+
encoding: "utf-8",
|
|
198
|
+
}).trim();
|
|
199
|
+
|
|
200
|
+
const newDate = "2026-01-15 00:00:00";
|
|
201
|
+
execSync(
|
|
202
|
+
`GIT_AUTHOR_DATE="${newDate}" GIT_COMMITTER_DATE="${newDate}" git commit --amend --no-edit --reset-author`,
|
|
203
|
+
{ stdio: "pipe" },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const afterHash = execSync("git rev-parse HEAD", {
|
|
207
|
+
encoding: "utf-8",
|
|
208
|
+
}).trim();
|
|
209
|
+
|
|
210
|
+
expect(beforeHash).not.toBe(afterHash);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("获取 commit 信息", () => {
|
|
215
|
+
it("should get commit by full hash", () => {
|
|
216
|
+
const hash = execSync("git rev-parse HEAD", {
|
|
217
|
+
encoding: "utf-8",
|
|
218
|
+
}).trim();
|
|
219
|
+
|
|
220
|
+
const message = execSync(`git log -1 ${hash} --format="%s"`, {
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
}).trim();
|
|
223
|
+
|
|
224
|
+
expect(message).toBe("Initial commit");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should get commit by short hash", () => {
|
|
228
|
+
const shortHash = execSync("git rev-parse --short HEAD", {
|
|
229
|
+
encoding: "utf-8",
|
|
230
|
+
}).trim();
|
|
231
|
+
|
|
232
|
+
const message = execSync(`git log -1 ${shortHash} --format="%s"`, {
|
|
233
|
+
encoding: "utf-8",
|
|
234
|
+
}).trim();
|
|
235
|
+
|
|
236
|
+
expect(message).toBe("Initial commit");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should get commit by HEAD reference", () => {
|
|
240
|
+
const message = execSync('git log -1 HEAD --format="%s"', {
|
|
241
|
+
encoding: "utf-8",
|
|
242
|
+
}).trim();
|
|
243
|
+
|
|
244
|
+
expect(message).toBe("Initial commit");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should get multiple commits", () => {
|
|
248
|
+
// 创建第二个 commit
|
|
249
|
+
execSync("echo 'test2' > test2.txt", { stdio: "pipe" });
|
|
250
|
+
execSync("git add .", { stdio: "pipe" });
|
|
251
|
+
execSync('git commit -m "Second commit"', { stdio: "pipe" });
|
|
252
|
+
|
|
253
|
+
const log = execSync('git log -2 --format="%s"', {
|
|
254
|
+
encoding: "utf-8",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(log).toContain("Second commit");
|
|
258
|
+
expect(log).toContain("Initial commit");
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe("边界情况", () => {
|
|
263
|
+
it("should handle commit with special characters in message", () => {
|
|
264
|
+
execSync('git commit --amend -m "feat: add \\"quotes\\" feature"', {
|
|
265
|
+
stdio: "pipe",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const message = execSync('git log -1 --format="%s"', {
|
|
269
|
+
encoding: "utf-8",
|
|
270
|
+
}).trim();
|
|
271
|
+
|
|
272
|
+
expect(message).toContain("quotes");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should handle very old dates", () => {
|
|
276
|
+
const oldDate = "2000-01-01 00:00:00";
|
|
277
|
+
|
|
278
|
+
execSync(
|
|
279
|
+
`GIT_AUTHOR_DATE="${oldDate}" GIT_COMMITTER_DATE="${oldDate}" git commit --amend --no-edit --reset-author`,
|
|
280
|
+
{ stdio: "pipe" },
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const authorDate = execSync(
|
|
284
|
+
'git log -1 --format="%ad" --date=format:"%Y-%m-%d"',
|
|
285
|
+
{ encoding: "utf-8" },
|
|
286
|
+
).trim();
|
|
287
|
+
|
|
288
|
+
expect(authorDate).toBe("2000-01-01");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should handle future dates", () => {
|
|
292
|
+
const futureDate = "2030-12-31 23:59:59";
|
|
293
|
+
|
|
294
|
+
execSync(
|
|
295
|
+
`GIT_AUTHOR_DATE="${futureDate}" GIT_COMMITTER_DATE="${futureDate}" git commit --amend --no-edit --reset-author`,
|
|
296
|
+
{ stdio: "pipe" },
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const authorDate = execSync(
|
|
300
|
+
'git log -1 --format="%ad" --date=format:"%Y-%m-%d"',
|
|
301
|
+
{ encoding: "utf-8" },
|
|
302
|
+
).trim();
|
|
303
|
+
|
|
304
|
+
expect(authorDate).toBe("2030-12-31");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("多个 commits", () => {
|
|
309
|
+
beforeEach(() => {
|
|
310
|
+
// 创建多个 commits
|
|
311
|
+
execSync("echo 'test2' > test2.txt", { stdio: "pipe" });
|
|
312
|
+
execSync("git add .", { stdio: "pipe" });
|
|
313
|
+
execSync('git commit -m "Second commit"', { stdio: "pipe" });
|
|
314
|
+
|
|
315
|
+
execSync("echo 'test3' > test3.txt", { stdio: "pipe" });
|
|
316
|
+
execSync("git add .", { stdio: "pipe" });
|
|
317
|
+
execSync('git commit -m "Third commit"', { stdio: "pipe" });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should have multiple commits", () => {
|
|
321
|
+
const count = execSync("git log --oneline | wc -l", {
|
|
322
|
+
encoding: "utf-8",
|
|
323
|
+
}).trim();
|
|
324
|
+
|
|
325
|
+
expect(parseInt(count)).toBe(3);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should get commits in reverse order", () => {
|
|
329
|
+
const firstHash = execSync('git log --reverse --format="%H" | head -1', {
|
|
330
|
+
encoding: "utf-8",
|
|
331
|
+
}).trim();
|
|
332
|
+
|
|
333
|
+
const message = execSync(`git log -1 ${firstHash} --format="%s"`, {
|
|
334
|
+
encoding: "utf-8",
|
|
335
|
+
}).trim();
|
|
336
|
+
|
|
337
|
+
expect(message).toBe("Initial commit");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("should modify only latest commit", () => {
|
|
341
|
+
const newDate = "2026-01-15 00:00:00";
|
|
342
|
+
|
|
343
|
+
execSync(
|
|
344
|
+
`GIT_AUTHOR_DATE="${newDate}" GIT_COMMITTER_DATE="${newDate}" git commit --amend --no-edit --reset-author`,
|
|
345
|
+
{ stdio: "pipe" },
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// 检查最新 commit
|
|
349
|
+
const latestDate = execSync(
|
|
350
|
+
'git log -1 --format="%ad" --date=format:"%Y-%m-%d"',
|
|
351
|
+
{ encoding: "utf-8" },
|
|
352
|
+
).trim();
|
|
353
|
+
|
|
354
|
+
// 检查第二个 commit(不应该被修改)
|
|
355
|
+
const secondDate = execSync(
|
|
356
|
+
'git log -1 HEAD~1 --format="%ad" --date=format:"%Y-%m-%d"',
|
|
357
|
+
{ encoding: "utf-8" },
|
|
358
|
+
).trim();
|
|
359
|
+
|
|
360
|
+
expect(latestDate).toBe("2026-01-15");
|
|
361
|
+
expect(secondDate).not.toBe("2026-01-15");
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|