























11// Git hook tests validate pre-commit hook behavior and scripts.
22import { execFileSync } from "node:child_process";
3-import { existsSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
3+import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
44import path from "node:path";
55import { afterEach, describe, expect, it } from "vitest";
66import { cleanupTempDirs, makeTempRepoRoot } from "./helpers/temp-repo.js";
@@ -39,8 +39,8 @@ const runFailure = (
3939const failure = error as Error & { status?: number; stderr?: string; stdout?: string };
4040return {
4141status: failure.status ?? 1,
42-stderr: String(failure.stderr ?? ""),
43-stdout: String(failure.stdout ?? ""),
42+stderr: failure.stderr ?? "",
43+stdout: failure.stdout ?? "",
4444};
4545}
4646throw error;
@@ -83,6 +83,34 @@ function installPreCommitFixture(dir: string): string {
8383return fakeBinDir;
8484}
858586+function installFormattingRecorder(dir: string): string {
87+const logPath = path.join(dir, "hook-tool.log");
88+writeFileSync(
89+path.join(dir, "scripts", "pre-commit", "filter-staged-files.mjs"),
90+`const files = process.argv.slice(3).filter((arg) => arg !== "--");
91+for (const file of files) {
92+ if (file.endsWith(".ts")) {
93+ process.stdout.write(file);
94+ process.stdout.write("\0");
95+ }
96+}
97+`,
98+"utf8",
99+);
100+writeFileSync(
101+path.join(dir, "scripts", "pre-commit", "run-node-tool.sh"),
102+`#!/usr/bin/env bash
103+set -euo pipefail
104+printf '%s\n' "$*" >> ${JSON.stringify(logPath)}
105+`,
106+{
107+encoding: "utf8",
108+mode: 0o755,
109+},
110+);
111+return logPath;
112+}
113+86114function installRunNodeToolFixture(dir: string): void {
87115mkdirSync(path.join(dir, "scripts", "pre-commit"), { recursive: true });
88116symlinkSync(
@@ -101,6 +129,13 @@ function splitNonEmptyLines(output: string): string[] {
101129return lines;
102130}
103131132+function readFormatterLog(logPath: string): string[] {
133+if (!existsSync(logPath)) {
134+return [];
135+}
136+return splitNonEmptyLines(readFileSync(logPath, "utf8"));
137+}
138+104139afterEach(() => {
105140cleanupTempDirs(tempDirs);
106141});
@@ -128,6 +163,100 @@ describe("git-hooks/pre-commit (integration)", () => {
128163expect(staged).toEqual(["--all"]);
129164});
130165166+it("skips formatting staged files while a merge commit is in progress", () => {
167+const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-merge-");
168+run(dir, "git", ["init", "-q", "--initial-branch=main"]);
169+installPreCommitFixture(dir);
170+const logPath = installFormattingRecorder(dir);
171+172+writeFileSync(path.join(dir, "changed.ts"), "export const value = 1;\n", "utf8");
173+run(dir, "git", ["add", "--", "changed.ts"]);
174+run(dir, "git", [
175+"-c",
176+"user.name=Test User",
177+"-c",
178+"user.email=test@example.invalid",
179+"commit",
180+"-q",
181+"-m",
182+"initial",
183+]);
184+run(dir, "git", ["checkout", "-q", "-b", "side"]);
185+writeFileSync(path.join(dir, "changed.ts"), "export const value = 2;\n", "utf8");
186+run(dir, "git", ["add", "--", "changed.ts"]);
187+run(dir, "git", [
188+"-c",
189+"user.name=Test User",
190+"-c",
191+"user.email=test@example.invalid",
192+"commit",
193+"-q",
194+"-m",
195+"side change",
196+]);
197+run(dir, "git", ["checkout", "-q", "main"]);
198+run(dir, "git", [
199+"-c",
200+"user.name=Test User",
201+"-c",
202+"user.email=test@example.invalid",
203+"merge",
204+"--no-commit",
205+"--no-ff",
206+"side",
207+]);
208+209+expect(existsSync(path.join(dir, ".git", "MERGE_HEAD"))).toBe(true);
210+expect(run(dir, "git", ["diff", "--cached", "--name-only"])).toBe("changed.ts");
211+212+run(dir, "bash", ["git-hooks/pre-commit"]);
213+214+expect(readFormatterLog(logPath)).toEqual([]);
215+});
216+217+it.each([
218+["cherry-pick", "CHERRY_PICK_HEAD", "file"],
219+["revert", "REVERT_HEAD", "file"],
220+["rebase head", "REBASE_HEAD", "file"],
221+["merge rebase state", "rebase-merge", "dir"],
222+["apply rebase state", "rebase-apply", "dir"],
223+])("skips formatting staged files while %s metadata is present", (_label, gitPath, kind) => {
224+const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-sequencer-");
225+run(dir, "git", ["init", "-q", "--initial-branch=main"]);
226+installPreCommitFixture(dir);
227+const logPath = installFormattingRecorder(dir);
228+229+writeFileSync(path.join(dir, "changed.ts"), "export const value = 1;\n", "utf8");
230+run(dir, "git", ["add", "--", "changed.ts"]);
231+232+const metadataPath = path.join(dir, ".git", gitPath);
233+if (kind === "dir") {
234+mkdirSync(metadataPath, { recursive: true });
235+} else {
236+writeFileSync(metadataPath, "sequencer state\n", "utf8");
237+}
238+239+run(dir, "bash", ["git-hooks/pre-commit"]);
240+241+expect(readFormatterLog(logPath)).toEqual([]);
242+});
243+244+it("still formats staged files during a normal commit", () => {
245+const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-normal-");
246+run(dir, "git", ["init", "-q", "--initial-branch=main"]);
247+installPreCommitFixture(dir);
248+const logPath = installFormattingRecorder(dir);
249+250+writeFileSync(path.join(dir, "changed.ts"), "export const value = 1;\n", "utf8");
251+run(dir, "git", ["add", "--", "changed.ts"]);
252+253+run(dir, "bash", ["git-hooks/pre-commit"]);
254+255+expect(readFormatterLog(logPath)).toEqual([
256+"oxfmt --write --no-error-on-unmatched-pattern changed.ts",
257+]);
258+});
259+131260it("does not run the changed-scope check for non-doc staged changes", () => {
132261const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-no-check-changed-");
133262run(dir, "git", ["init", "-q", "--initial-branch=main"]);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。