#!/usr/bin/env python3 import argparse import subprocess import sys from pathlib import Path CONFIG_FILE = 'repos.conf' def parse_config(): """ Parses the repos.conf file and returns a list of tuples with repository information. The file should contain 5 fields separated by `;` and support `#` comments. """ repos = [] try: with open(CONFIG_FILE) as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue parts = list(map(str.strip, line.split(';'))) if len(parts) != 5: print(f"Invalid config line: {line}") continue nickname, server_nick, remote_name, url, directory = parts repos.append((nickname, server_nick, remote_name, url, Path(directory))) except FileNotFoundError: print(f"Error: Configuration file '{CONFIG_FILE}' not found.") sys.exit(1) except Exception as e: print(f"Error reading config file: {e}") sys.exit(1) return repos def match_repos(repos, from_server, to_server, only=None): """ Matches repository pairs based on from/to server nicknames. If --only is specified, limits to the specific repo-nickname. """ matched = {} for nick in {r[0] for r in repos}: if only and nick != only: continue from_entry = next((r for r in repos if r[0] == nick and r[1] == from_server), None) to_entry = next((r for r in repos if r[0] == nick and r[1] == to_server), None) if from_entry and to_entry: matched[nick] = (from_entry, to_entry) return matched def run_git(cmd, cwd, dry_run=False): """ Prints and optionally executes the git command in the specified working directory. This is where the script changes directory using cwd before running git. """ print(f"[CMD] ({cwd}) $ {' '.join(cmd)}") if not dry_run: try: subprocess.run(cmd, cwd=cwd, check=True, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: print(f"Error: Git command '{' '.join(cmd)}' failed in {cwd}\nOutput:\n{e.output.decode() if e.output else str(e)}") sys.exit(1) def sync_repo(nick, from_entry, to_entry, use_mirror, dry_run=False): """ Synchronizes a repository from from_entry to to_entry. Executes git fetch from the source and git push to the destination. If --mirror is used, performs full push of all refs. Otherwise, creates local tracking branches and pushes them without checking out. """ _, _, from_remote, from_url, local_dir = from_entry _, _, to_remote, to_url, _ = to_entry if not local_dir.exists(): print(f"Directory {local_dir} not found. Cloning...") run_git(["git", "clone", from_url, str(local_dir)], cwd=".", dry_run=dry_run) else: run_git(["git", "fetch", "--all"], cwd=local_dir, dry_run=dry_run) if not dry_run: try: remotes = subprocess.check_output(["git", "remote"], cwd=local_dir).decode().splitlines() except subprocess.CalledProcessError as e: print(f"Error: Unable to list git remotes in {local_dir}\nMessage: {e}") sys.exit(1) if to_remote not in remotes: run_git(["git", "remote", "add", to_remote, to_url], cwd=local_dir, dry_run=dry_run) if use_mirror: run_git(["git", "push", to_remote, "--mirror"], cwd=local_dir, dry_run=dry_run) else: if not dry_run: try: remotes = subprocess.check_output(["git", "branch", "-r"], cwd=local_dir).decode().splitlines() for r in remotes: if f'{from_remote}/' in r and '->' not in r: branch = r.strip().split(f'{from_remote}/')[1] subprocess.run(["git", "branch", branch, f"{from_remote}/{branch}"], cwd=local_dir) except subprocess.CalledProcessError as e: print(f"Error: Failed to create tracking branches in {local_dir}\nMessage: {e}") sys.exit(1) run_git(["git", "push", to_remote, "--all"], cwd=local_dir, dry_run=dry_run) run_git(["git", "push", to_remote, "--tags"], cwd=local_dir, dry_run=dry_run) def main(): """ Main function. Parses command-line arguments, reads config, and syncs matching repositories between the specified servers. """ parser = argparse.ArgumentParser() parser.add_argument('--from', dest='from_server', required=True) parser.add_argument('--to', dest='to_server', required=True) parser.add_argument('--mirror', action='store_true') parser.add_argument('--dry-run', action='store_true') parser.add_argument('--only', type=str, help='Sync only a specific repository nickname') try: args = parser.parse_args() except SystemExit: print("Error: Invalid or missing arguments. Usage: --from --to [--mirror] [--dry-run] [--only ]") sys.exit(1) repos = parse_config() matched = match_repos(repos, args.from_server, args.to_server, args.only) if not matched: print("No matching repo pairs found.") sys.exit(1) for nick, (from_entry, to_entry) in matched.items(): print(f"\n=== Syncing [{nick}] from {args.from_server} to {args.to_server} ===") sync_repo(nick, from_entry, to_entry, args.mirror, args.dry_run) if __name__ == "__main__": main()