git-synchro/git-synchro.py
2025-06-27 12:48:12 +03:00

133 lines
5.4 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 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 <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()