Initial commit
This commit is contained in:
1
python/__init__.py
Normal file
1
python/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Python package for gitreposetup."""
|
||||
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()
|
||||
101
python/test_main.py
Normal file
101
python/test_main.py
Normal 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
80
python/workflows.py
Normal 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.
|
||||
Reference in New Issue
Block a user