#!/usr/bin/env python3 import os import subprocess import logging from datetime import datetime import shutil LOCAL_REPOS_BASE_DIR = "/var/git" # personal gitea directory, yours may differ GITHUB_USER_OR_ORG = "fddldev" # my github account name, obviously change this to yours MIRROR_WORK_DIR = "/home/tristan/github_mirror" USE_GH_FOR_REPO_CREATION = True GH_REPO_VISIBILITY = "--public" LOG_FILE = os.path.join(MIRROR_WORK_DIR, "mirror_sync_python.log") def setup_logging(): """Sets up logging to file and console.""" os.makedirs(MIRROR_WORK_DIR, exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.FileHandler(LOG_FILE), logging.StreamHandler() ] ) def run_command(command, cwd=None, check=True, suppress_output=False): """ Runs a shell command and logs its execution. Returns the subprocess.CompletedProcess object. Raises subprocess.CalledProcessError if 'check' is True and command fails. """ cmd_str = ' '.join(command) logging.info(f"Running command: {cmd_str} in {cwd or os.getcwd()}") try: process = subprocess.run( command, cwd=cwd, check=check, text=True, capture_output=True ) if not suppress_output: if process.stdout: logging.info(f"STDOUT:\n{process.stdout.strip()}") if process.stderr: logging.info(f"STDERR:\n{process.stderr.strip()}") return process except subprocess.CalledProcessError as e: logging.error(f"Error running command: {' '.join(e.cmd)}") if e.stdout: logging.error(f"STDOUT:\n{e.stdout.strip()}") if e.stderr: logging.error(f"STDERR:\n{e.stderr.strip()}") raise except FileNotFoundError: logging.error(f"Error: The command '{command[0]}' was not found. Is it installed and in PATH?") raise def main(): setup_logging() logging.info(f"--- Starting GitHub Mirror Sync (Python): {datetime.now()} ---") if GITHUB_USER_OR_ORG == "your-github-username-or-org": logging.error("CRITICAL: Please update GITHUB_USER_OR_ORG in the script.") print("ERROR: Please update GITHUB_USER_OR_ORG in the script before running.") return if not shutil.which("git"): logging.error("CRITICAL: 'git' command not found. Please install Git and ensure it's in your PATH.") return if USE_GH_FOR_REPO_CREATION and not shutil.which("gh"): logging.error("CRITICAL: 'gh' command not found, but USE_GH_FOR_REPO_CREATION is True. " "Please install GitHub CLI or set USE_GH_FOR_REPO_CREATION to False.") return if not os.path.isdir(LOCAL_REPOS_BASE_DIR): logging.error(f"Local repositories base directory '{LOCAL_REPOS_BASE_DIR}' does not exist.") return try: os.makedirs(MIRROR_WORK_DIR, exist_ok=True) if not os.access(MIRROR_WORK_DIR, os.W_OK): raise OSError(f"Directory {MIRROR_WORK_DIR} is not writable.") except OSError as e: logging.error(f"Mirror working directory error: {e}") return for item in os.listdir(LOCAL_REPOS_BASE_DIR): local_repo_path_on_server = os.path.join(LOCAL_REPOS_BASE_DIR, item) if os.path.isdir(local_repo_path_on_server) and item.endswith(".git"): repo_name_with_git = item repo_name = item[:-4] logging.info(f"\nProcessing repository: {repo_name}") local_mirror_clone_path = os.path.join(MIRROR_WORK_DIR, repo_name_with_git) github_repo_url = f"git@github.com:{GITHUB_USER_OR_ORG}/{repo_name}.git" local_server_source_url = f"file://{os.path.abspath(local_repo_path_on_server)}" try: if not os.path.isdir(local_mirror_clone_path): logging.info(f"Action: New repository or first-time sync for '{repo_name}'.") if USE_GH_FOR_REPO_CREATION: logging.info(f"Checking if repository '{GITHUB_USER_OR_ORG}/{repo_name}' exists on GitHub...") try: gh_view_cmd = ["gh", "repo", "view", f"{GITHUB_USER_OR_ORG}/{repo_name}"] process_gh_view = run_command(gh_view_cmd, check=False, suppress_output=True) if process_gh_view.returncode != 0: logging.info(f"Repository does not appear to exist on GitHub (gh repo view failed). Attempting to create it...") gh_create_cmd = [ "gh", "repo", "create", f"{GITHUB_USER_OR_ORG}/{repo_name}", GH_REPO_VISIBILITY, "--description", f"Mirror of git.fddl.dev repo {repo_name}" ] run_command(gh_create_cmd) logging.info(f"Successfully created '{GITHUB_USER_OR_ORG}/{repo_name}' on GitHub via gh CLI.") else: logging.info(f"Repository '{GITHUB_USER_OR_ORG}/{repo_name}' already exists on GitHub.") except FileNotFoundError: logging.error("GitHub CLI 'gh' not found. Cannot auto-create repositories. Please install it or set USE_GH_FOR_REPO_CREATION to False.") logging.info("You'll need to ensure the repository exists on GitHub manually for the push to succeed.") except subprocess.CalledProcessError as e_gh: logging.error(f"Error during GitHub repo check/create for {repo_name}: {e_gh}") logging.info("Please ensure the repository exists on GitHub manually for the push to succeed.") logging.info(f"Cloning from local server: '{local_server_source_url}' to '{local_mirror_clone_path}'") run_command(["git", "clone", "--mirror", local_server_source_url, local_mirror_clone_path]) logging.info(f"Adding GitHub remote 'github' with URL '{github_repo_url}'") try: run_command(["git", "remote", "add", "github", github_repo_url], cwd=local_mirror_clone_path) except subprocess.CalledProcessError: logging.warning("Failed to add GitHub remote (it might already exist). Attempting to set URL.") run_command(["git", "remote", "set-url", "github", github_repo_url], cwd=local_mirror_clone_path) logging.info("GitHub remote 'github' configured.") else: logging.info(f"Action: Existing repository '{repo_name}' in work area. Fetching updates from local server.") logging.info(f"Ensuring 'origin' remote in '{local_mirror_clone_path}' points to '{local_server_source_url}'") run_command(["git", "remote", "set-url", "origin", local_server_source_url], cwd=local_mirror_clone_path) logging.info("Fetching from 'origin' (local server)...") run_command(["git", "fetch", "--prune", "origin"], cwd=local_mirror_clone_path) logging.info("Successfully fetched updates from local server.") logging.info(f"Pushing mirror of '{repo_name}' to GitHub remote: '{github_repo_url}'") run_command(["git", "push", "--mirror", "github"], cwd=local_mirror_clone_path) logging.info(f"Successfully mirrored '{repo_name}' to GitHub.") except subprocess.CalledProcessError: logging.error(f"A Git command failed for repository '{repo_name}'. Details logged above. Skipping further actions for this repo.") except FileNotFoundError: logging.error(f"A required command (like git) was not found while processing '{repo_name}'.") return except Exception as e: logging.error(f"An unexpected error occurred while processing repository '{repo_name}': {str(e)}") logging.info(f"--- GitHub Mirror Sync (Python) Finished: {datetime.now()} ---") logging.info("") if __name__ == "__main__": main()