140 lines
6.0 KiB
Python
140 lines
6.0 KiB
Python
#!/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 or updates 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:
|
|
current_branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=local_dir).decode().strip()
|
|
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]
|
|
if branch == current_branch:
|
|
try:
|
|
subprocess.run(["git", "checkout", f"{from_remote}/{branch}"], cwd=local_dir, check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Warning: Could not update currently checked-out branch '{branch}' in {local_dir}. Possible uncommitted changes.")
|
|
else:
|
|
subprocess.run(["git", "branch", "-f", branch, f"{from_remote}/{branch}"], cwd=local_dir)
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error: Failed to create or update 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 <source> --to <destination> [--mirror] [--dry-run] [--only <repo>]")
|
|
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()
|