blob: 30c30ebcdf8c45da782ac5e1cba744333d191a6b [file]
#!/usr/bin/env python3
#
# Copyright 2026 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Checks that repository Git blobs do not exceed the 25 MiB size limit."""
import argparse
import os
from pathlib import Path
import subprocess
import sys
BYTES_PER_MIB = 1024 * 1024
MAX_FILE_SIZE_BYTES = 25 * BYTES_PER_MIB
DEFAULT_REPO_DIR = str(Path(__file__).resolve().parents[2])
def _run_git(repo_dir, args, input_text=None, check=True):
return subprocess.run(
["git", "-C", repo_dir, "-c", "core.quotePath=false", *args],
input=input_text,
check=check,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="surrogateescape",
)
def find_oversized_blobs(repo_dir, base_ref="origin/master", max_bytes=MAX_FILE_SIZE_BYTES):
"""Returns sorted (path, size_bytes) pairs exceeding max_bytes."""
head_check = _run_git(repo_dir, ["rev-parse", "--verify", "HEAD"], check=False)
if head_check.returncode != 0:
return []
oversized = {}
ls_tree = _run_git(repo_dir, ["ls-tree", "-r", "-l", "-z", "HEAD"])
for entry in ls_tree.stdout.split("\0"):
if not entry or "\t" not in entry:
continue
meta, path = entry.split("\t", 1)
parts = meta.split()
if len(parts) < 4:
continue
obj_type, size_str = parts[1], parts[3]
if obj_type != "blob" or size_str == "-":
continue
size = int(size_str)
if size > max_bytes:
oversized[path] = max(oversized.get(path, 0), size)
if base_ref:
merge_base = _run_git(repo_dir, ["merge-base", base_ref, "HEAD"], check=False)
if merge_base.returncode == 0:
base_sha = merge_base.stdout.strip()
rev_list = _run_git(repo_dir, ["rev-list", "--objects", f"{base_sha}..HEAD"])
if rev_list.stdout:
cat_file = _run_git(
repo_dir,
["cat-file", "--batch-check=%(objecttype) %(objectsize) %(rest)"],
input_text=rev_list.stdout,
)
for line in cat_file.stdout.splitlines():
parts = line.split(" ", 2)
if len(parts) < 2 or parts[0] != "blob":
continue
size = int(parts[1])
if size > max_bytes:
path = parts[2] if len(parts) > 2 and parts[2] else "<unnamed blob>"
oversized[path] = max(oversized.get(path, 0), size)
return sorted(oversized.items())
def _resolve_default_base_ref(repo_dir):
gh_base = os.environ.get("GITHUB_BASE_REF", "").strip()
if gh_base:
remote_ref = f"origin/{gh_base}"
verify = _run_git(
repo_dir,
["rev-parse", "--verify", "--quiet", f"refs/remotes/{remote_ref}"],
check=False,
)
if verify.returncode == 0:
return remote_ref
fetch = _run_git(repo_dir, ["fetch", "--no-tags", "origin", gh_base], check=False)
if fetch.returncode == 0:
return "FETCH_HEAD"
return remote_ref
return "origin/master"
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--repo-dir",
default=DEFAULT_REPO_DIR,
help="Path to the Git repository root.",
)
parser.add_argument(
"--base-ref",
default=None,
help="Base Git ref used to inspect blobs across commits in base..HEAD.",
)
parser.add_argument(
"--max-bytes",
type=int,
default=MAX_FILE_SIZE_BYTES,
help="Maximum allowed blob size in bytes.",
)
args = parser.parse_args(argv)
base_ref = (
args.base_ref if args.base_ref is not None else _resolve_default_base_ref(args.repo_dir)
)
oversized = find_oversized_blobs(
args.repo_dir, base_ref=base_ref, max_bytes=args.max_bytes
)
if not oversized:
return 0
limit_mib = args.max_bytes / BYTES_PER_MIB
print(
f"ERROR: File(s) exceed the {limit_mib:g} MiB ({args.max_bytes:,} bytes) limit "
"due to limitations of an internal clone of this repository:",
file=sys.stderr,
)
for path, size in oversized:
size_mib = size / BYTES_PER_MIB
print(f" - {path}: {size_mib:.2f} MiB ({size:,} bytes)", file=sys.stderr)
print(
"Store large binaries or archives externally (for example, in GCS) "
"instead of committing them to Git.",
file=sys.stderr,
)
return 1
if __name__ == "__main__":
sys.exit(main())