chore: protect main from force push

This commit is contained in:
YBF
2026-09-17 00:31:13 +08:00
parent 4f98c1e845
commit e9a13014ad
3 changed files with 123 additions and 1 deletions
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env sh
set -e
zero_object_id=0000000000000000000000000000000000000000
while IFS=' ' read -r local_ref local_object_id remote_ref remote_object_id
do
if [ "$remote_ref" != "refs/heads/main" ]; then
continue
fi
if [ "$local_object_id" = "$zero_object_id" ]; then
echo "拒绝推送:禁止删除远端 main。" >&2
exit 1
fi
if [ "$remote_object_id" = "$zero_object_id" ]; then
continue
fi
if ! git merge-base --is-ancestor "$remote_object_id" "$local_object_id"; then
echo "拒绝推送:远端 main 只允许快进更新,禁止强制推送。" >&2
exit 1
fi
done
+1 -1
View File
@@ -15,7 +15,7 @@
"dev:harness": "pnpm --filter @trade-message-center/mind-test-harness run dev",
"build": "pnpm run clean:dist && pnpm run version:ensure && node scripts/with-build-hash.mjs --filter @trade-message-center/onetalk-contract --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run build",
"typecheck": "pnpm run clean:dist && pnpm run version:ensure && pnpm --filter @trade-message-center/onetalk-contract --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run typecheck",
"test": "pnpm run clean:dist && pnpm run version:ensure && node --test scripts/package-version.test.mjs scripts/release.test.mjs scripts/dev-startup.test.mjs && pnpm --filter @trade-message-center/onetalk-contract --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run test",
"test": "pnpm run clean:dist && pnpm run version:ensure && node --test scripts/package-version.test.mjs scripts/release.test.mjs scripts/dev-startup.test.mjs scripts/pre-push-main-protection.test.mjs && pnpm --filter @trade-message-center/onetalk-contract --filter @trade-message-center/server --filter @trade-message-center/chrome-extension --if-present run test",
"format": "oxfmt",
"format:check": "pnpm run version:ensure && oxfmt --check",
"version:sync": "node scripts/sync-package-versions.mjs",
+96
View File
@@ -0,0 +1,96 @@
// 验证 pre-push 对远端 main 的非快进更新进行保护。
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import test from "node:test";
const repositoryRoot = fileURLToPath(new URL("../", import.meta.url));
const hookPath = join(repositoryRoot, ".husky", "pre-push");
const zeroObjectId = "0".repeat(40);
const runGit = (cwd, arguments_) => {
const result = spawnSync("git", arguments_, {
cwd,
encoding: "utf8",
});
assert.equal(result.status, 0, result.stderr);
return result.stdout.trim();
};
const createRepository = () => {
const repository = mkdtempSync(join(tmpdir(), "tmc-pre-push-"));
runGit(repository, ["init", "--quiet"]);
runGit(repository, ["config", "user.email", "test@example.com"]);
runGit(repository, ["config", "user.name", "Pre-push test"]);
const commit = (fileName, content, message) => {
writeFileSync(join(repository, fileName), content);
runGit(repository, ["add", fileName]);
runGit(repository, ["commit", "--quiet", "-m", message]);
return runGit(repository, ["rev-parse", "HEAD"]);
};
const base = commit("base.txt", "base\n", "base");
const main = commit("main.txt", "main\n", "main");
runGit(repository, ["switch", "--detach", base]);
const divergent = commit("divergent.txt", "divergent\n", "divergent");
return { base, divergent, main, repository };
};
const runHook = (repository, localObjectId, remoteRef, remoteObjectId) =>
spawnSync("sh", [hookPath], {
cwd: repository,
encoding: "utf8",
input: `refs/heads/local ${localObjectId} ${remoteRef} ${remoteObjectId}\n`,
});
test("allows a fast-forward update to remote main", () => {
const { base, main, repository } = createRepository();
try {
const result = runHook(repository, main, "refs/heads/main", base);
assert.equal(result.status, 0, result.stderr);
} finally {
rmSync(repository, { force: true, recursive: true });
}
});
test("rejects a non-fast-forward update to remote main", () => {
const { divergent, main, repository } = createRepository();
try {
const result = runHook(repository, divergent, "refs/heads/main", main);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /只允许快进更新/u);
} finally {
rmSync(repository, { force: true, recursive: true });
}
});
test("does not restrict non-fast-forward updates to other remote branches", () => {
const { divergent, main, repository } = createRepository();
try {
const result = runHook(repository, divergent, "refs/heads/release", main);
assert.equal(result.status, 0, result.stderr);
} finally {
rmSync(repository, { force: true, recursive: true });
}
});
test("rejects deleting remote main", () => {
const { main, repository } = createRepository();
try {
const result = runHook(repository, zeroObjectId, "refs/heads/main", main);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /禁止删除远端 main/u);
} finally {
rmSync(repository, { force: true, recursive: true });
}
});