blob: 55267d5d05a58c215d024c1bb6db914709cfd8a3 [file]
#!/usr/bin/env python3
# Copyright (C) 2026 The Android Open Source Project
#
# 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.
import argparse
import subprocess
import sys
import yaml
GIT_HOME = "/var/gerrit/git"
DEFAULT_CONFIG_FILE = "/var/gerrit/etc/incoming-replication.config.yaml"
DEFAULT_REFSPEC_TEMPLATE = "+refs/heads/*:refs/heads/{remote_name}/*"
def parse_timeout(value):
"""Convert a timeout string like '5m', '30s', or '120' to seconds (int)."""
if value is None or value == "null":
return None
value = str(value).strip()
if value.endswith("m"):
return int(value[:-1]) * 60
if value.endswith("s"):
return int(value[:-1])
return int(value)
class GitRepo:
def __init__(self, git_dir):
self.git_dir = git_dir
self.base = ["git", f"--git-dir={git_dir}"]
def config(self, *args, check=True):
subprocess.run(self.base + ["config"] + list(args), check=check)
def fetch(self, *args, timeout=None):
subprocess.run(self.base + ["fetch"] + list(args), check=True, timeout=timeout)
def __str__(self):
return self.git_dir
def configure_remote(git_repo, remote_name, remote_url, refspec):
git_repo.config(f"remote.{remote_name}.url", remote_url)
# exit code 5 means the key did not exist — that is fine on first run
git_repo.config("unset", "--all", f"remote.{remote_name}.fetch", check=False)
refspecs = refspec if isinstance(refspec, list) else [refspec]
for spec in refspecs:
git_repo.config("--add", f"remote.{remote_name}.fetch", spec)
def process_remote(remote):
remote_name = remote["name"]
server_url = remote["url"].rstrip("/")
timeout_seconds = parse_timeout(remote.get("timeout"))
for fetch in remote.get("fetch", []):
remote_repo = fetch["remoteRepo"]
local_repo = fetch.get("localRepo") or remote_repo
refspec = fetch.get("refSpec") or DEFAULT_REFSPEC_TEMPLATE.format(
remote_name=remote_name
)
git_repo = GitRepo(f"{GIT_HOME}/{local_repo}.git")
configure_remote(git_repo, remote_name, f"{server_url}/{remote_repo}", refspec)
print(f"Fetching {remote_repo} from {remote_name} into {git_repo}")
try:
git_repo.fetch(remote_name, timeout=timeout_seconds)
except subprocess.TimeoutExpired:
print(
f"ERROR: fetch of {remote_repo} from {remote_name} timed out "
f"after {timeout_seconds}s",
file=sys.stderr,
)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Fetch from remote Git repositories into local Gerrit repositories."
)
parser.add_argument(
"--config",
default=DEFAULT_CONFIG_FILE,
metavar="CONFIG_FILE",
help=f"Path to the YAML config file (default: {DEFAULT_CONFIG_FILE})",
)
args = parser.parse_args()
try:
with open(args.config) as f:
config = yaml.safe_load(f)
except FileNotFoundError:
print(f"ERROR: config file not found: {args.config}", file=sys.stderr)
sys.exit(1)
for remote in config.get("remotes", []):
process_remote(remote)
if __name__ == "__main__":
main()