Initial commit

This commit is contained in:
2025-11-04 15:40:02 +01:00
commit 3658b090e0
42 changed files with 5429 additions and 0 deletions

1
python/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Python package for gitreposetup."""

399
python/main.py Normal file
View 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()

101
python/test_main.py Normal file
View File

@@ -0,0 +1,101 @@
"""
Simple tests for gitreposetup Python implementation.
"""
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent))
from main import (
DeployType,
License,
decode_html_entities,
get_config_path,
get_license_content,
load_config,
)
def test_config_path():
"""Test config path generation."""
config_path = get_config_path()
assert ".config" in str(config_path)
assert "GMS" in str(config_path)
assert ".config.yaml" in str(config_path)
print(f"✓ Config path: {config_path}")
def test_load_config():
"""Test config loading."""
config = load_config()
assert isinstance(config, dict)
assert "owner" in config
assert "license" in config
assert config["license"] == "MIT"
assert config["develop_branch"] == "dev"
print("✓ Config loaded successfully")
def test_license_enum():
"""Test license enumeration."""
assert License.MIT.value == "MIT.txt"
assert License.GPLv3.value == "GPL-3.0.txt"
assert License.AGPLv3.value == "AGPL-3.0.txt"
assert License.Unlicense.value == "Unlicense.txt"
print("✓ License enum correct")
def test_deploy_type_enum():
"""Test deploy type enumeration."""
assert DeployType.DOCKER.value == "docker"
assert DeployType.PYPI.value == "pypi"
assert DeployType.CARGO.value == "cargo"
assert DeployType.GO.value == "go"
print("✓ DeployType enum correct")
def test_decode_html_entities():
"""Test HTML entity decoding."""
text = "<test> & "quoted""
decoded = decode_html_entities(text)
assert decoded == '<test> & "quoted"'
print("✓ HTML entities decoded")
def test_license_content():
"""Test license content generation."""
license_content = get_license_content(License.MIT, "TestOrg")
assert "MIT License" in license_content
assert "2025" in license_content # Current year
assert "TestOrg" in license_content or "Copyright" in license_content
assert "{year}" not in license_content
assert "{fullname}" not in license_content
print("✓ License content generated with substitutions")
def run_all_tests():
"""Run all tests."""
print("\n=== Running Python Implementation Tests ===\n")
try:
test_config_path()
test_load_config()
test_license_enum()
test_deploy_type_enum()
test_decode_html_entities()
test_license_content()
print("\n=== All tests passed! ===\n")
return 0
except AssertionError as e:
print(f"\n✗ Test failed: {e}\n")
return 1
except Exception as e:
print(f"\n✗ Error: {e}\n")
return 1
if __name__ == "__main__":
sys.exit(run_all_tests())

80
python/workflows.py Normal file
View File

@@ -0,0 +1,80 @@
"""
Workflow template manager for gitreposetup.
This module would handle dynamic workflow generation.
Currently workflows are pre-created in .gitea/workflows/
"""
from pathlib import Path
from typing import List
class WorkflowManager:
"""Manages Gitea workflow templates."""
def __init__(self, repo_root: Path = None):
"""Initialize with repository root."""
self.repo_root = repo_root or Path.cwd()
self.gitea_dir = self.repo_root / ".gitea"
self.workflows_dir = self.gitea_dir / "workflows"
def setup_workflows(self, deploy_type: str, language: str = "python"):
"""
Set up Gitea workflows based on deploy type and language.
Args:
deploy_type: One of 'docker', 'pypi', 'cargo', 'go'
language: Programming language ('python', 'rust', 'go')
"""
# Create directories
self.gitea_dir.mkdir(exist_ok=True)
self.workflows_dir.mkdir(exist_ok=True)
# Workflow mapping
workflows_to_copy = self._get_workflows_for_type(deploy_type, language)
# Copy workflows (if templates exist)
for workflow in workflows_to_copy:
print(f" - Setting up workflow: {workflow}")
print(f"✓ Set up {len(workflows_to_copy)} workflows for {deploy_type}")
def _get_workflows_for_type(self, deploy_type: str, language: str) -> List[str]:
"""Get list of workflow files needed for deploy type and language."""
workflows = []
if deploy_type == "docker":
workflows.extend(
[
f"test-{language}.yml",
f"test-{language}-docker-build.yml",
"docker-release.yml",
]
)
elif deploy_type == "pypi":
workflows.extend(
[
"test-python.yml",
"python_package-release.yml",
]
)
elif deploy_type == "cargo":
workflows.extend(
[
"test-rust.yml",
"cargo-release.yml",
]
)
elif deploy_type == "go":
workflows.extend(
[
"test-go.yml",
"go-release.yml",
]
)
return workflows
# For future implementation:
# This would copy workflow files from templates embedded in the binary
# or from a templates directory in the package.