Files
coding-agent-gitea/gitea/tools/pr_tools.py
T
meeks dfdd8c0931 fix: improve prompts, error messages, and workspace concurrency
- Externalize coordinator, notification, and planning prompts to separate files
- Add workspace mutex for concurrent file operations
- Improve error messages across file_tools, issue_tools, and pr_tools
- Add logging to tool modules for better debugging
- Update tests to match new error message strings
2026-08-01 20:01:46 +02:00

282 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tools for Gitea pull request operations."""
import json
import logging
from typing import Any
from gitea.client import GiteaClient
from gitea.models import PullRequestModel, CommentModel
logger: logging.Logger = logging.getLogger("gitea.tools.pr_tools")
_MAX_DIFF_CHARS: int = 15_000
def _truncate_diff(
text: str,
max_chars: int = _MAX_DIFF_CHARS,
char_offset: int = 0,
) -> str:
"""Slice [char_offset : char_offset+max_chars] from text, cutting at a hunk boundary."""
total: int = len(text)
chunk: str = text[char_offset : char_offset + max_chars]
if char_offset > 0 or (char_offset + max_chars) < total:
# Try to cut at a diff hunk boundary (@@) for coherence
hunk_boundary: int = chunk.rfind("\n@@")
if hunk_boundary > int(len(chunk) * 0.6):
chunk = chunk[:hunk_boundary]
next_offset: int = char_offset + len(chunk)
chunk += (
f"\n\n[Diff truncated — {total} chars total. "
f"Showing chars {char_offset}{next_offset}. "
f"Re-call with char_offset={next_offset} to read more.]"
)
return chunk
class PRTools:
"""Tools for Gitea pull request operations."""
def __init__(self, client: GiteaClient) -> None:
self._client = client
def get_pull_request(
self, owner: str, repo: str, pull_number: int
) -> PullRequestModel:
try:
return self._client.prs.get_pull_request(owner, repo, pull_number)
except Exception as e:
logger.error(
f"Error getting pull request #{pull_number}: {e}", exc_info=True
)
raise
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
try:
self._client.prs.close_pull_request(owner, repo, pull_number)
return f"Pull request #{pull_number} closed successfully."
except Exception as e:
logger.error(
f"Failed to close PR #{pull_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not close PR #{pull_number} in {owner}/{repo}. "
f"Check permissions or if the PR is already closed. Details: {e}"
)
def get_pull_request_comments(
self,
owner: str,
repo: str,
pull_number: int,
limit: int = 20,
offset: int = 0,
) -> str:
"""Get comments on a pull request with optional paging.
Args:
limit: Maximum number of comments to return (default 20).
offset: Zero-based comment index to start from (default 0).
"""
try:
comments: list[CommentModel] = self._client.prs.get_pull_request_comments(
owner, repo, pull_number
)
total: int = len(comments)
page: list[CommentModel] = comments[offset : offset + limit]
result: str = json.dumps([c.model_dump() for c in page], indent=2)
if total > offset + limit:
next_offset: int = offset + limit
result += (
f"\n\n[{total} comments total — showing {offset}{offset + len(page)}. "
f"Re-call with offset={next_offset} to see more.]"
)
return result
except Exception as e:
logger.error(
f"Failed to get comments for PR #{pull_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not retrieve comments for PR #{pull_number} in {owner}/{repo}. "
f"Verify the PR exists and you have access. Details: {e}"
)
def list_assigned_pull_requests(self) -> list[dict[str, Any]]:
try:
repos = self._client.repos.list_all_user_repos()
all_prs: list[dict[str, Any]] = []
for repo_info in repos:
repo_owner = repo_info.owner
repo_name = repo_info.name
prs = self._client.prs.list_assigned_pull_requests(
repo_owner, repo_name
)
if prs:
all_prs.extend(
[
pr.model_dump() if hasattr(pr, "model_dump") else pr
for pr in prs
]
)
return all_prs
except Exception as e:
logger.error(f"Error listing assigned pull requests: {e}", exc_info=True)
return []
def list_pull_requests(self, owner: str, repo: str, state: str = "open") -> str:
try:
prs = self._client.prs.list_repo_pull_requests(owner, repo, state)
if not prs:
return f"No {state} PRs in {owner}/{repo}."
summary = [f"#{pr.number}: {pr.title}" for pr in prs]
return "\n".join(summary)
except Exception as e:
logger.error(
f"Failed to list {state} PRs in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not list PRs for {owner}/{repo}. "
f"Verify the repository exists and you have access. Details: {e}"
)
def create_pull_request(
self,
owner: str,
repo: str,
head: str,
base: str,
title: str,
description: str = "",
) -> PullRequestModel:
try:
return self._client.prs.create_pr_via_tea(
owner, repo, title, description, head, base
)
except Exception as e:
logger.error(f"Error creating PR in {owner}/{repo}: {e}", exc_info=True)
raise
def update_pull_request(
self,
owner: str,
repo: str,
pull_number: int,
title: str | None = None,
body: str | None = None,
state: str | None = None,
) -> PullRequestModel:
try:
return self._client.prs.update_pull_request(
owner, repo, pull_number, title, body, state
)
except Exception as e:
logger.error(f"Error updating PR #{pull_number}: {e}", exc_info=True)
raise
def add_label_to_pr(self, owner: str, repo: str, pr_number: int, label: str) -> str:
try:
self._client.prs.add_label_pr(owner, repo, pr_number, label)
return f"Label '{label}' added to PR #{pr_number}."
except Exception as e:
logger.error(
f"Failed to add label '{label}' to PR #{pr_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not add label '{label}' to PR #{pr_number} in {owner}/{repo}. "
f"Verify the label exists in the repository. Details: {e}"
)
def get_pull_request_diff(
self,
owner: str,
repo: str,
pull_number: int,
max_chars: int = _MAX_DIFF_CHARS,
char_offset: int = 0,
) -> str:
"""Get the diff of a pull request, with truncation and offset paging.
Args:
max_chars: Maximum characters to return (default 15 000).
char_offset: Character offset to start reading from (default 0).
Increment by max_chars to page through a large diff.
"""
try:
diff: str = self._client.prs.get_pull_request_diff(owner, repo, pull_number)
return _truncate_diff(diff, max_chars, char_offset)
except Exception as e:
logger.error(
f"Failed to get diff for PR #{pull_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not retrieve diff for PR #{pull_number} in {owner}/{repo}. "
f"The PR may have no changes or the API is unavailable. Details: {e}"
)
def get_pull_request_patch(
self,
owner: str,
repo: str,
pull_number: int,
max_chars: int = _MAX_DIFF_CHARS,
char_offset: int = 0,
) -> str:
"""Get the patch of a pull request, with truncation and offset paging.
Args:
max_chars: Maximum characters to return (default 15 000).
char_offset: Character offset to start reading from (default 0).
Increment by max_chars to page through a large patch.
"""
try:
patch: str = self._client.prs.get_pull_request_patch(
owner, repo, pull_number
)
return _truncate_diff(patch, max_chars, char_offset)
except Exception as e:
logger.error(
f"Failed to get patch for PR #{pull_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not retrieve patch for PR #{pull_number} in {owner}/{repo}. "
f"The PR may have no changes or the API is unavailable. Details: {e}"
)
def approve_pull_request(
self, owner: str, repo: str, pull_number: int, comment: str
) -> str:
try:
self._client.prs.approve_pr(owner, repo, pull_number, comment)
return f"Approved PR #{pull_number}."
except Exception as e:
logger.error(
f"Failed to approve PR #{pull_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not approve PR #{pull_number} in {owner}/{repo}. "
f"Check that you have review permissions. Details: {e}"
)
def request_changes(
self, owner: str, repo: str, pull_number: int, comment: str
) -> str:
try:
self._client.prs.request_changes_pr(owner, repo, pull_number, comment)
return f"Requested changes on PR #{pull_number}."
except Exception as e:
logger.error(
f"Failed to request changes on PR #{pull_number} in {owner}/{repo}: {e}",
exc_info=True,
)
return (
f"Error: Could not request changes on PR #{pull_number} in {owner}/{repo}. "
f"Check that you have review permissions. Details: {e}"
)