Files
WorldTeacher/python/main.py
2025-11-04 15:28:26 +01:00

388 lines
12 KiB
Python

"""
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()