Initial commit
This commit is contained in:
399
python/main.py
Normal file
399
python/main.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
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_config_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."""
|
||||
# Get git user name for the appdata path
|
||||
git_user = get_git_user_name()
|
||||
if not git_user:
|
||||
git_user = "Default"
|
||||
|
||||
# Construct appdata path: {appdata}/{git_user.name}/GitRepoSetup
|
||||
if platform.system() == "Windows":
|
||||
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
||||
else:
|
||||
appdata = Path.home() / ".local" / "share"
|
||||
|
||||
data_dir = appdata / git_user / "GitRepoSetup"
|
||||
|
||||
# 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."""
|
||||
config_dir = Path(user_config_dir("GitRepoSetup", "WorldTeacher"))
|
||||
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()
|
||||
|
||||
# Get current folder name for {repo}
|
||||
current_folder = Path.cwd().name
|
||||
|
||||
remote_url = (
|
||||
config.get("default_git_url", "")
|
||||
.replace("{owner}", config["owner"])
|
||||
.replace("{repo}", current_folder)
|
||||
)
|
||||
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
|
||||
else:
|
||||
# Default to current folder name if not specified
|
||||
config["name"] = Path.cwd().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"):
|
||||
print("Error: --owner is 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()
|
||||
Reference in New Issue
Block a user