Init commit
This commit is contained in:
commit
da998d8eec
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#Pythons envs
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
|
132
git-synchro.py
Normal file
132
git-synchro.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
#!/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, checks out all remote branches and pushes with --all and --tags.
|
||||||
|
"""
|
||||||
|
_, _, 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", "checkout", "-B", branch, f"{from_remote}/{branch}"], cwd=local_dir)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Error: Failed to checkout remote 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()
|
8
repos.conf
Normal file
8
repos.conf
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#
|
||||||
|
# git-synchro configuration file
|
||||||
|
#
|
||||||
|
# format:
|
||||||
|
# repo-nickname; server-nickname; repo-remote; repo-url; directory
|
||||||
|
#
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user