96 lines
2.6 KiB
JavaScript
Executable File
96 lines
2.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { statSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const repoRoot = process.cwd();
|
|
const allowAssetCommit = process.env.ALLOW_ASSET_COMMIT === '1';
|
|
const maxBinarySizeBytes = 5 * 1024 * 1024;
|
|
const tmpCapturePattern = /^tmp-.*\.(png|jpe?g|webp|gif|mp4|mov|webm)$/i;
|
|
const binaryAssetPattern = /\.(png|jpe?g|webp|gif|svg|mp4|mov|webm|pdf|zip|7z)$/i;
|
|
|
|
const blockedAssetPrefixes = ['public/assets/'];
|
|
|
|
function getStagedPaths() {
|
|
const output = execFileSync(
|
|
'git',
|
|
['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
|
|
{ cwd: repoRoot, encoding: 'utf8' }
|
|
);
|
|
|
|
return output
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function toDisplaySize(bytes) {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
function isBlockedAssetPath(filePath) {
|
|
return blockedAssetPrefixes.some((prefix) => filePath.startsWith(prefix));
|
|
}
|
|
|
|
function main() {
|
|
const stagedPaths = getStagedPaths();
|
|
const violations = [];
|
|
|
|
for (const relativePath of stagedPaths) {
|
|
const absolutePath = path.join(repoRoot, relativePath);
|
|
let stats;
|
|
|
|
try {
|
|
stats = statSync(absolutePath);
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
if (!stats.isFile()) continue;
|
|
|
|
const baseName = path.basename(relativePath);
|
|
const isTmpCapture = tmpCapturePattern.test(baseName);
|
|
const isBinaryAsset = binaryAssetPattern.test(relativePath);
|
|
const isOversizedBinary = isBinaryAsset && stats.size > maxBinarySizeBytes;
|
|
const isBlockedAsset = isBinaryAsset && isBlockedAssetPath(relativePath);
|
|
|
|
if (!isTmpCapture && !isOversizedBinary && !isBlockedAsset) continue;
|
|
|
|
const reasons = [];
|
|
if (isTmpCapture) reasons.push('temporary capture file');
|
|
if (isBlockedAsset) reasons.push('asset path under public/assets');
|
|
if (isOversizedBinary) {
|
|
reasons.push(`binary file exceeds ${toDisplaySize(maxBinarySizeBytes)}`);
|
|
}
|
|
|
|
violations.push({
|
|
relativePath,
|
|
size: stats.size,
|
|
reasons,
|
|
});
|
|
}
|
|
|
|
if (violations.length === 0 || allowAssetCommit) {
|
|
return;
|
|
}
|
|
|
|
console.error('');
|
|
console.error('Blocked commit: staged asset files need an explicit override.');
|
|
console.error('Set ALLOW_ASSET_COMMIT=1 only when the asset commit is intentional.');
|
|
console.error('');
|
|
|
|
for (const violation of violations) {
|
|
console.error(
|
|
`- ${violation.relativePath} (${toDisplaySize(violation.size)}): ${violation.reasons.join(', ')}`
|
|
);
|
|
}
|
|
|
|
console.error('');
|
|
process.exit(1);
|
|
}
|
|
|
|
main();
|