Files
markitect-main/tddai/issue_writer.py
tegwick 2b681b31c6 feat: Implement comprehensive project management system with issue lifecycle support
- Add ProjectManager with milestone and label-based project organization
- Support project states (Todo, Active, Review, Done, Blocked) via labels
- Add priority management (Low, Medium, High, Critical) with label integration
- Implement milestone creation and management for project tracking
- Enhance IssueWriter with project management methods (assign_to_milestone, add/remove_labels)
- Add 8 new CLI commands for complete project management workflow
- Support automatic project management setup with ensure_project_labels()
- Enable issue state transitions with automatic closing for completed issues
- Integrate with existing Gitea API authentication and error handling patterns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 23:51:29 +02:00

138 lines
5.5 KiB
Python

"""
Issue writing to Gitea API.
"""
import json
import os
import subprocess
from subprocess import PIPE
from typing import Dict, Any, Optional
from .config import get_config
from .exceptions import IssueError
class IssueWriter:
"""Writes issue updates to Gitea API."""
def __init__(self, config=None, auth_token=None):
self.config = config or get_config()
self.auth_token = auth_token or os.getenv('GITEA_API_TOKEN')
def update_issue(self, issue_number: int, update_data: Dict[str, Any]) -> Dict[str, Any]:
"""Update an issue via PATCH operation."""
if not self.auth_token:
raise IssueError("Authentication token required for issue updates")
url = f"{self.config.issues_api_url}/{issue_number}"
try:
# Prepare curl command with authentication
curl_cmd = [
'curl', '-s', '-X', 'PATCH',
'-H', 'Content-Type: application/json',
'-H', f'Authorization: token {self.auth_token}',
'-d', json.dumps(update_data),
url
]
result = subprocess.run(
curl_cmd,
stdout=PIPE,
stderr=PIPE,
universal_newlines=True,
check=True
)
if result.returncode != 0:
raise IssueError(f"Failed to update issue #{issue_number}: {result.stderr}")
response_data = json.loads(result.stdout)
if 'message' in response_data and 'number' not in response_data:
raise IssueError(f"Failed to update issue #{issue_number}: {response_data['message']}")
return response_data
except subprocess.CalledProcessError as e:
raise IssueError(f"Failed to update issue #{issue_number}: {e}")
except json.JSONDecodeError as e:
raise IssueError(f"Failed to parse response data: {e}")
def update_issue_title(self, issue_number: int, new_title: str) -> Dict[str, Any]:
"""Update only the title of an issue."""
return self.update_issue(issue_number, {'title': new_title})
def update_issue_body(self, issue_number: int, new_body: str) -> Dict[str, Any]:
"""Update only the body of an issue."""
return self.update_issue(issue_number, {'body': new_body})
def update_issue_state(self, issue_number: int, new_state: str) -> Dict[str, Any]:
"""Update only the state of an issue (open/closed)."""
if new_state not in ['open', 'closed']:
raise IssueError(f"Invalid state '{new_state}'. Must be 'open' or 'closed'")
return self.update_issue(issue_number, {'state': new_state})
def close_issue(self, issue_number: int) -> Dict[str, Any]:
"""Close an issue."""
return self.update_issue_state(issue_number, 'closed')
def reopen_issue(self, issue_number: int) -> Dict[str, Any]:
"""Reopen a closed issue."""
return self.update_issue_state(issue_number, 'open')
def assign_to_milestone(self, issue_number: int, milestone_id: int) -> Dict[str, Any]:
"""Assign issue to a milestone (project)."""
return self.update_issue(issue_number, {'milestone': milestone_id})
def remove_from_milestone(self, issue_number: int) -> Dict[str, Any]:
"""Remove issue from its current milestone."""
return self.update_issue(issue_number, {'milestone': None})
def update_labels(self, issue_number: int, labels: list) -> Dict[str, Any]:
"""Update issue labels completely."""
return self.update_issue(issue_number, {'labels': labels})
def add_labels(self, issue_number: int, new_labels: list) -> Dict[str, Any]:
"""Add labels to issue (preserving existing labels)."""
# First get current labels
url = f"{self.config.issues_api_url}/{issue_number}"
curl_cmd = [
'curl', '-s', '-X', 'GET',
'-H', f'Authorization: token {self.auth_token}',
url
]
try:
result = subprocess.run(curl_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=True)
issue_data = json.loads(result.stdout)
current_labels = [label['name'] for label in issue_data.get('labels', [])]
# Add new labels (avoid duplicates)
updated_labels = list(set(current_labels + new_labels))
return self.update_labels(issue_number, updated_labels)
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
raise IssueError(f"Failed to add labels to issue #{issue_number}: {e}")
def remove_labels(self, issue_number: int, labels_to_remove: list) -> Dict[str, Any]:
"""Remove specific labels from issue."""
# First get current labels
url = f"{self.config.issues_api_url}/{issue_number}"
curl_cmd = [
'curl', '-s', '-X', 'GET',
'-H', f'Authorization: token {self.auth_token}',
url
]
try:
result = subprocess.run(curl_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, check=True)
issue_data = json.loads(result.stdout)
current_labels = [label['name'] for label in issue_data.get('labels', [])]
# Remove specified labels
updated_labels = [label for label in current_labels if label not in labels_to_remove]
return self.update_labels(issue_number, updated_labels)
except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
raise IssueError(f"Failed to remove labels from issue #{issue_number}: {e}")