#!/usr/bin/env python3 """ Gitea Sync - Automatically sync repositories to Gitea Creates/updates repositories and pushes code automatically. Auto-detects new vs existing repositories. Usage: python3 sync.py --repo my-website --path ./my-website """ import os import sys import json import argparse import requests import subprocess from pathlib import Path def load_env(): """Load environment from .env file.""" env_path = Path(__file__).parent / ".env" if env_path.exists(): for line in env_path.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) os.environ.setdefault(k.strip(), v.strip().strip("\"'")) load_env() GITEA_URL = os.environ.get("GITEA_URL", "https://git.moreminimore.com") GITEA_API_TOKEN = os.environ.get("GITEA_API_TOKEN") GITEA_USERNAME = os.environ.get("GITEA_USERNAME") def check_auth(): """Verify Gitea authentication.""" if not GITEA_API_TOKEN: print("Error: GITEA_API_TOKEN not set", file=sys.stderr) sys.exit(1) response = requests.get( f"{GITEA_URL}/api/v1/user", headers={"Authorization": f"token {GITEA_API_TOKEN}"} ) if response.status_code != 200: print(f"Error: Gitea authentication failed ({response.status_code})", file=sys.stderr) print(f"Check your API token at: {GITEA_URL}/user/settings/applications", file=sys.stderr) sys.exit(1) user = response.json() return user.get("login", GITEA_USERNAME) def repo_exists(username, repo_name): """Check if repository exists on Gitea.""" response = requests.get( f"{GITEA_URL}/api/v1/repos/{username}/{repo_name}", headers={"Authorization": f"token {GITEA_API_TOKEN}"} ) return response.status_code == 200 def create_repo(repo_name, description="", private=False): """Create new repository on Gitea.""" print(f"đŸ“Ļ Creating repository: {repo_name}") data = { "name": repo_name, "description": description, "private": private, "auto_init": True, "readme": "Default", "default_branch": "main" } response = requests.post( f"{GITEA_URL}/api/v1/user/repos", headers={"Authorization": f"token {GITEA_API_TOKEN}"}, json=data ) if response.status_code == 201: print(f"✅ Repository created: {repo_name}") return response.json() elif response.status_code == 409: print(f"âš ī¸ Repository already exists: {repo_name}") return None else: print(f"❌ Failed to create repository: {response.text}", file=sys.stderr) sys.exit(1) def update_repo(repo_name, description=""): """Update existing repository.""" print(f"🔄 Updating repository: {repo_name}") data = { "description": description, "website": "", "has_issues": True, "has_pull_requests": True, "has_wiki": False } response = requests.patch( f"{GITEA_URL}/api/v1/repos/{GITEA_USERNAME}/{repo_name}", headers={"Authorization": f"token {GITEA_API_TOKEN}"}, json=data ) if response.status_code == 200: print(f"✅ Repository updated: {repo_name}") return response.json() else: print(f"âš ī¸ Could not update repository: {response.text}") return None def get_repo_url(username, repo_name): """Get HTTPS URL for repository.""" return f"{GITEA_URL}/{username}/{repo_name}.git" def is_git_repo(path): """Check if directory is a git repository.""" git_dir = Path(path) / ".git" return git_dir.exists() def push_code(repo_path, git_url, branch="main"): """Push code to Gitea repository.""" repo_path = Path(repo_path) if not repo_path.exists(): print(f"Error: Path does not exist: {repo_path}", file=sys.stderr) sys.exit(1) print(f"🚀 Pushing code to Gitea...") # Initialize git if needed if not is_git_repo(repo_path): print(" → Initializing git repository") subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True) # Configure git to use token for authentication # This avoids interactive password prompts subprocess.run( ["git", "config", "credential.helper", "store"], cwd=repo_path, check=True, capture_output=True ) # Add .gitignore if not exists gitignore = repo_path / ".gitignore" if not gitignore.exists(): with open(gitignore, "w") as f: f.write("""node_modules dist .env .astro *.db *.log .DS_Store """) # Add remote if not exists result = subprocess.run( ["git", "remote", "get-url", "origin"], cwd=repo_path, capture_output=True ) if result.returncode != 0: print(f" → Adding remote: {git_url}") # Use token in URL for authentication auth_url = git_url.replace( f"{GITEA_URL}/", f"{GITEA_URL}/{GITEA_API_TOKEN}:@" ) subprocess.run( ["git", "remote", "add", "origin", auth_url], cwd=repo_path, check=True, capture_output=True ) else: # Update existing remote with auth auth_url = git_url.replace( f"{GITEA_URL}/", f"{GITEA_URL}/{GITEA_API_TOKEN}:@" ) subprocess.run( ["git", "remote", "set-url", "origin", auth_url], cwd=repo_path, check=True, capture_output=True ) # Add all files print(" → Adding files") subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True) # Check if there are changes to commit result = subprocess.run( ["git", "status", "--porcelain"], cwd=repo_path, capture_output=True, text=True ) if result.stdout.strip(): # Commit changes print(" → Committing changes") subprocess.run( ["git", "commit", "-m", "Auto-sync from website-creator"], cwd=repo_path, check=True, capture_output=True ) # Set main as default branch subprocess.run( ["git", "branch", "-M", branch], cwd=repo_path, check=True, capture_output=True ) # Push with force to handle initial push print(" → Pushing to Gitea") result = subprocess.run( ["git", "push", "-u", "-f", "origin", branch], cwd=repo_path, capture_output=True, text=True ) if result.returncode == 0: print(f"✅ Code pushed successfully") return True else: print(f"âš ī¸ Push output: {result.stderr}") # Try without -f if it fails subprocess.run( ["git", "push", "-u", "origin", branch], cwd=repo_path, capture_output=True ) print(f"✅ Code pushed (without force)") return True else: print(f"â„šī¸ No changes to push") return True def sync_repo(repo_name, repo_path, description="", auto_push=True): """Complete sync workflow.""" # Step 1: Check auth username = check_auth() print(f"🔐 Authenticated as: {username}") print("") # Step 2: Check if repo exists exists = repo_exists(username, repo_name) if exists: update_repo(repo_name, description) else: create_repo(repo_name, description) print("") # Step 3: Push code if auto_push: git_url = get_repo_url(username, repo_name) push_code(repo_path, git_url) print("") print(f"🌐 Repository URL: {git_url.replace('.git', '')}") return { "username": username, "repo_name": repo_name, "git_url": get_repo_url(username, repo_name), "created": not exists } def main(): parser = argparse.ArgumentParser(description="Sync repository to Gitea") parser.add_argument("--repo", required=True, help="Repository name") parser.add_argument("--path", required=True, help="Path to repository") parser.add_argument("--description", default="", help="Repository description") parser.add_argument("--no-push", action="store_true", help="Don't push code") parser.add_argument("--private", action="store_true", help="Make repository private") args = parser.parse_args() print("🔄 Gitea Sync") print("=" * 50) print(f"Repository: {args.repo}") print(f"Path: {args.path}") print(f"Description: {args.description or '(none)'}") print("=" * 50) print("") result = sync_repo( args.repo, args.path, args.description, auto_push=not args.no_push ) print("") print("=" * 50) print("✅ Sync complete!") print(f"Repository: {result['repo_name']}") print(f"URL: {result['git_url'].replace('.git', '')}") if result['created']: print("Status: Created new repository") else: print("Status: Updated existing repository") print("=" * 50) if __name__ == "__main__": main()