mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(git-hooks): skip sequencer pre-commit formatting (#95842)
* fix(git-hooks): skip sequencer pre-commit formatting * chore: rerun CI * fix(git-hooks): skip revert sequencer formatting --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -16,6 +16,18 @@ if [[ ! -f "$FILTER_FILES" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
|
||||||
|
if [[ -n "$GIT_DIR" ]] && \
|
||||||
|
{ [[ -f "$GIT_DIR/MERGE_HEAD" ]] || \
|
||||||
|
[[ -f "$GIT_DIR/CHERRY_PICK_HEAD" ]] || \
|
||||||
|
[[ -f "$GIT_DIR/REVERT_HEAD" ]] || \
|
||||||
|
[[ -f "$GIT_DIR/REBASE_HEAD" ]] || \
|
||||||
|
[[ -d "$GIT_DIR/rebase-merge" ]] || \
|
||||||
|
[[ -d "$GIT_DIR/rebase-apply" ]]; }; then
|
||||||
|
# Sequencer commits stage the operation result, not just the user's local edits.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Security: avoid option-injection from malicious file names (e.g. "--all", "--force").
|
# Security: avoid option-injection from malicious file names (e.g. "--all", "--force").
|
||||||
# Robustness: NUL-delimited file list handles spaces/newlines safely.
|
# Robustness: NUL-delimited file list handles spaces/newlines safely.
|
||||||
# Compatibility: use read loops instead of `mapfile` so this runs on macOS Bash 3.x.
|
# Compatibility: use read loops instead of `mapfile` so this runs on macOS Bash 3.x.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Git hook tests validate pre-commit hook behavior and scripts.
|
// Git hook tests validate pre-commit hook behavior and scripts.
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { existsSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { cleanupTempDirs, makeTempRepoRoot } from "./helpers/temp-repo.js";
|
import { cleanupTempDirs, makeTempRepoRoot } from "./helpers/temp-repo.js";
|
||||||
@@ -39,8 +39,8 @@ const runFailure = (
|
|||||||
const failure = error as Error & { status?: number; stderr?: string; stdout?: string };
|
const failure = error as Error & { status?: number; stderr?: string; stdout?: string };
|
||||||
return {
|
return {
|
||||||
status: failure.status ?? 1,
|
status: failure.status ?? 1,
|
||||||
stderr: String(failure.stderr ?? ""),
|
stderr: failure.stderr ?? "",
|
||||||
stdout: String(failure.stdout ?? ""),
|
stdout: failure.stdout ?? "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -83,6 +83,34 @@ function installPreCommitFixture(dir: string): string {
|
|||||||
return fakeBinDir;
|
return fakeBinDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function installFormattingRecorder(dir: string): string {
|
||||||
|
const logPath = path.join(dir, "hook-tool.log");
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, "scripts", "pre-commit", "filter-staged-files.mjs"),
|
||||||
|
`const files = process.argv.slice(3).filter((arg) => arg !== "--");
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith(".ts")) {
|
||||||
|
process.stdout.write(file);
|
||||||
|
process.stdout.write("\0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
path.join(dir, "scripts", "pre-commit", "run-node-tool.sh"),
|
||||||
|
`#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
printf '%s\n' "$*" >> ${JSON.stringify(logPath)}
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
encoding: "utf8",
|
||||||
|
mode: 0o755,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return logPath;
|
||||||
|
}
|
||||||
|
|
||||||
function installRunNodeToolFixture(dir: string): void {
|
function installRunNodeToolFixture(dir: string): void {
|
||||||
mkdirSync(path.join(dir, "scripts", "pre-commit"), { recursive: true });
|
mkdirSync(path.join(dir, "scripts", "pre-commit"), { recursive: true });
|
||||||
symlinkSync(
|
symlinkSync(
|
||||||
@@ -101,6 +129,13 @@ function splitNonEmptyLines(output: string): string[] {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readFormatterLog(logPath: string): string[] {
|
||||||
|
if (!existsSync(logPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return splitNonEmptyLines(readFileSync(logPath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanupTempDirs(tempDirs);
|
cleanupTempDirs(tempDirs);
|
||||||
});
|
});
|
||||||
@@ -128,6 +163,100 @@ describe("git-hooks/pre-commit (integration)", () => {
|
|||||||
expect(staged).toEqual(["--all"]);
|
expect(staged).toEqual(["--all"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips formatting staged files while a merge commit is in progress", () => {
|
||||||
|
const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-merge-");
|
||||||
|
run(dir, "git", ["init", "-q", "--initial-branch=main"]);
|
||||||
|
installPreCommitFixture(dir);
|
||||||
|
const logPath = installFormattingRecorder(dir);
|
||||||
|
|
||||||
|
writeFileSync(path.join(dir, "changed.ts"), "export const value = 1;\n", "utf8");
|
||||||
|
run(dir, "git", ["add", "--", "changed.ts"]);
|
||||||
|
run(dir, "git", [
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.invalid",
|
||||||
|
"commit",
|
||||||
|
"-q",
|
||||||
|
"-m",
|
||||||
|
"initial",
|
||||||
|
]);
|
||||||
|
run(dir, "git", ["checkout", "-q", "-b", "side"]);
|
||||||
|
writeFileSync(path.join(dir, "changed.ts"), "export const value = 2;\n", "utf8");
|
||||||
|
run(dir, "git", ["add", "--", "changed.ts"]);
|
||||||
|
run(dir, "git", [
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.invalid",
|
||||||
|
"commit",
|
||||||
|
"-q",
|
||||||
|
"-m",
|
||||||
|
"side change",
|
||||||
|
]);
|
||||||
|
run(dir, "git", ["checkout", "-q", "main"]);
|
||||||
|
run(dir, "git", [
|
||||||
|
"-c",
|
||||||
|
"user.name=Test User",
|
||||||
|
"-c",
|
||||||
|
"user.email=test@example.invalid",
|
||||||
|
"merge",
|
||||||
|
"--no-commit",
|
||||||
|
"--no-ff",
|
||||||
|
"side",
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(existsSync(path.join(dir, ".git", "MERGE_HEAD"))).toBe(true);
|
||||||
|
expect(run(dir, "git", ["diff", "--cached", "--name-only"])).toBe("changed.ts");
|
||||||
|
|
||||||
|
run(dir, "bash", ["git-hooks/pre-commit"]);
|
||||||
|
|
||||||
|
expect(readFormatterLog(logPath)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["cherry-pick", "CHERRY_PICK_HEAD", "file"],
|
||||||
|
["revert", "REVERT_HEAD", "file"],
|
||||||
|
["rebase head", "REBASE_HEAD", "file"],
|
||||||
|
["merge rebase state", "rebase-merge", "dir"],
|
||||||
|
["apply rebase state", "rebase-apply", "dir"],
|
||||||
|
])("skips formatting staged files while %s metadata is present", (_label, gitPath, kind) => {
|
||||||
|
const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-sequencer-");
|
||||||
|
run(dir, "git", ["init", "-q", "--initial-branch=main"]);
|
||||||
|
installPreCommitFixture(dir);
|
||||||
|
const logPath = installFormattingRecorder(dir);
|
||||||
|
|
||||||
|
writeFileSync(path.join(dir, "changed.ts"), "export const value = 1;\n", "utf8");
|
||||||
|
run(dir, "git", ["add", "--", "changed.ts"]);
|
||||||
|
|
||||||
|
const metadataPath = path.join(dir, ".git", gitPath);
|
||||||
|
if (kind === "dir") {
|
||||||
|
mkdirSync(metadataPath, { recursive: true });
|
||||||
|
} else {
|
||||||
|
writeFileSync(metadataPath, "sequencer state\n", "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
run(dir, "bash", ["git-hooks/pre-commit"]);
|
||||||
|
|
||||||
|
expect(readFormatterLog(logPath)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still formats staged files during a normal commit", () => {
|
||||||
|
const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-normal-");
|
||||||
|
run(dir, "git", ["init", "-q", "--initial-branch=main"]);
|
||||||
|
installPreCommitFixture(dir);
|
||||||
|
const logPath = installFormattingRecorder(dir);
|
||||||
|
|
||||||
|
writeFileSync(path.join(dir, "changed.ts"), "export const value = 1;\n", "utf8");
|
||||||
|
run(dir, "git", ["add", "--", "changed.ts"]);
|
||||||
|
|
||||||
|
run(dir, "bash", ["git-hooks/pre-commit"]);
|
||||||
|
|
||||||
|
expect(readFormatterLog(logPath)).toEqual([
|
||||||
|
"oxfmt --write --no-error-on-unmatched-pattern changed.ts",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not run the changed-scope check for non-doc staged changes", () => {
|
it("does not run the changed-scope check for non-doc staged changes", () => {
|
||||||
const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-no-check-changed-");
|
const dir = makeTempRepoRoot(tempDirs, "openclaw-pre-commit-no-check-changed-");
|
||||||
run(dir, "git", ["init", "-q", "--initial-branch=main"]);
|
run(dir, "git", ["init", "-q", "--initial-branch=main"]);
|
||||||
|
|||||||
Reference in New Issue
Block a user