@zjex/git-workflow 0.4.2 → 0.4.3
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 +11 -0
- package/README.md +1 -1
- package/dist/index.js +323 -11
- 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/index.ts +39 -4
- package/tests/amend-date.test.ts +364 -0
- package/tests/amend.test.ts +441 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
3
|
+
import { colors, theme, execOutput, divider } from "../utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 获取最近的 commits
|
|
7
|
+
* @param limit 数量限制
|
|
8
|
+
* @returns commit 列表
|
|
9
|
+
*/
|
|
10
|
+
function getRecentCommits(limit: number = 20): Array<{
|
|
11
|
+
hash: string;
|
|
12
|
+
message: string;
|
|
13
|
+
date: string;
|
|
14
|
+
}> {
|
|
15
|
+
const output = execOutput(`git log -${limit} --pretty=format:"%H|%s|%ai"`);
|
|
16
|
+
|
|
17
|
+
if (!output) return [];
|
|
18
|
+
|
|
19
|
+
return output.split("\n").map((line) => {
|
|
20
|
+
const [hash, message, date] = line.split("|");
|
|
21
|
+
return { hash, message, date };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 根据 hash 获取 commit 信息
|
|
27
|
+
* @param hash commit hash (可以是短 hash)
|
|
28
|
+
* @returns commit 信息或 null
|
|
29
|
+
*/
|
|
30
|
+
function getCommitByHash(hash: string): {
|
|
31
|
+
hash: string;
|
|
32
|
+
message: string;
|
|
33
|
+
date: string;
|
|
34
|
+
} | null {
|
|
35
|
+
try {
|
|
36
|
+
const output = execOutput(`git log -1 ${hash} --pretty=format:"%H|%s|%ai"`);
|
|
37
|
+
|
|
38
|
+
if (!output) return null;
|
|
39
|
+
|
|
40
|
+
const [fullHash, message, date] = output.split("|");
|
|
41
|
+
return { hash: fullHash, message, date };
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 修改指定 commit 的提交信息
|
|
49
|
+
* @param commitHash 可选的 commit hash
|
|
50
|
+
*/
|
|
51
|
+
export async function amend(commitHash?: string): Promise<void> {
|
|
52
|
+
console.log(colors.cyan("修改 Commit 提交信息"));
|
|
53
|
+
divider();
|
|
54
|
+
|
|
55
|
+
let selectedCommit: { hash: string; message: string; date: string };
|
|
56
|
+
|
|
57
|
+
// ========== 步骤 1: 确定要修改的 commit ==========
|
|
58
|
+
if (commitHash) {
|
|
59
|
+
// 如果指定了 hash,直接获取该 commit
|
|
60
|
+
const commit = getCommitByHash(commitHash);
|
|
61
|
+
if (!commit) {
|
|
62
|
+
console.log(colors.red(`✖ 找不到 commit: ${commitHash}`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
selectedCommit = commit;
|
|
66
|
+
} else {
|
|
67
|
+
// 否则让用户选择
|
|
68
|
+
const commits = getRecentCommits(20);
|
|
69
|
+
|
|
70
|
+
if (commits.length === 0) {
|
|
71
|
+
console.log(colors.yellow("没有找到任何 commit"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
selectedCommit = await select({
|
|
76
|
+
message: "选择要修改的 commit:",
|
|
77
|
+
choices: commits.map((c) => ({
|
|
78
|
+
name: `${colors.yellow(c.hash.slice(0, 7))} ${c.message} ${colors.dim(c.date)}`,
|
|
79
|
+
value: c,
|
|
80
|
+
description: c.message,
|
|
81
|
+
})),
|
|
82
|
+
pageSize: 15,
|
|
83
|
+
theme,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log("当前 commit 信息:");
|
|
89
|
+
console.log(` Hash: ${colors.yellow(selectedCommit.hash.slice(0, 7))}`);
|
|
90
|
+
console.log(` Message: ${selectedCommit.message}`);
|
|
91
|
+
console.log(` Date: ${colors.dim(selectedCommit.date)}`);
|
|
92
|
+
divider();
|
|
93
|
+
|
|
94
|
+
// ========== 步骤 2: 输入新的 commit message ==========
|
|
95
|
+
const newMessage = await input({
|
|
96
|
+
message: "输入新的 commit message:",
|
|
97
|
+
default: selectedCommit.message,
|
|
98
|
+
validate: (value) => {
|
|
99
|
+
if (!value.trim()) {
|
|
100
|
+
return "commit message 不能为空";
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
theme,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ========== 步骤 3: 预览并确认 ==========
|
|
108
|
+
divider();
|
|
109
|
+
console.log("修改预览:");
|
|
110
|
+
console.log(
|
|
111
|
+
` Commit: ${colors.yellow(selectedCommit.hash.slice(0, 7))}`,
|
|
112
|
+
);
|
|
113
|
+
console.log(` 旧 Message: ${colors.dim(selectedCommit.message)}`);
|
|
114
|
+
console.log(` 新 Message: ${colors.green(newMessage)}`);
|
|
115
|
+
divider();
|
|
116
|
+
|
|
117
|
+
// 检查是否是最新的 commit
|
|
118
|
+
const latestHash = execOutput("git rev-parse HEAD");
|
|
119
|
+
const isLatestCommit = selectedCommit.hash === latestHash;
|
|
120
|
+
|
|
121
|
+
if (!isLatestCommit) {
|
|
122
|
+
console.log(
|
|
123
|
+
colors.yellow(
|
|
124
|
+
"⚠️ 警告: 修改非最新 commit 需要使用 rebase,可能会改变 commit hash",
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
console.log(colors.dim(" 这会影响已推送到远程的 commit,请谨慎操作"));
|
|
128
|
+
console.log("");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const shouldProceed = await confirm({
|
|
132
|
+
message: "确认修改?",
|
|
133
|
+
default: false,
|
|
134
|
+
theme,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!shouldProceed) {
|
|
138
|
+
console.log(colors.yellow("已取消"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ========== 步骤 4: 执行修改 ==========
|
|
143
|
+
try {
|
|
144
|
+
if (isLatestCommit) {
|
|
145
|
+
// 最新 commit,使用 amend
|
|
146
|
+
execSync(`git commit --amend -m "${newMessage.replace(/"/g, '\\"')}"`, {
|
|
147
|
+
stdio: "pipe",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(colors.green("✔ 修改成功"));
|
|
152
|
+
} else {
|
|
153
|
+
// 非最新 commit,使用 rebase
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(colors.cyan("正在执行 rebase..."));
|
|
156
|
+
|
|
157
|
+
// 使用 git rebase -i 的自动化方式
|
|
158
|
+
const parentHash = execOutput(`git rev-parse ${selectedCommit.hash}^`);
|
|
159
|
+
|
|
160
|
+
// 创建临时的 rebase 脚本
|
|
161
|
+
const rebaseScript = `#!/bin/sh
|
|
162
|
+
if [ "$1" = "${selectedCommit.hash}" ]; then
|
|
163
|
+
echo "${newMessage}" > "$2"
|
|
164
|
+
fi
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
// 使用 filter-branch 修改 commit message
|
|
168
|
+
const filterCmd = `git filter-branch -f --msg-filter 'if [ "$GIT_COMMIT" = "${selectedCommit.hash}" ]; then echo "${newMessage.replace(/"/g, '\\"')}"; else cat; fi' ${parentHash}..HEAD`;
|
|
169
|
+
|
|
170
|
+
execSync(filterCmd, { stdio: "pipe" });
|
|
171
|
+
|
|
172
|
+
console.log(colors.green("✔ 修改成功"));
|
|
173
|
+
console.log("");
|
|
174
|
+
console.log(colors.yellow("⚠️ 注意: commit hash 已改变"));
|
|
175
|
+
console.log(colors.dim(" 如果已推送到远程,需要使用 force push:"));
|
|
176
|
+
console.log(colors.cyan(" git push --force-with-lease"));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.log("");
|
|
182
|
+
console.log(colors.red("✖ 修改失败"));
|
|
183
|
+
if (error instanceof Error) {
|
|
184
|
+
console.log(colors.dim(error.message));
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ import { commit } from "./commands/commit.js";
|
|
|
29
29
|
import { checkForUpdates } from "./update-notifier.js";
|
|
30
30
|
import { update } from "./commands/update.js";
|
|
31
31
|
import { log, quickLog } from "./commands/log.js";
|
|
32
|
+
import { amendDate } from "./commands/amend-date.js";
|
|
33
|
+
import { amend } from "./commands/amend.js";
|
|
32
34
|
|
|
33
35
|
// ========== 全局错误处理 ==========
|
|
34
36
|
|
|
@@ -109,7 +111,7 @@ async function mainMenu(): Promise<void> {
|
|
|
109
111
|
███╔╝ ██ ██║██╔══╝ ██╔██╗
|
|
110
112
|
███████╗╚█████╔╝███████╗██╔╝ ██╗
|
|
111
113
|
╚══════╝ ╚════╝ ╚══════╝╚═╝ ╚═╝
|
|
112
|
-
`)
|
|
114
|
+
`),
|
|
113
115
|
);
|
|
114
116
|
console.log(colors.dim(` git-workflow v${colors.yellow(version)}\n`));
|
|
115
117
|
|
|
@@ -157,11 +159,19 @@ async function mainMenu(): Promise<void> {
|
|
|
157
159
|
value: "stash",
|
|
158
160
|
},
|
|
159
161
|
{
|
|
160
|
-
name: `[b]
|
|
162
|
+
name: `[b] 📜 查看日志 ${colors.dim("gw log")}`,
|
|
161
163
|
value: "log",
|
|
162
164
|
},
|
|
163
165
|
{
|
|
164
|
-
name: `[c]
|
|
166
|
+
name: `[c] 🕐 修改提交时间 ${colors.dim("gw ad")}`,
|
|
167
|
+
value: "amend-date",
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: `[d] ✏️ 修改提交信息 ${colors.dim("gw amend")}`,
|
|
171
|
+
value: "amend",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: `[e] ⚙️ 初始化配置 ${colors.dim("gw init")}`,
|
|
165
175
|
value: "init",
|
|
166
176
|
},
|
|
167
177
|
{ name: "[0] ❓ 帮助", value: "help" },
|
|
@@ -215,6 +225,14 @@ async function mainMenu(): Promise<void> {
|
|
|
215
225
|
checkGitRepo();
|
|
216
226
|
await log();
|
|
217
227
|
break;
|
|
228
|
+
case "amend-date":
|
|
229
|
+
checkGitRepo();
|
|
230
|
+
await amendDate();
|
|
231
|
+
break;
|
|
232
|
+
case "amend":
|
|
233
|
+
checkGitRepo();
|
|
234
|
+
await amend();
|
|
235
|
+
break;
|
|
218
236
|
case "init":
|
|
219
237
|
await init();
|
|
220
238
|
break;
|
|
@@ -378,6 +396,23 @@ cli
|
|
|
378
396
|
return log(logOptions);
|
|
379
397
|
});
|
|
380
398
|
|
|
399
|
+
cli
|
|
400
|
+
.command("amend:date [hash]", "修改指定 commit 的提交时间")
|
|
401
|
+
.alias("ad")
|
|
402
|
+
.action(async (hash?: string) => {
|
|
403
|
+
await checkForUpdates(version, "@zjex/git-workflow");
|
|
404
|
+
checkGitRepo();
|
|
405
|
+
return amendDate(hash);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
cli
|
|
409
|
+
.command("amend [hash]", "修改指定 commit 的提交信息")
|
|
410
|
+
.action(async (hash?: string) => {
|
|
411
|
+
await checkForUpdates(version, "@zjex/git-workflow");
|
|
412
|
+
checkGitRepo();
|
|
413
|
+
return amend(hash);
|
|
414
|
+
});
|
|
415
|
+
|
|
381
416
|
cli
|
|
382
417
|
.command("clean", "清理缓存和临时文件")
|
|
383
418
|
.alias("cc")
|
|
@@ -446,7 +481,7 @@ cli
|
|
|
446
481
|
console.log("");
|
|
447
482
|
console.log(colors.yellow("⚠️ 全局配置文件已删除"));
|
|
448
483
|
console.log(
|
|
449
|
-
colors.dim(` 如需重新配置,请运行: ${colors.cyan("gw init")}`)
|
|
484
|
+
colors.dim(` 如需重新配置,请运行: ${colors.cyan("gw init")}`),
|
|
450
485
|
);
|
|
451
486
|
}
|
|
452
487
|
|
|
@@ -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
|
+
});
|