Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
- Add GiteaConfig.from_git_repository() method for auto-detection - Support HTTP(S) and SSH git remote URL formats - Parse gitea_url, repo_owner, repo_name from git remote origin - Only requires GITEA_API_TOKEN environment variable - Update GiteaClient to use auto-detection as primary method - Maintain backward compatibility with environment variables - Fix issue creation API to use label IDs instead of names - Add comprehensive error handling and validation - Successfully tested with issues #33 and #34 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
"""
|
|
Gitea-specific configuration management.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
from urllib.parse import urlparse
|
|
|
|
from .exceptions import GiteaConfigError
|
|
|
|
|
|
def load_dotenv_file(env_file: Path) -> None:
|
|
"""Load environment variables from a .env file."""
|
|
if not env_file.exists():
|
|
return
|
|
|
|
with open(env_file, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#') and '=' in line:
|
|
key, value = line.split('=', 1)
|
|
os.environ.setdefault(key.strip(), value.strip())
|
|
|
|
|
|
@dataclass
|
|
class GiteaConfig:
|
|
"""Configuration for Gitea API access."""
|
|
|
|
# Repository settings (required)
|
|
gitea_url: str = ""
|
|
repo_owner: str = ""
|
|
repo_name: str = ""
|
|
|
|
# Authentication (optional for read operations)
|
|
auth_token: Optional[str] = None
|
|
|
|
@property
|
|
def base_api_url(self) -> str:
|
|
"""Get the base API URL for this repository."""
|
|
return f"{self.gitea_url}/api/v1"
|
|
|
|
@property
|
|
def repo_api_url(self) -> str:
|
|
"""Get the repository API URL."""
|
|
return f"{self.base_api_url}/repos/{self.repo_owner}/{self.repo_name}"
|
|
|
|
@property
|
|
def issues_api_url(self) -> str:
|
|
"""Get the issues API URL."""
|
|
return f"{self.repo_api_url}/issues"
|
|
|
|
@property
|
|
def milestones_api_url(self) -> str:
|
|
"""Get the milestones API URL."""
|
|
return f"{self.repo_api_url}/milestones"
|
|
|
|
@property
|
|
def labels_api_url(self) -> str:
|
|
"""Get the labels API URL."""
|
|
return f"{self.repo_api_url}/labels"
|
|
|
|
@classmethod
|
|
def from_environment(cls, env_prefix: str = "GITEA") -> "GiteaConfig":
|
|
"""Create config from environment variables.
|
|
|
|
Args:
|
|
env_prefix: Environment variable prefix (default: GITEA)
|
|
Looks for {prefix}_URL, {prefix}_REPO_OWNER, etc.
|
|
"""
|
|
# Auto-load .env.gitea file if it exists
|
|
env_file = Path(".env.gitea")
|
|
load_dotenv_file(env_file)
|
|
|
|
config = cls()
|
|
|
|
# Load from environment
|
|
config.gitea_url = os.getenv(f"{env_prefix}_URL", "")
|
|
config.repo_owner = os.getenv(f"{env_prefix}_REPO_OWNER", "")
|
|
config.repo_name = os.getenv(f"{env_prefix}_REPO_NAME", "")
|
|
config.auth_token = os.getenv(f"{env_prefix}_API_TOKEN")
|
|
|
|
return config
|
|
|
|
@classmethod
|
|
def from_git_repository(cls) -> "GiteaConfig":
|
|
"""Create config by auto-detecting from current git repository.
|
|
|
|
Only requires GITEA_API_TOKEN environment variable.
|
|
All other settings are detected from git remote origin.
|
|
"""
|
|
try:
|
|
# Get git remote origin URL
|
|
result = subprocess.run(
|
|
['git', 'remote', 'get-url', 'origin'],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True
|
|
)
|
|
origin_url = result.stdout.strip()
|
|
|
|
# Parse different URL formats
|
|
if origin_url.startswith('http://') or origin_url.startswith('https://'):
|
|
# HTTP(S) format: https://gitea.example.com/owner/repo.git
|
|
parsed = urlparse(origin_url)
|
|
gitea_url = f"{parsed.scheme}://{parsed.netloc}"
|
|
path_parts = parsed.path.strip('/').split('/')
|
|
if len(path_parts) >= 2:
|
|
repo_owner = path_parts[0]
|
|
repo_name = path_parts[1].replace('.git', '')
|
|
else:
|
|
raise GiteaConfigError(f"Cannot parse repository path from URL: {origin_url}")
|
|
elif '@' in origin_url:
|
|
# SSH format: git@gitea.example.com:owner/repo.git
|
|
if ':' in origin_url:
|
|
host_part, path_part = origin_url.split(':', 1)
|
|
host = host_part.split('@')[-1]
|
|
gitea_url = f"https://{host}" # Assume HTTPS for API
|
|
path_parts = path_part.strip('/').split('/')
|
|
if len(path_parts) >= 2:
|
|
repo_owner = path_parts[0]
|
|
repo_name = path_parts[1].replace('.git', '')
|
|
else:
|
|
raise GiteaConfigError(f"Cannot parse repository path from SSH URL: {origin_url}")
|
|
else:
|
|
raise GiteaConfigError(f"Cannot parse SSH URL format: {origin_url}")
|
|
else:
|
|
raise GiteaConfigError(f"Unsupported git remote URL format: {origin_url}")
|
|
|
|
# Get auth token from environment
|
|
auth_token = os.getenv('GITEA_API_TOKEN')
|
|
|
|
return cls(
|
|
gitea_url=gitea_url,
|
|
repo_owner=repo_owner,
|
|
repo_name=repo_name,
|
|
auth_token=auth_token
|
|
)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
raise GiteaConfigError(f"Failed to get git remote origin: {e}")
|
|
except Exception as e:
|
|
raise GiteaConfigError(f"Failed to auto-detect git repository config: {e}")
|
|
|
|
@classmethod
|
|
def from_tddai_config(cls, tddai_config) -> "GiteaConfig":
|
|
"""Create GiteaConfig from legacy TddaiConfig for backwards compatibility."""
|
|
return cls(
|
|
gitea_url=tddai_config.gitea_url,
|
|
repo_owner=tddai_config.repo_owner,
|
|
repo_name=tddai_config.repo_name,
|
|
auth_token=os.getenv('GITEA_API_TOKEN')
|
|
)
|
|
|
|
def validate(self) -> None:
|
|
"""Validate configuration settings."""
|
|
if not self.gitea_url:
|
|
raise GiteaConfigError("gitea_url cannot be empty")
|
|
|
|
if not self.repo_owner:
|
|
raise GiteaConfigError("repo_owner cannot be empty")
|
|
|
|
if not self.repo_name:
|
|
raise GiteaConfigError("repo_name cannot be empty")
|
|
|
|
# Validate URL format
|
|
if not (self.gitea_url.startswith('http://') or self.gitea_url.startswith('https://')):
|
|
raise GiteaConfigError("gitea_url must start with http:// or https://")
|
|
|
|
def requires_auth(self, operation: str = "read") -> bool:
|
|
"""Check if operation requires authentication."""
|
|
write_operations = {"create", "update", "delete", "write"}
|
|
return operation in write_operations and not self.auth_token
|