388 lines
12 KiB
Python
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()
|