""" GitRepoSetup - A tool to initialize and configure git repositories with licenses and workflows. """ import argparse import html import os import platform import shutil import subprocess import sys from datetime import datetime from enum import Enum from pathlib import Path from typing import Optional try: import yaml except ImportError: print("Error: PyYAML is required. Install with: pip install pyyaml") sys.exit(1) try: from appdirs import user_data_dir except ImportError: print("Error: appdirs is required. Install with: pip install appdirs") sys.exit(1) class License(Enum): """Supported license types.""" MIT = "MIT.txt" GPLv3 = "GPL-3.0.txt" AGPLv3 = "AGPL-3.0.txt" Unlicense = "Unlicense.txt" class DeployType(Enum): """Supported deployment types for Gitea workflows.""" DOCKER = "docker" PYPI = "pypi" CARGO = "cargo" GO = "go" def get_package_data_dir() -> Path: """Get the package data directory containing licenses and workflows.""" # When installed as a package, data is in appdirs user_data_dir data_dir = Path(user_data_dir("GitRepoSetup", "WorldTeacher")) # For development, fall back to repo structure if not data_dir.exists(): # Try to find the repo root (parent of python/ directory) current = Path(__file__).parent.parent if (current / "licenses").exists(): return current return data_dir def get_config_path() -> Path: """Get the path to the config file.""" if platform.system() == "Windows": config_dir = Path(os.environ.get("USERPROFILE", "~")) / ".config" / "GMS" else: config_dir = Path.home() / ".config" / "GMS" config_dir.mkdir(parents=True, exist_ok=True) return config_dir / ".config.yaml" def load_config() -> dict: """Load configuration from config file.""" config_path = get_config_path() if not config_path.exists(): # Create default config default_config = { "owner": "", "name": "", "license": "MIT", "develop_branch": "dev", "default_gitignore": True, "default_git_url": "https://git.theprivateserver.de/{owner}/{repo}.git", } with open(config_path, "w", encoding="utf-8") as f: yaml.dump(default_config, f, default_flow_style=False) print(f"Created default config at: {config_path}") return default_config with open(config_path, "r", encoding="utf-8") as f: return yaml.safe_load(f) def run_git_command(args: list) -> str: """Run a git command and return output.""" try: result = subprocess.run( ["git"] + args, check=True, capture_output=True, text=True ) return result.stdout.strip() except subprocess.CalledProcessError as e: raise RuntimeError(f"Git command failed: {e.stderr}") def is_git_repo() -> bool: """Check if current directory is a git repository.""" try: run_git_command(["status"]) return True except RuntimeError: return False def get_git_user_name() -> Optional[str]: """Get git user.name from config.""" try: return run_git_command(["config", "user.name"]) except RuntimeError: return None def decode_html_entities(text: str) -> str: """Decode HTML entities in text.""" return html.unescape(text) def get_license_content(license_type: License, owner: str) -> str: """Get license content with substitutions.""" data_dir = get_package_data_dir() license_path = data_dir / "licenses" / license_type.value if not license_path.exists(): raise FileNotFoundError(f"License file not found: {license_path}") content = license_path.read_text(encoding="utf-8") # Decode HTML entities content = decode_html_entities(content) # Substitute placeholders year = str(datetime.now().year) fullname = get_git_user_name() or owner content = content.replace("{year}", year) content = content.replace("{fullname}", fullname) return content def remove_all_remotes(): """Remove all existing git remotes.""" try: remotes = run_git_command(["remote"]).split("\n") for remote in remotes: if remote: run_git_command(["remote", "remove", remote]) print(f"Removed remote: {remote}") except RuntimeError: pass def setup_git_repo(config: dict, force_license: bool = False): """Set up or configure a git repository.""" # Initialize git if not already a repo if not is_git_repo(): run_git_command(["init"]) print("Initialized git repository") # Ensure we're on main branch try: current_branch = run_git_command(["branch", "--show-current"]) if current_branch != "main": run_git_command(["checkout", "-b", "main"]) except RuntimeError: run_git_command(["checkout", "-b", "main"]) # Force-remove all existing remotes and set new one remove_all_remotes() remote_url = ( config.get("default_git_url", "") .replace("{owner}", config["owner"]) .replace("{repo}", config["name"]) ) if remote_url: run_git_command(["remote", "add", "origin", remote_url]) print(f"Set remote to: {remote_url}") # Add/update LICENSE license_path = Path("LICENSE") if not license_path.exists() or force_license: license_type = License[config.get("license", "MIT")] license_content = get_license_content(license_type, config["owner"]) license_path.write_text(license_content, encoding="utf-8") print(f"Created LICENSE: {license_type.name}") else: print("LICENSE exists (use --force to overwrite)") # Add/update .gitignore if config.get("default_gitignore", True): data_dir = get_package_data_dir() gitignore_source = data_dir / ".gitignore" if gitignore_source.exists(): gitignore_path = Path(".gitignore") shutil.copy2(gitignore_source, gitignore_path) print("Updated .gitignore from package data") else: print("⚠️ Warning: .gitignore not found in package data") # Commit changes run_git_command(["add", "."]) try: run_git_command(["commit", "-m", "Initial commit"]) print("Created initial commit") except RuntimeError: print("Nothing to commit") # Create and switch to dev branch dev_branch = config.get("develop_branch", "dev") try: run_git_command(["checkout", "-b", dev_branch]) print(f"Created and switched to branch: {dev_branch}") except RuntimeError: print(f"Branch {dev_branch} already exists") # Push to remote (with network error handling) try: run_git_command(["push", "-u", "origin", "main"]) run_git_command(["push", "-u", "origin", dev_branch]) print("Pushed to remote successfully") except RuntimeError: print( "⚠️ Warning: Could not push to remote. Network may be unavailable or remote not accessible." ) print(" Repository configured locally. Push manually when ready.") def setup_gitea_workflows(deploy_type: DeployType): """Set up Gitea workflows based on deploy type.""" data_dir = get_package_data_dir() source_workflows_dir = data_dir / ".gitea" / "workflows" if not source_workflows_dir.exists(): print(f"⚠️ Warning: Workflows directory not found at {source_workflows_dir}") print(" Ensure the package data is properly installed.") return gitea_dir = Path(".gitea") workflows_dir = gitea_dir / "workflows" # Create directories gitea_dir.mkdir(exist_ok=True) workflows_dir.mkdir(exist_ok=True) # Copy changelog config changelog_config = source_workflows_dir.parent / "changelog_config.json" if changelog_config.exists(): shutil.copy2(changelog_config, gitea_dir / "changelog_config.json") print("Copied changelog_config.json") # Determine which language-specific workflows to copy language_map = { DeployType.DOCKER: "python", # Default to Python for docker DeployType.PYPI: "python", DeployType.CARGO: "rust", DeployType.GO: "go", } language = language_map.get(deploy_type, "python") # Copy all workflow files that match the language copied_count = 0 for workflow_file in source_workflows_dir.glob("*.yml"): filename = workflow_file.name.lower() # Check if workflow matches the primary language pattern match = False # Always check against the main language if ( filename.startswith(language) or filename.startswith(f"{language}-") or filename.startswith(f"{language}_") or f"-{language}." in filename or f"-{language}-" in filename ): match = True # For CARGO, also match "cargo" prefix (cargo-release.yml) if deploy_type == DeployType.CARGO and filename.startswith("cargo"): match = True # For DOCKER, also match docker-release.yml specifically if deploy_type == DeployType.DOCKER and filename == "docker-release.yml": match = True if match: dest = workflows_dir / workflow_file.name shutil.copy2(workflow_file, dest) print(f" Copied: {workflow_file.name}") copied_count += 1 print( f"Set up Gitea workflows for: {deploy_type.value} ({language}) - {copied_count} files copied" ) print("⚠️ Note: Ensure required secrets are configured in Gitea user/org settings:") print(" - GITEA_TOKEN, TOKEN, REGISTRY, DOCKER_USERNAME") if deploy_type == DeployType.DOCKER: print( "⚠️ Warning: Docker workflows require a Dockerfile in the repository root." ) def main(): """Main entry point.""" parser = argparse.ArgumentParser( description="Initialize and configure git repositories with licenses and workflows" ) parser.add_argument("--owner", type=str, help="Repository owner/organization name") parser.add_argument("--name", type=str, help="Repository name") parser.add_argument( "--license", type=str, choices=[l.name for l in License], help="License type" ) parser.add_argument( "--develop-branch-name", type=str, help="Development branch name (default: dev)" ) parser.add_argument("--default-gitignore", type=bool, help="Use default .gitignore") parser.add_argument("--default-git-url", type=str, help="Git remote URL template") parser.add_argument( "--force", action="store_true", help="Force overwrite existing LICENSE" ) parser.add_argument( "--deploy-type", type=str, choices=[d.value for d in DeployType], help="Deployment type for Gitea workflows", ) args = parser.parse_args() # Load config config = load_config() # Override config with CLI arguments if args.owner: config["owner"] = args.owner if args.name: config["name"] = args.name if args.license: config["license"] = args.license if args.develop_branch_name: config["develop_branch"] = args.develop_branch_name if args.default_gitignore is not None: config["default_gitignore"] = args.default_gitignore if args.default_git_url: config["default_git_url"] = args.default_git_url # Validate required fields if not config.get("owner") or not config.get("name"): print("Error: --owner and --name are required (or set in config file)") sys.exit(1) # Setup git repository try: setup_git_repo(config, force_license=args.force) except Exception as e: print(f"Error setting up git repository: {e}") sys.exit(1) # Setup workflows if requested if args.deploy_type: deploy_type = DeployType(args.deploy_type) setup_gitea_workflows(deploy_type) print("\n✅ Repository setup complete!") if __name__ == "__main__": main()