refactor: extract focused clients from GiteaClient (Slices 1-6)
- Create gitea/issues_client.py with IssuesClient class (9 methods) - Create gitea/prs_client.py with PullRequestsClient class (17 methods) - Create gitea/files_client.py with FilesClient class (4 methods) - Create gitea/notifications_client.py with NotificationsClient class (2 methods) - Create gitea/repos_client.py with ReposClient class (2 methods) - Create gitea/__init__.py to export all client classes - Remove delegation methods from GiteaClient (now ~70 lines) - Update all callers to use sub-clients (client.issues, client.prs, etc.) - Update test files to mock sub-client attributes GiteaClient is now a facade that provides access to focused sub-clients: - repos: Repository operations (ReposClient) - issues: Issue operations (IssuesClient) - prs: Pull request operations (PullRequestsClient) - files: File and git ref operations (FilesClient) - notifications: Notification operations (NotificationsClient) Refs: #godclass-refactor
This commit is contained in:
+29
-25
@@ -43,7 +43,7 @@ def _find_pr_for_issue_helper(
|
|||||||
"""Find an open pull request that addresses the given issue number."""
|
"""Find an open pull request that addresses the given issue number."""
|
||||||
owner, repo_name = repo_full_name.split("/")
|
owner, repo_name = repo_full_name.split("/")
|
||||||
try:
|
try:
|
||||||
prs = client.list_repo_pull_requests(owner, repo_name)
|
prs = client.prs.list_repo_pull_requests(owner, repo_name)
|
||||||
for pr in prs:
|
for pr in prs:
|
||||||
ref = pr.head.get("ref", "") if pr.head else ""
|
ref = pr.head.get("ref", "") if pr.head else ""
|
||||||
if re.search(rf"(?<!\d){issue_number}(?!\d)", ref):
|
if re.search(rf"(?<!\d){issue_number}(?!\d)", ref):
|
||||||
@@ -195,7 +195,7 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
pr_details = pr_info.model_dump_json(indent=2)
|
pr_details = pr_info.model_dump_json(indent=2)
|
||||||
pr_diff = ""
|
pr_diff = ""
|
||||||
try:
|
try:
|
||||||
pr_diff = self.client.get_pull_request_diff(
|
pr_diff = self.client.prs.get_pull_request_diff(
|
||||||
self.owner, self.repo_name, pr_number
|
self.owner, self.repo_name, pr_number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -204,7 +204,7 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
|
|
||||||
pr_files: list[PullRequestFileModel] = []
|
pr_files: list[PullRequestFileModel] = []
|
||||||
try:
|
try:
|
||||||
pr_files = self.client.get_pull_request_files(
|
pr_files = self.client.prs.get_pull_request_files(
|
||||||
self.owner, self.repo_name, pr_number
|
self.owner, self.repo_name, pr_number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -220,7 +220,7 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
|
|
||||||
comments: list[CommentModel] = []
|
comments: list[CommentModel] = []
|
||||||
try:
|
try:
|
||||||
comments = self.client.get_pull_request_comments(
|
comments = self.client.prs.get_pull_request_comments(
|
||||||
self.owner, self.repo_name, pr_number
|
self.owner, self.repo_name, pr_number
|
||||||
)
|
)
|
||||||
if not isinstance(comments, list):
|
if not isinstance(comments, list):
|
||||||
@@ -232,7 +232,9 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
|
|
||||||
reviews: list[dict[str, Any]] = []
|
reviews: list[dict[str, Any]] = []
|
||||||
try:
|
try:
|
||||||
reviews = self.client.get_pr_reviews(self.owner, self.repo_name, pr_number)
|
reviews = self.client.prs.get_pr_reviews(
|
||||||
|
self.owner, self.repo_name, pr_number
|
||||||
|
)
|
||||||
if not isinstance(reviews, list):
|
if not isinstance(reviews, list):
|
||||||
reviews = []
|
reviews = []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -302,8 +304,10 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
issues_details = []
|
issues_details = []
|
||||||
for issue_num in linked_issues:
|
for issue_num in linked_issues:
|
||||||
try:
|
try:
|
||||||
issue = self.client.get_issue(self.owner, self.repo_name, issue_num)
|
issue = self.client.issues.get_issue(
|
||||||
issue_comments = self.client.get_issue_comments(
|
self.owner, self.repo_name, issue_num
|
||||||
|
)
|
||||||
|
issue_comments = self.client.issues.get_issue_comments(
|
||||||
self.owner, self.repo_name, issue_num
|
self.owner, self.repo_name, issue_num
|
||||||
)
|
)
|
||||||
comments_list = (
|
comments_list = (
|
||||||
@@ -392,14 +396,14 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
|
|
||||||
async def process(self, attempt_limit: int) -> str:
|
async def process(self, attempt_limit: int) -> str:
|
||||||
try:
|
try:
|
||||||
pr_detail = self.client.get_pull_request(
|
pr_detail = self.client.prs.get_pull_request(
|
||||||
self.owner, self.repo_name, self.item.task_number
|
self.owner, self.repo_name, self.item.task_number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error fetching PR #{self.item.task_number} detail: {e}")
|
logger.warning(f"Error fetching PR #{self.item.task_number} detail: {e}")
|
||||||
return f"FAILED: Could not fetch details for PR #{self.item.task_number}."
|
return f"FAILED: Could not fetch details for PR #{self.item.task_number}."
|
||||||
|
|
||||||
is_own_pr = pr_detail.user and pr_detail.user.login == self.ai_username
|
is_own_pr = bool(pr_detail.user and pr_detail.user.login == self.ai_username)
|
||||||
is_requested_reviewer = any(
|
is_requested_reviewer = any(
|
||||||
r.login == self.ai_username for r in pr_detail.requested_reviewers
|
r.login == self.ai_username for r in pr_detail.requested_reviewers
|
||||||
)
|
)
|
||||||
@@ -412,7 +416,7 @@ class PRTaskProcessor(TaskProcessor):
|
|||||||
|
|
||||||
pr_comments = []
|
pr_comments = []
|
||||||
try:
|
try:
|
||||||
pr_comments = self.client.get_pull_request_comments(
|
pr_comments = self.client.prs.get_pull_request_comments(
|
||||||
self.owner, self.repo_name, self.item.task_number
|
self.owner, self.repo_name, self.item.task_number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -490,7 +494,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
|
|
||||||
comments: list[CommentModel] = []
|
comments: list[CommentModel] = []
|
||||||
try:
|
try:
|
||||||
comments = self.client.get_issue_comments(
|
comments = self.client.issues.get_issue_comments(
|
||||||
self.owner, self.repo_name, issue_number
|
self.owner, self.repo_name, issue_number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -558,7 +562,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
is_wip = title_upper.startswith("WIP:") or "[WIP]" in title_upper
|
is_wip = title_upper.startswith("WIP:") or "[WIP]" in title_upper
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reviews = self.client.get_pr_reviews(
|
reviews = self.client.prs.get_pr_reviews(
|
||||||
self.owner, self.repo_name, existing_pr.number
|
self.owner, self.repo_name, existing_pr.number
|
||||||
)
|
)
|
||||||
has_request_changes = any(
|
has_request_changes = any(
|
||||||
@@ -578,7 +582,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
# Check comments on issue and PR
|
# Check comments on issue and PR
|
||||||
issue_comments = []
|
issue_comments = []
|
||||||
try:
|
try:
|
||||||
issue_comments = self.client.get_issue_comments(
|
issue_comments = self.client.issues.get_issue_comments(
|
||||||
self.owner, self.repo_name, self.item.task_number
|
self.owner, self.repo_name, self.item.task_number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -590,7 +594,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
pr_comments = []
|
pr_comments = []
|
||||||
if existing_pr:
|
if existing_pr:
|
||||||
try:
|
try:
|
||||||
pr_comments = self.client.get_pull_request_comments(
|
pr_comments = self.client.prs.get_pull_request_comments(
|
||||||
self.owner, self.repo_name, existing_pr.number
|
self.owner, self.repo_name, existing_pr.number
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -639,7 +643,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
else "No PR comments yet."
|
else "No PR comments yet."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
reviews = self.client.get_pr_reviews(
|
reviews = self.client.prs.get_pr_reviews(
|
||||||
self.owner, self.repo_name, existing_pr.number
|
self.owner, self.repo_name, existing_pr.number
|
||||||
)
|
)
|
||||||
reviews_str = (
|
reviews_str = (
|
||||||
@@ -703,7 +707,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
f"<!-- agent:plan-proposal -->\n"
|
f"<!-- agent:plan-proposal -->\n"
|
||||||
f"<!-- agent:awaiting-reply -->"
|
f"<!-- agent:awaiting-reply -->"
|
||||||
)
|
)
|
||||||
self.client.add_comment(
|
self.client.issues.add_comment(
|
||||||
self.owner, self.repo_name, self.item.task_number, comment_body
|
self.owner, self.repo_name, self.item.task_number, comment_body
|
||||||
)
|
)
|
||||||
return f"POSTED_COMMENT: PROPOSE_PLAN comment posted to issue #{self.item.task_number}."
|
return f"POSTED_COMMENT: PROPOSE_PLAN comment posted to issue #{self.item.task_number}."
|
||||||
@@ -718,7 +722,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
f"<!-- agent:question-response -->\n"
|
f"<!-- agent:question-response -->\n"
|
||||||
f"<!-- agent:awaiting-reply -->"
|
f"<!-- agent:awaiting-reply -->"
|
||||||
)
|
)
|
||||||
self.client.add_comment(
|
self.client.issues.add_comment(
|
||||||
self.owner, self.repo_name, self.item.task_number, comment_body
|
self.owner, self.repo_name, self.item.task_number, comment_body
|
||||||
)
|
)
|
||||||
return f"POSTED_COMMENT: ANSWER_QUESTION comment posted to issue #{self.item.task_number}."
|
return f"POSTED_COMMENT: ANSWER_QUESTION comment posted to issue #{self.item.task_number}."
|
||||||
@@ -727,10 +731,10 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
comment = coord_tools.arguments.get(
|
comment = coord_tools.arguments.get(
|
||||||
"comment", "Closing the issue as resolved."
|
"comment", "Closing the issue as resolved."
|
||||||
)
|
)
|
||||||
self.client.add_comment(
|
self.client.issues.add_comment(
|
||||||
self.owner, self.repo_name, self.item.task_number, comment
|
self.owner, self.repo_name, self.item.task_number, comment
|
||||||
)
|
)
|
||||||
self.client.close_issue(
|
self.client.issues.close_issue(
|
||||||
self.owner, self.repo_name, self.item.task_number
|
self.owner, self.repo_name, self.item.task_number
|
||||||
)
|
)
|
||||||
return f"CLOSED_ISSUE: Issue #{self.item.task_number} closed."
|
return f"CLOSED_ISSUE: Issue #{self.item.task_number} closed."
|
||||||
@@ -805,7 +809,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
pr_description = (
|
pr_description = (
|
||||||
f"Work in progress for issue #{self.item.task_number}."
|
f"Work in progress for issue #{self.item.task_number}."
|
||||||
)
|
)
|
||||||
pr_to_use = self.client.create_pull_request(
|
pr_to_use = self.client.prs.create_pull_request(
|
||||||
self.owner,
|
self.owner,
|
||||||
self.repo_name,
|
self.repo_name,
|
||||||
head=branch_name,
|
head=branch_name,
|
||||||
@@ -821,7 +825,7 @@ class IssueTaskProcessor(TaskProcessor):
|
|||||||
start_comment = (
|
start_comment = (
|
||||||
f"Started work on PR #{pr_to_use.number} ({pr_link})."
|
f"Started work on PR #{pr_to_use.number} ({pr_link})."
|
||||||
)
|
)
|
||||||
self.client.add_comment(
|
self.client.issues.add_comment(
|
||||||
self.owner,
|
self.owner,
|
||||||
self.repo_name,
|
self.repo_name,
|
||||||
self.item.task_number,
|
self.item.task_number,
|
||||||
@@ -919,7 +923,7 @@ class AgentDispatcher:
|
|||||||
results: list[str] = []
|
results: list[str] = []
|
||||||
# Get authenticated username for reviewer filter
|
# Get authenticated username for reviewer filter
|
||||||
try:
|
try:
|
||||||
user = self._client.get_authenticated_user()
|
user = self._client.repos.get_authenticated_user()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError("No authenticated user found.") from e
|
raise RuntimeError("No authenticated user found.") from e
|
||||||
|
|
||||||
@@ -973,7 +977,7 @@ class AgentDispatcher:
|
|||||||
return _find_pr_for_issue_helper(self._client, repo_full_name, issue_number)
|
return _find_pr_for_issue_helper(self._client, repo_full_name, issue_number)
|
||||||
|
|
||||||
def _is_awaiting_reply(self, comments: list[CommentModel]) -> bool:
|
def _is_awaiting_reply(self, comments: list[CommentModel]) -> bool:
|
||||||
user = self._client.get_authenticated_user()
|
user = self._client.repos.get_authenticated_user()
|
||||||
if not user or not user.login:
|
if not user or not user.login:
|
||||||
raise RuntimeError("No authenticated user found.")
|
raise RuntimeError("No authenticated user found.")
|
||||||
return _is_awaiting_reply_helper(comments, user.login)
|
return _is_awaiting_reply_helper(comments, user.login)
|
||||||
@@ -982,7 +986,7 @@ class AgentDispatcher:
|
|||||||
pr_info = item.task_info
|
pr_info = item.task_info
|
||||||
if not isinstance(pr_info, PullRequestModel):
|
if not isinstance(pr_info, PullRequestModel):
|
||||||
raise TypeError("Expected task_info to be a PullRequestModel")
|
raise TypeError("Expected task_info to be a PullRequestModel")
|
||||||
user = self._client.get_authenticated_user()
|
user = self._client.repos.get_authenticated_user()
|
||||||
if not user or not user.login:
|
if not user or not user.login:
|
||||||
raise RuntimeError("No authenticated user found.")
|
raise RuntimeError("No authenticated user found.")
|
||||||
processor = PRTaskProcessor(
|
processor = PRTaskProcessor(
|
||||||
@@ -1002,7 +1006,7 @@ class AgentDispatcher:
|
|||||||
issue_info = item.task_info
|
issue_info = item.task_info
|
||||||
if not isinstance(issue_info, IssueModel):
|
if not isinstance(issue_info, IssueModel):
|
||||||
raise TypeError("Expected task_info to be an IssueModel")
|
raise TypeError("Expected task_info to be an IssueModel")
|
||||||
user = self._client.get_authenticated_user()
|
user = self._client.repos.get_authenticated_user()
|
||||||
if not user or not user.login:
|
if not user or not user.login:
|
||||||
raise RuntimeError("No authenticated user found.")
|
raise RuntimeError("No authenticated user found.")
|
||||||
processor = IssueTaskProcessor(
|
processor = IssueTaskProcessor(
|
||||||
|
|||||||
+13
-5
@@ -90,7 +90,9 @@ class AgentOrchestrator:
|
|||||||
f"Polling unread notifications since: {last_checked or 'beginning'}"
|
f"Polling unread notifications since: {last_checked or 'beginning'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
notifications = self._client.list_unread_notifications(since=last_checked)
|
notifications = self._client.notifications.list_unread_notifications(
|
||||||
|
since=last_checked
|
||||||
|
)
|
||||||
|
|
||||||
if not notifications:
|
if not notifications:
|
||||||
logger.info("No new notifications found.")
|
logger.info("No new notifications found.")
|
||||||
@@ -165,7 +167,9 @@ class AgentOrchestrator:
|
|||||||
f"Skipping notification {notification_id} for {repo_full_name}#{task_number}. Reason: {reason}"
|
f"Skipping notification {notification_id} for {repo_full_name}#{task_number}. Reason: {reason}"
|
||||||
)
|
)
|
||||||
if notification_id is not None:
|
if notification_id is not None:
|
||||||
self._client.mark_notification_as_read(notification_id)
|
self._client.notifications.mark_notification_as_read(
|
||||||
|
notification_id
|
||||||
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Marked skipped Gitea notification thread {notification_id} as read."
|
f"Marked skipped Gitea notification thread {notification_id} as read."
|
||||||
)
|
)
|
||||||
@@ -174,7 +178,7 @@ class AgentOrchestrator:
|
|||||||
# Route based on decided action
|
# Route based on decided action
|
||||||
if notification_tools.action == "PROCESS_ISSUE":
|
if notification_tools.action == "PROCESS_ISSUE":
|
||||||
try:
|
try:
|
||||||
issue = self._client.get_issue(owner, repo_name, task_number)
|
issue = self._client.issues.get_issue(owner, repo_name, task_number)
|
||||||
if issue.repository is None:
|
if issue.repository is None:
|
||||||
issue = issue.model_copy(
|
issue = issue.model_copy(
|
||||||
update={"repository": RepositoryModel(**repo_info)}
|
update={"repository": RepositoryModel(**repo_info)}
|
||||||
@@ -195,7 +199,9 @@ class AgentOrchestrator:
|
|||||||
)
|
)
|
||||||
elif notification_tools.action == "PROCESS_PR":
|
elif notification_tools.action == "PROCESS_PR":
|
||||||
try:
|
try:
|
||||||
pr = self._client.get_pull_request(owner, repo_name, task_number)
|
pr = self._client.prs.get_pull_request(
|
||||||
|
owner, repo_name, task_number
|
||||||
|
)
|
||||||
if pr.repository is None:
|
if pr.repository is None:
|
||||||
pr = pr.model_copy(
|
pr = pr.model_copy(
|
||||||
update={"repository": RepositoryModel(**repo_info)}
|
update={"repository": RepositoryModel(**repo_info)}
|
||||||
@@ -252,7 +258,9 @@ class AgentOrchestrator:
|
|||||||
f"Completed {item.task_type} #{item.task_number}: {result[:200]}"
|
f"Completed {item.task_type} #{item.task_number}: {result[:200]}"
|
||||||
)
|
)
|
||||||
if item.notification_id is not None:
|
if item.notification_id is not None:
|
||||||
self._client.mark_notification_as_read(item.notification_id)
|
self._client.notifications.mark_notification_as_read(
|
||||||
|
item.notification_id
|
||||||
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Marked Gitea notification thread {item.notification_id} as read."
|
f"Marked Gitea notification thread {item.notification_id} as read."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Gitea API client package."""
|
||||||
|
|
||||||
|
from .client import GiteaClient
|
||||||
|
from .files_client import FilesClient
|
||||||
|
from .issues_client import IssuesClient
|
||||||
|
from .notifications_client import NotificationsClient
|
||||||
|
from .prs_client import PullRequestsClient
|
||||||
|
from .repos_client import ReposClient
|
||||||
|
from .models import (
|
||||||
|
CommentModel,
|
||||||
|
GiteaConfig,
|
||||||
|
IssueModel,
|
||||||
|
LabelModel,
|
||||||
|
PullRequestFileModel,
|
||||||
|
PullRequestModel,
|
||||||
|
RepositoryModel,
|
||||||
|
UserModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"FilesClient",
|
||||||
|
"GiteaClient",
|
||||||
|
"IssuesClient",
|
||||||
|
"NotificationsClient",
|
||||||
|
"PullRequestsClient",
|
||||||
|
"ReposClient",
|
||||||
|
"CommentModel",
|
||||||
|
"GiteaConfig",
|
||||||
|
"IssueModel",
|
||||||
|
"LabelModel",
|
||||||
|
"PullRequestFileModel",
|
||||||
|
"PullRequestModel",
|
||||||
|
"RepositoryModel",
|
||||||
|
"UserModel",
|
||||||
|
]
|
||||||
+35
-469
@@ -1,25 +1,28 @@
|
|||||||
import httpx
|
import httpx
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("gitea.client")
|
logger: logging.Logger = logging.getLogger("gitea.client")
|
||||||
|
|
||||||
from .config import GITEA_URL, GITEA_TOKEN, GITEA_ORG_FILTER
|
from .config import GITEA_URL, GITEA_TOKEN, GITEA_ORG_FILTER
|
||||||
from .models import (
|
from .files_client import FilesClient
|
||||||
UserModel,
|
from .issues_client import IssuesClient
|
||||||
LabelModel,
|
from .notifications_client import NotificationsClient
|
||||||
RepositoryModel,
|
from .prs_client import PullRequestsClient
|
||||||
IssueModel,
|
from .repos_client import ReposClient
|
||||||
PullRequestModel,
|
|
||||||
CommentModel,
|
|
||||||
PullRequestFileModel,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GiteaClient:
|
class GiteaClient:
|
||||||
"""HTTP client for Gitea API v1."""
|
"""HTTP client for Gitea API v1.
|
||||||
|
|
||||||
|
This is a facade class that provides access to focused sub-clients
|
||||||
|
for different API domains:
|
||||||
|
- repos: Repository operations (ReposClient)
|
||||||
|
- issues: Issue operations (IssuesClient)
|
||||||
|
- prs: Pull request operations (PullRequestsClient)
|
||||||
|
- files: File and git ref operations (FilesClient)
|
||||||
|
- notifications: Notification operations (NotificationsClient)
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.base_url: str = GITEA_URL.rstrip("/")
|
self.base_url: str = GITEA_URL.rstrip("/")
|
||||||
@@ -28,6 +31,25 @@ class GiteaClient:
|
|||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
self.client: httpx.Client = httpx.Client(headers=self.headers)
|
self.client: httpx.Client = httpx.Client(headers=self.headers)
|
||||||
|
self.repos: ReposClient = ReposClient(
|
||||||
|
self.base_url, self.client, GITEA_ORG_FILTER
|
||||||
|
)
|
||||||
|
self.issues: IssuesClient = IssuesClient(
|
||||||
|
self.base_url,
|
||||||
|
self.client,
|
||||||
|
get_user=self.repos.get_authenticated_user,
|
||||||
|
get_repos=self.repos.list_all_user_repos,
|
||||||
|
)
|
||||||
|
self.prs: PullRequestsClient = PullRequestsClient(
|
||||||
|
self.base_url,
|
||||||
|
self.client,
|
||||||
|
get_user=self.repos.get_authenticated_user,
|
||||||
|
get_repos=self.repos.list_all_user_repos,
|
||||||
|
)
|
||||||
|
self.files: FilesClient = FilesClient(self.base_url, self.client)
|
||||||
|
self.notifications: NotificationsClient = NotificationsClient(
|
||||||
|
self.base_url, self.client, GITEA_ORG_FILTER
|
||||||
|
)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close the underlying HTTP client."""
|
"""Close the underlying HTTP client."""
|
||||||
@@ -44,459 +66,3 @@ class GiteaClient:
|
|||||||
self.client.close()
|
self.client.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_authenticated_user(self) -> UserModel:
|
|
||||||
try:
|
|
||||||
response = self.client.get(f"{self.base_url}/api/v1/user")
|
|
||||||
response.raise_for_status()
|
|
||||||
return UserModel(**response.json())
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting authenticated user: {e}", exc_info=True)
|
|
||||||
raise RuntimeError(f"Could not get authenticated user: {e}") from e
|
|
||||||
|
|
||||||
def list_all_user_repos(self) -> list[RepositoryModel]:
|
|
||||||
try:
|
|
||||||
url = f"{self.base_url}/api/v1/user/repos"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
repos: list[dict[str, Any]] = response.json()
|
|
||||||
# Filter to ONLY configured organization repos, include mirrors
|
|
||||||
seen: set[str] = set()
|
|
||||||
result: list[RepositoryModel] = []
|
|
||||||
for r in repos:
|
|
||||||
full_name = r.get("full_name", "")
|
|
||||||
if (
|
|
||||||
full_name
|
|
||||||
and full_name not in seen
|
|
||||||
and (r.get("owner") or {}).get("login") == GITEA_ORG_FILTER
|
|
||||||
):
|
|
||||||
seen.add(full_name)
|
|
||||||
result.append(RepositoryModel(**r))
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing user repos: {e}", exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def list_repo_issues(
|
|
||||||
self, owner: str, repo: str, state: str = "open"
|
|
||||||
) -> list[IssueModel]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?type=issues&state={state}"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return [IssueModel(**item) for item in response.json()]
|
|
||||||
|
|
||||||
def list_repo_pull_requests(
|
|
||||||
self, owner: str, repo: str, state: str = "open"
|
|
||||||
) -> list[PullRequestModel]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls?state={state}"
|
|
||||||
response = self.client.get(url)
|
|
||||||
if response.status_code == 404:
|
|
||||||
return []
|
|
||||||
response.raise_for_status()
|
|
||||||
return [PullRequestModel(**item) for item in response.json()]
|
|
||||||
|
|
||||||
def get_pull_request(
|
|
||||||
self, owner: str, repo: str, pull_number: int
|
|
||||||
) -> PullRequestModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return PullRequestModel(**response.json())
|
|
||||||
|
|
||||||
def get_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return IssueModel(**response.json())
|
|
||||||
|
|
||||||
def close_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
|
|
||||||
data: dict[str, str] = {"state": "closed"}
|
|
||||||
response = self.client.patch(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return IssueModel(**response.json())
|
|
||||||
|
|
||||||
def close_pull_request(
|
|
||||||
self, owner: str, repo: str, pull_number: int
|
|
||||||
) -> PullRequestModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
|
||||||
data: dict[str, str] = {"state": "closed"}
|
|
||||||
response = self.client.patch(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return PullRequestModel(**response.json())
|
|
||||||
|
|
||||||
def get_issue_comments(
|
|
||||||
self, owner: str, repo: str, issue_number: int
|
|
||||||
) -> list[CommentModel]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return [CommentModel(**item) for item in response.json()]
|
|
||||||
|
|
||||||
def get_pull_request_comments(
|
|
||||||
self, owner: str, repo: str, pull_number: int
|
|
||||||
) -> list[CommentModel]:
|
|
||||||
url = (
|
|
||||||
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{pull_number}/comments"
|
|
||||||
)
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return [CommentModel(**item) for item in response.json()]
|
|
||||||
|
|
||||||
def get_pull_request_diff(self, owner: str, repo: str, pull_number: int) -> str:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/diff"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
def get_pull_request_patch(self, owner: str, repo: str, pull_number: int) -> str:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/patch"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.text
|
|
||||||
|
|
||||||
def get_pull_request_files(
|
|
||||||
self, owner: str, repo: str, pull_number: int
|
|
||||||
) -> list[PullRequestFileModel]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/files"
|
|
||||||
response = self.client.get(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return [PullRequestFileModel(**item) for item in response.json()]
|
|
||||||
|
|
||||||
def list_assigned_issues(self, owner: str = "", repo: str = "") -> list[IssueModel]:
|
|
||||||
try:
|
|
||||||
user = self.get_authenticated_user()
|
|
||||||
if not user:
|
|
||||||
return []
|
|
||||||
username: str = user.login
|
|
||||||
if owner and repo:
|
|
||||||
response = self.client.get(
|
|
||||||
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?assignee={username}&state=open&type=issues",
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return [IssueModel(**item) for item in response.json()]
|
|
||||||
all_issues: list[IssueModel] = []
|
|
||||||
repos = self.list_all_user_repos()
|
|
||||||
for r in repos:
|
|
||||||
repo_owner = r.owner
|
|
||||||
repo_name = r.name
|
|
||||||
resp = self.client.get(
|
|
||||||
f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/issues?assignee={username}&state=open&type=issues",
|
|
||||||
)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
for item in resp.json():
|
|
||||||
issue = IssueModel(**item)
|
|
||||||
# Backfill repository if Gitea omitted it
|
|
||||||
if issue.repository is None:
|
|
||||||
issue = issue.model_copy(update={"repository": r})
|
|
||||||
all_issues.append(issue)
|
|
||||||
return all_issues
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing assigned issues: {e}", exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def list_assigned_pull_requests(
|
|
||||||
self, owner: str = "", repo: str = ""
|
|
||||||
) -> list[PullRequestModel]:
|
|
||||||
"""List all pull requests assigned to or authored by the authenticated user."""
|
|
||||||
try:
|
|
||||||
user = self.get_authenticated_user()
|
|
||||||
if not user:
|
|
||||||
return []
|
|
||||||
username: str = user.login
|
|
||||||
if owner and repo:
|
|
||||||
response = self.client.get(
|
|
||||||
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls?state=open",
|
|
||||||
)
|
|
||||||
if response.status_code == 404:
|
|
||||||
return []
|
|
||||||
response.raise_for_status()
|
|
||||||
all_prs: list[PullRequestModel] = [
|
|
||||||
PullRequestModel(**pr) for pr in response.json()
|
|
||||||
]
|
|
||||||
return [
|
|
||||||
pr
|
|
||||||
for pr in all_prs
|
|
||||||
if (pr.assignee and pr.assignee.login == username)
|
|
||||||
or (pr.user and pr.user.login == username)
|
|
||||||
]
|
|
||||||
all_prs: list[PullRequestModel] = []
|
|
||||||
repos = self.list_all_user_repos()
|
|
||||||
for r in repos:
|
|
||||||
repo_owner = r.owner
|
|
||||||
repo_name = r.name
|
|
||||||
resp = self.client.get(
|
|
||||||
f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/pulls?state=open",
|
|
||||||
)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
for pr_data in resp.json():
|
|
||||||
pr = PullRequestModel(**pr_data)
|
|
||||||
if (pr.assignee and pr.assignee.login == username) or (
|
|
||||||
pr.user and pr.user.login == username
|
|
||||||
):
|
|
||||||
# Backfill repository if Gitea omitted it
|
|
||||||
if pr.repository is None:
|
|
||||||
pr = pr.model_copy(update={"repository": r})
|
|
||||||
all_prs.append(pr)
|
|
||||||
return all_prs
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing assigned pull requests: {e}", exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def create_pull_request(
|
|
||||||
self,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
head: str,
|
|
||||||
base: str,
|
|
||||||
title: str,
|
|
||||||
description: str = "",
|
|
||||||
) -> PullRequestModel:
|
|
||||||
try:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls"
|
|
||||||
data: dict[str, str] = {
|
|
||||||
"title": title,
|
|
||||||
"body": description,
|
|
||||||
"head": head,
|
|
||||||
"base": base,
|
|
||||||
}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return PullRequestModel(**response.json())
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating pull request: {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:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
|
||||||
data: dict[str, Any] = {}
|
|
||||||
if title is not None:
|
|
||||||
data["title"] = title
|
|
||||||
if body is not None:
|
|
||||||
data["body"] = body
|
|
||||||
if state is not None:
|
|
||||||
data["state"] = state
|
|
||||||
response = self.client.patch(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return PullRequestModel(**response.json())
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error updating pull request: {e}", exc_info=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def create_pr_via_tea(
|
|
||||||
self, owner: str, repo: str, title: str, description: str, head: str, base: str
|
|
||||||
) -> PullRequestModel:
|
|
||||||
return self.create_pull_request(owner, repo, head, base, title, description)
|
|
||||||
|
|
||||||
def approve_pr(
|
|
||||||
self, owner: str, repo: str, pr_number: int, comment: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
|
||||||
data: dict[str, Any] = {"event": "APPROVED", "body": comment}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def request_changes_pr(
|
|
||||||
self, owner: str, repo: str, pr_number: int, comment: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
|
||||||
data: dict[str, Any] = {"event": "REQUEST_CHANGES", "body": comment}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def get_pr_reviews(
|
|
||||||
self, owner: str, repo: str, pr_number: int
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
|
||||||
response = self.client.get(url)
|
|
||||||
if response.status_code == 404:
|
|
||||||
return []
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def dismiss_review_pr(
|
|
||||||
self, owner: str, repo: str, pr_number: int, review_id: int, message: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews/{review_id}/dismissals"
|
|
||||||
data: dict[str, str] = {"message": message}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def assign_issue(
|
|
||||||
self, owner: str, repo: str, issue_number: int, username: str
|
|
||||||
) -> IssueModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
|
|
||||||
data: dict[str, list[str]] = {"assignees": [username]}
|
|
||||||
response = self.client.patch(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return IssueModel(**response.json())
|
|
||||||
|
|
||||||
def create_issue(
|
|
||||||
self,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
title: str,
|
|
||||||
body: str,
|
|
||||||
labels: list[str] | None = None,
|
|
||||||
assignees: list[str] | None = None,
|
|
||||||
) -> IssueModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues"
|
|
||||||
data: dict[str, Any] = {"title": title, "body": body}
|
|
||||||
if labels:
|
|
||||||
data["labels"] = labels
|
|
||||||
if assignees:
|
|
||||||
data["assignees"] = assignees
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return IssueModel(**response.json())
|
|
||||||
|
|
||||||
def add_comment(
|
|
||||||
self, owner: str, repo: str, issue_number: int, body: str
|
|
||||||
) -> CommentModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
|
||||||
data: dict[str, str] = {"body": body}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return CommentModel(**response.json())
|
|
||||||
|
|
||||||
def add_label(
|
|
||||||
self, owner: str, repo: str, issue_number: int, label: str
|
|
||||||
) -> LabelModel:
|
|
||||||
url = (
|
|
||||||
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/labels"
|
|
||||||
)
|
|
||||||
data: list[str] = [label]
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return LabelModel(**response.json())
|
|
||||||
|
|
||||||
def add_label_pr(
|
|
||||||
self, owner: str, repo: str, pr_number: int, label: str
|
|
||||||
) -> LabelModel:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{pr_number}/labels"
|
|
||||||
data: list[str] = [label]
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return LabelModel(**response.json())
|
|
||||||
|
|
||||||
def update_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/git/ref/{ref}"
|
|
||||||
data: dict[str, str] = {"sha": sha}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def create_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/git/refs"
|
|
||||||
data: dict[str, str] = {"ref": ref, "sha": sha}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def update_file(
|
|
||||||
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
|
||||||
data: dict[str, str] = {
|
|
||||||
"message": message,
|
|
||||||
"content": base64.b64encode(content.encode()).decode(),
|
|
||||||
"branch": branch,
|
|
||||||
"new_branch": f"{branch}-update-{path}",
|
|
||||||
}
|
|
||||||
response = self.client.put(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def get_file_content(
|
|
||||||
self, owner: str, repo: str, path: str, ref: str = "master"
|
|
||||||
) -> str | list[str]:
|
|
||||||
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
|
||||||
params: dict[str, str] = {"ref": ref}
|
|
||||||
response = self.client.get(url, params=params)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
if isinstance(data, list):
|
|
||||||
return [
|
|
||||||
item.get("content", "") for item in data if item.get("type") == "file"
|
|
||||||
]
|
|
||||||
return (
|
|
||||||
base64.b64decode(data.get("content", "")).decode()
|
|
||||||
if data.get("content")
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
def list_unread_notifications(
|
|
||||||
self, since: Optional[str] = None
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
try:
|
|
||||||
url = f"{self.base_url}/api/v1/notifications"
|
|
||||||
params: dict[str, str] = {"all": "false"}
|
|
||||||
if since:
|
|
||||||
params["since"] = since
|
|
||||||
response = self.client.get(url, params=params)
|
|
||||||
response.raise_for_status()
|
|
||||||
notifications: list[dict[str, Any]] = response.json()
|
|
||||||
|
|
||||||
result: list[dict[str, Any]] = []
|
|
||||||
for n in notifications:
|
|
||||||
repo_info = n.get("repository") or {}
|
|
||||||
owner_info = repo_info.get("owner") or {}
|
|
||||||
owner_login = owner_info.get("login", "")
|
|
||||||
if owner_login == GITEA_ORG_FILTER:
|
|
||||||
result.append(n)
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error listing unread notifications: {e}", exc_info=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def mark_notification_as_read(self, thread_id: int) -> bool:
|
|
||||||
try:
|
|
||||||
url = f"{self.base_url}/api/v1/notifications/threads/{thread_id}"
|
|
||||||
response = self.client.patch(url)
|
|
||||||
response.raise_for_status()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error marking notification thread {thread_id} as read: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def merge_pull_request(
|
|
||||||
self,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
pull_number: int,
|
|
||||||
style: str = "squash",
|
|
||||||
title: str = "",
|
|
||||||
message: str = "",
|
|
||||||
) -> bool:
|
|
||||||
try:
|
|
||||||
url = (
|
|
||||||
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/merge"
|
|
||||||
)
|
|
||||||
data: dict[str, Any] = {
|
|
||||||
"Do": style,
|
|
||||||
"MergeTitleField": title,
|
|
||||||
"MergeMessageField": message,
|
|
||||||
}
|
|
||||||
response = self.client.post(url, json=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
f"Error merging pull request {pull_number}: {e}", exc_info=True
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""Files client for Gitea API operations."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("gitea.files_client")
|
||||||
|
|
||||||
|
|
||||||
|
class FilesClient:
|
||||||
|
"""HTTP client for Gitea Files and Git Refs API operations."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, client: httpx.Client) -> None:
|
||||||
|
"""Initialize the FilesClient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: The base URL for the Gitea API.
|
||||||
|
client: The httpx client for making requests.
|
||||||
|
"""
|
||||||
|
self.base_url: str = base_url
|
||||||
|
self.client: httpx.Client = client
|
||||||
|
|
||||||
|
def update_file(
|
||||||
|
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Update a file in a repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
path: File path.
|
||||||
|
message: Commit message.
|
||||||
|
content: File content.
|
||||||
|
branch: Branch name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The API response.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
||||||
|
data: dict[str, str] = {
|
||||||
|
"message": message,
|
||||||
|
"content": base64.b64encode(content.encode()).decode(),
|
||||||
|
"branch": branch,
|
||||||
|
"new_branch": f"{branch}-update-{path}",
|
||||||
|
}
|
||||||
|
response = self.client.put(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_file_content(
|
||||||
|
self, owner: str, repo: str, path: str, ref: str = "master"
|
||||||
|
) -> str | list[str]:
|
||||||
|
"""Get the content of a file or directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
path: File or directory path.
|
||||||
|
ref: Git reference (branch, tag, commit).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
File content as string, or list of file names if path is a directory.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
||||||
|
params: dict[str, str] = {"ref": ref}
|
||||||
|
response = self.client.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if isinstance(data, list):
|
||||||
|
return [
|
||||||
|
item.get("content", "") for item in data if item.get("type") == "file"
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
base64.b64decode(data.get("content", "")).decode()
|
||||||
|
if data.get("content")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
|
||||||
|
"""Update a git reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
ref: Reference name (e.g., heads/main).
|
||||||
|
sha: New SHA for the reference.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The API response.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/git/ref/{ref}"
|
||||||
|
data: dict[str, str] = {"sha": sha}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def create_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
|
||||||
|
"""Create a new git reference.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
ref: Reference name (e.g., refs/heads/new-branch).
|
||||||
|
sha: SHA for the reference.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The API response.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/git/refs"
|
||||||
|
data: dict[str, str] = {"ref": ref, "sha": sha}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"""Issues client for Gitea API operations."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Callable, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
CommentModel,
|
||||||
|
IssueModel,
|
||||||
|
LabelModel,
|
||||||
|
RepositoryModel,
|
||||||
|
UserModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("gitea.issues_client")
|
||||||
|
|
||||||
|
|
||||||
|
class IssuesClient:
|
||||||
|
"""HTTP client for Gitea Issues API operations."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
client: httpx.Client,
|
||||||
|
get_user: Callable[[], UserModel] | None = None,
|
||||||
|
get_repos: Callable[[], list[RepositoryModel]] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the IssuesClient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: The base URL for the Gitea API.
|
||||||
|
client: The httpx client for making requests.
|
||||||
|
get_user: Optional callable to get the authenticated user.
|
||||||
|
get_repos: Optional callable to get all user repos.
|
||||||
|
"""
|
||||||
|
self.base_url: str = base_url
|
||||||
|
self.client: httpx.Client = client
|
||||||
|
self._get_user: Callable[[], UserModel] | None = get_user
|
||||||
|
self._get_repos: Callable[[], list[RepositoryModel]] | None = get_repos
|
||||||
|
|
||||||
|
def list_repo_issues(
|
||||||
|
self, owner: str, repo: str, state: str = "open"
|
||||||
|
) -> list[IssueModel]:
|
||||||
|
"""List issues for a repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
state: Issue state filter (open, closed, all).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of issues matching the criteria.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?type=issues&state={state}"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return [IssueModel(**item) for item in response.json()]
|
||||||
|
|
||||||
|
def get_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel:
|
||||||
|
"""Get a specific issue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
issue_number: Issue number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requested issue.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return IssueModel(**response.json())
|
||||||
|
|
||||||
|
def close_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel:
|
||||||
|
"""Close an issue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
issue_number: Issue number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated issue.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
|
||||||
|
data: dict[str, str] = {"state": "closed"}
|
||||||
|
response = self.client.patch(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return IssueModel(**response.json())
|
||||||
|
|
||||||
|
def get_issue_comments(
|
||||||
|
self, owner: str, repo: str, issue_number: int
|
||||||
|
) -> list[CommentModel]:
|
||||||
|
"""Get comments on an issue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
issue_number: Issue number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of comments on the issue.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return [CommentModel(**item) for item in response.json()]
|
||||||
|
|
||||||
|
def list_assigned_issues(self, owner: str = "", repo: str = "") -> list[IssueModel]:
|
||||||
|
"""List issues assigned to the authenticated user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Optional repository owner to filter by.
|
||||||
|
repo: Optional repository name to filter by.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of issues assigned to the authenticated user.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self._get_user is None or self._get_repos is None:
|
||||||
|
logger.error("get_user and get_repos callables are required")
|
||||||
|
return []
|
||||||
|
|
||||||
|
user = self._get_user()
|
||||||
|
if not user:
|
||||||
|
return []
|
||||||
|
username: str = user.login
|
||||||
|
if owner and repo:
|
||||||
|
response = self.client.get(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?assignee={username}&state=open&type=issues",
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return [IssueModel(**item) for item in response.json()]
|
||||||
|
all_issues: list[IssueModel] = []
|
||||||
|
repos = self._get_repos()
|
||||||
|
for r in repos:
|
||||||
|
repo_owner = r.owner
|
||||||
|
repo_name = r.name
|
||||||
|
resp = self.client.get(
|
||||||
|
f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/issues?assignee={username}&state=open&type=issues",
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
for item in resp.json():
|
||||||
|
issue = IssueModel(**item)
|
||||||
|
# Backfill repository if Gitea omitted it
|
||||||
|
if issue.repository is None:
|
||||||
|
issue = issue.model_copy(update={"repository": r})
|
||||||
|
all_issues.append(issue)
|
||||||
|
return all_issues
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing assigned issues: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def assign_issue(
|
||||||
|
self, owner: str, repo: str, issue_number: int, username: str
|
||||||
|
) -> IssueModel:
|
||||||
|
"""Assign an issue to a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
issue_number: Issue number.
|
||||||
|
username: Username to assign.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated issue.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
|
||||||
|
data: dict[str, list[str]] = {"assignees": [username]}
|
||||||
|
response = self.client.patch(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return IssueModel(**response.json())
|
||||||
|
|
||||||
|
def create_issue(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
title: str,
|
||||||
|
body: str,
|
||||||
|
labels: list[str] | None = None,
|
||||||
|
assignees: list[str] | None = None,
|
||||||
|
) -> IssueModel:
|
||||||
|
"""Create a new issue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
title: Issue title.
|
||||||
|
body: Issue body/description.
|
||||||
|
labels: Optional list of label IDs.
|
||||||
|
assignees: Optional list of usernames to assign.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created issue.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues"
|
||||||
|
data: dict[str, Any] = {"title": title, "body": body}
|
||||||
|
if labels:
|
||||||
|
data["labels"] = labels
|
||||||
|
if assignees:
|
||||||
|
data["assignees"] = assignees
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return IssueModel(**response.json())
|
||||||
|
|
||||||
|
def add_comment(
|
||||||
|
self, owner: str, repo: str, issue_number: int, body: str
|
||||||
|
) -> CommentModel:
|
||||||
|
"""Add a comment to an issue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
issue_number: Issue number.
|
||||||
|
body: Comment body.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created comment.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
|
||||||
|
data: dict[str, str] = {"body": body}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return CommentModel(**response.json())
|
||||||
|
|
||||||
|
def add_label(
|
||||||
|
self, owner: str, repo: str, issue_number: int, label: str
|
||||||
|
) -> LabelModel:
|
||||||
|
"""Add a label to an issue.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
issue_number: Issue number.
|
||||||
|
label: Label name or ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The added label.
|
||||||
|
"""
|
||||||
|
url = (
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/labels"
|
||||||
|
)
|
||||||
|
data: list[str] = [label]
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return LabelModel(**response.json())
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"""Notifications client for Gitea API operations."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("gitea.notifications_client")
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsClient:
|
||||||
|
"""HTTP client for Gitea Notifications API operations."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, client: httpx.Client, org_filter: str) -> None:
|
||||||
|
"""Initialize the NotificationsClient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: The base URL for the Gitea API.
|
||||||
|
client: The httpx client for making requests.
|
||||||
|
org_filter: Organization filter for notifications.
|
||||||
|
"""
|
||||||
|
self.base_url: str = base_url
|
||||||
|
self.client: httpx.Client = client
|
||||||
|
self.org_filter: str = org_filter
|
||||||
|
|
||||||
|
def list_unread_notifications(
|
||||||
|
self, since: Optional[str] = None
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""List unread notifications.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
since: Optional ISO 8601 timestamp to filter notifications after.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of unread notifications for the configured organization.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.base_url}/api/v1/notifications"
|
||||||
|
params: dict[str, str] = {"all": "false"}
|
||||||
|
if since:
|
||||||
|
params["since"] = since
|
||||||
|
response = self.client.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
notifications: list[dict[str, Any]] = response.json()
|
||||||
|
|
||||||
|
result: list[dict[str, Any]] = []
|
||||||
|
for n in notifications:
|
||||||
|
repo_info = n.get("repository") or {}
|
||||||
|
owner_info = repo_info.get("owner") or {}
|
||||||
|
owner_login = owner_info.get("login", "")
|
||||||
|
if owner_login == self.org_filter:
|
||||||
|
result.append(n)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing unread notifications: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def mark_notification_as_read(self, thread_id: int) -> bool:
|
||||||
|
"""Mark a notification as read.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
thread_id: Notification thread ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.base_url}/api/v1/notifications/threads/{thread_id}"
|
||||||
|
response = self.client.patch(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error marking notification thread {thread_id} as read: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return False
|
||||||
@@ -0,0 +1,462 @@
|
|||||||
|
"""Pull Requests client for Gitea API operations."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
CommentModel,
|
||||||
|
LabelModel,
|
||||||
|
PullRequestFileModel,
|
||||||
|
PullRequestModel,
|
||||||
|
RepositoryModel,
|
||||||
|
UserModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("gitea.prs_client")
|
||||||
|
|
||||||
|
|
||||||
|
class PullRequestsClient:
|
||||||
|
"""HTTP client for Gitea Pull Requests API operations."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
client: httpx.Client,
|
||||||
|
get_user: Callable[[], UserModel] | None = None,
|
||||||
|
get_repos: Callable[[], list[RepositoryModel]] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the PullRequestsClient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: The base URL for the Gitea API.
|
||||||
|
client: The httpx client for making requests.
|
||||||
|
get_user: Optional callable to get the authenticated user.
|
||||||
|
get_repos: Optional callable to get all user repos.
|
||||||
|
"""
|
||||||
|
self.base_url: str = base_url
|
||||||
|
self.client: httpx.Client = client
|
||||||
|
self._get_user: Callable[[], UserModel] | None = get_user
|
||||||
|
self._get_repos: Callable[[], list[RepositoryModel]] | None = get_repos
|
||||||
|
|
||||||
|
def list_repo_pull_requests(
|
||||||
|
self, owner: str, repo: str, state: str = "open"
|
||||||
|
) -> list[PullRequestModel]:
|
||||||
|
"""List pull requests for a repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
state: PR state filter (open, closed, all).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of pull requests matching the criteria.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls?state={state}"
|
||||||
|
response = self.client.get(url)
|
||||||
|
if response.status_code == 404:
|
||||||
|
return []
|
||||||
|
response.raise_for_status()
|
||||||
|
return [PullRequestModel(**item) for item in response.json()]
|
||||||
|
|
||||||
|
def get_pull_request(
|
||||||
|
self, owner: str, repo: str, pull_number: int
|
||||||
|
) -> PullRequestModel:
|
||||||
|
"""Get a specific pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The requested pull request.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return PullRequestModel(**response.json())
|
||||||
|
|
||||||
|
def close_pull_request(
|
||||||
|
self, owner: str, repo: str, pull_number: int
|
||||||
|
) -> PullRequestModel:
|
||||||
|
"""Close a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated pull request.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
||||||
|
data: dict[str, str] = {"state": "closed"}
|
||||||
|
response = self.client.patch(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return PullRequestModel(**response.json())
|
||||||
|
|
||||||
|
def get_pull_request_comments(
|
||||||
|
self, owner: str, repo: str, pull_number: int
|
||||||
|
) -> list[CommentModel]:
|
||||||
|
"""Get comments on a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of comments on the pull request.
|
||||||
|
"""
|
||||||
|
url = (
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{pull_number}/comments"
|
||||||
|
)
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return [CommentModel(**item) for item in response.json()]
|
||||||
|
|
||||||
|
def get_pull_request_diff(self, owner: str, repo: str, pull_number: int) -> str:
|
||||||
|
"""Get the diff for a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The diff as a string.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/diff"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
def get_pull_request_patch(self, owner: str, repo: str, pull_number: int) -> str:
|
||||||
|
"""Get the patch for a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The patch as a string.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/patch"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.text
|
||||||
|
|
||||||
|
def get_pull_request_files(
|
||||||
|
self, owner: str, repo: str, pull_number: int
|
||||||
|
) -> list[PullRequestFileModel]:
|
||||||
|
"""Get the files changed in a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of files changed in the pull request.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/files"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
return [PullRequestFileModel(**item) for item in response.json()]
|
||||||
|
|
||||||
|
def list_assigned_pull_requests(
|
||||||
|
self, owner: str = "", repo: str = ""
|
||||||
|
) -> list[PullRequestModel]:
|
||||||
|
"""List all pull requests assigned to or authored by the authenticated user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Optional repository owner to filter by.
|
||||||
|
repo: Optional repository name to filter by.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of pull requests assigned to or authored by the user.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self._get_user is None or self._get_repos is None:
|
||||||
|
logger.error("get_user and get_repos callables are required")
|
||||||
|
return []
|
||||||
|
|
||||||
|
user = self._get_user()
|
||||||
|
if not user:
|
||||||
|
return []
|
||||||
|
username: str = user.login
|
||||||
|
if owner and repo:
|
||||||
|
response = self.client.get(
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls?state=open",
|
||||||
|
)
|
||||||
|
if response.status_code == 404:
|
||||||
|
return []
|
||||||
|
response.raise_for_status()
|
||||||
|
all_prs: list[PullRequestModel] = [
|
||||||
|
PullRequestModel(**pr) for pr in response.json()
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
pr
|
||||||
|
for pr in all_prs
|
||||||
|
if (pr.assignee and pr.assignee.login == username)
|
||||||
|
or (pr.user and pr.user.login == username)
|
||||||
|
]
|
||||||
|
all_prs: list[PullRequestModel] = []
|
||||||
|
repos = self._get_repos()
|
||||||
|
for r in repos:
|
||||||
|
repo_owner = r.owner
|
||||||
|
repo_name = r.name
|
||||||
|
resp = self.client.get(
|
||||||
|
f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/pulls?state=open",
|
||||||
|
)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
for pr_data in resp.json():
|
||||||
|
pr = PullRequestModel(**pr_data)
|
||||||
|
if (pr.assignee and pr.assignee.login == username) or (
|
||||||
|
pr.user and pr.user.login == username
|
||||||
|
):
|
||||||
|
# Backfill repository if Gitea omitted it
|
||||||
|
if pr.repository is None:
|
||||||
|
pr = pr.model_copy(update={"repository": r})
|
||||||
|
all_prs.append(pr)
|
||||||
|
return all_prs
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing assigned pull requests: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create_pull_request(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
head: str,
|
||||||
|
base: str,
|
||||||
|
title: str,
|
||||||
|
description: str = "",
|
||||||
|
) -> PullRequestModel:
|
||||||
|
"""Create a new pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
head: Head branch name.
|
||||||
|
base: Base branch name.
|
||||||
|
title: Pull request title.
|
||||||
|
description: Pull request description.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created pull request.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls"
|
||||||
|
data: dict[str, str] = {
|
||||||
|
"title": title,
|
||||||
|
"body": description,
|
||||||
|
"head": head,
|
||||||
|
"base": base,
|
||||||
|
}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return PullRequestModel(**response.json())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating pull request: {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:
|
||||||
|
"""Update a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
title: Optional new title.
|
||||||
|
body: Optional new body.
|
||||||
|
state: Optional new state.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated pull request.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
|
||||||
|
data: dict[str, Any] = {}
|
||||||
|
if title is not None:
|
||||||
|
data["title"] = title
|
||||||
|
if body is not None:
|
||||||
|
data["body"] = body
|
||||||
|
if state is not None:
|
||||||
|
data["state"] = state
|
||||||
|
response = self.client.patch(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return PullRequestModel(**response.json())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating pull request: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_pr_via_tea(
|
||||||
|
self, owner: str, repo: str, title: str, description: str, head: str, base: str
|
||||||
|
) -> PullRequestModel:
|
||||||
|
"""Create a pull request (alias for create_pull_request).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
title: Pull request title.
|
||||||
|
description: Pull request description.
|
||||||
|
head: Head branch name.
|
||||||
|
base: Base branch name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created pull request.
|
||||||
|
"""
|
||||||
|
return self.create_pull_request(owner, repo, head, base, title, description)
|
||||||
|
|
||||||
|
def approve_pr(
|
||||||
|
self, owner: str, repo: str, pr_number: int, comment: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Approve a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pr_number: Pull request number.
|
||||||
|
comment: Review comment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The review response.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
|
data: dict[str, Any] = {"event": "APPROVED", "body": comment}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def request_changes_pr(
|
||||||
|
self, owner: str, repo: str, pr_number: int, comment: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Request changes on a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pr_number: Pull request number.
|
||||||
|
comment: Review comment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The review response.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
|
data: dict[str, Any] = {"event": "REQUEST_CHANGES", "body": comment}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_pr_reviews(
|
||||||
|
self, owner: str, repo: str, pr_number: int
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Get reviews for a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pr_number: Pull request number.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of reviews.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
|
||||||
|
response = self.client.get(url)
|
||||||
|
if response.status_code == 404:
|
||||||
|
return []
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def dismiss_review_pr(
|
||||||
|
self, owner: str, repo: str, pr_number: int, review_id: int, message: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Dismiss a review on a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pr_number: Pull request number.
|
||||||
|
review_id: Review ID to dismiss.
|
||||||
|
message: Dismissal message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The dismissal response.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews/{review_id}/dismissals"
|
||||||
|
data: dict[str, str] = {"message": message}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def add_label_pr(
|
||||||
|
self, owner: str, repo: str, pr_number: int, label: str
|
||||||
|
) -> LabelModel:
|
||||||
|
"""Add a label to a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pr_number: Pull request number.
|
||||||
|
label: Label name or ID.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The added label.
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{pr_number}/labels"
|
||||||
|
data: list[str] = [label]
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return LabelModel(**response.json())
|
||||||
|
|
||||||
|
def merge_pull_request(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
pull_number: int,
|
||||||
|
style: str = "squash",
|
||||||
|
title: str = "",
|
||||||
|
message: str = "",
|
||||||
|
) -> bool:
|
||||||
|
"""Merge a pull request.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
owner: Repository owner.
|
||||||
|
repo: Repository name.
|
||||||
|
pull_number: Pull request number.
|
||||||
|
style: Merge style (squash, merge, rebase).
|
||||||
|
title: Optional merge commit title.
|
||||||
|
message: Optional merge commit message.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if merge was successful.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = (
|
||||||
|
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/merge"
|
||||||
|
)
|
||||||
|
data: dict[str, Any] = {
|
||||||
|
"Do": style,
|
||||||
|
"MergeTitleField": title,
|
||||||
|
"MergeMessageField": message,
|
||||||
|
}
|
||||||
|
response = self.client.post(url, json=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error merging pull request {pull_number}: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
raise
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Repositories client for Gitea API operations."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from .models import RepositoryModel, UserModel
|
||||||
|
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger("gitea.repos_client")
|
||||||
|
|
||||||
|
|
||||||
|
class ReposClient:
|
||||||
|
"""HTTP client for Gitea Repositories API operations."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str, client: httpx.Client, org_filter: str) -> None:
|
||||||
|
"""Initialize the ReposClient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: The base URL for the Gitea API.
|
||||||
|
client: The httpx client for making requests.
|
||||||
|
org_filter: Organization filter for repositories.
|
||||||
|
"""
|
||||||
|
self.base_url: str = base_url
|
||||||
|
self.client: httpx.Client = client
|
||||||
|
self.org_filter: str = org_filter
|
||||||
|
|
||||||
|
def list_all_user_repos(self) -> list[RepositoryModel]:
|
||||||
|
"""List all repositories for the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of repositories belonging to the configured organization.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.base_url}/api/v1/user/repos"
|
||||||
|
response = self.client.get(url)
|
||||||
|
response.raise_for_status()
|
||||||
|
repos: list[dict[str, Any]] = response.json()
|
||||||
|
# Filter to ONLY configured organization repos, include mirrors
|
||||||
|
seen: set[str] = set()
|
||||||
|
result: list[RepositoryModel] = []
|
||||||
|
for r in repos:
|
||||||
|
full_name = r.get("full_name", "")
|
||||||
|
if (
|
||||||
|
full_name
|
||||||
|
and full_name not in seen
|
||||||
|
and (r.get("owner") or {}).get("login") == self.org_filter
|
||||||
|
):
|
||||||
|
seen.add(full_name)
|
||||||
|
result.append(RepositoryModel(**r))
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing user repos: {e}", exc_info=True)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_authenticated_user(self) -> UserModel:
|
||||||
|
"""Get the authenticated user.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authenticated user.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the user cannot be retrieved.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.get(f"{self.base_url}/api/v1/user")
|
||||||
|
response.raise_for_status()
|
||||||
|
return UserModel(**response.json())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting authenticated user: {e}", exc_info=True)
|
||||||
|
raise RuntimeError(f"Could not get authenticated user: {e}") from e
|
||||||
@@ -50,7 +50,7 @@ class FileTools:
|
|||||||
limit: Maximum number of lines to return (default 250).
|
limit: Maximum number of lines to return (default 250).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
content = self._client.get_file_content(owner, repo, path)
|
content = self._client.files.get_file_content(owner, repo, path)
|
||||||
raw: str = "\n".join(content) if isinstance(content, list) else content
|
raw: str = "\n".join(content) if isinstance(content, list) else content
|
||||||
return self._paginate_lines(raw, offset, limit)
|
return self._paginate_lines(raw, offset, limit)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -73,22 +73,26 @@ class FileTools:
|
|||||||
limit: Maximum number of lines to return (default 250).
|
limit: Maximum number of lines to return (default 250).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
content = self._client.get_file_content(owner, repo, path, ref)
|
content = self._client.files.get_file_content(owner, repo, path, ref)
|
||||||
raw: str = "\n".join(content) if isinstance(content, list) else content
|
raw: str = "\n".join(content) if isinstance(content, list) else content
|
||||||
return self._paginate_lines(raw, offset, limit)
|
return self._paginate_lines(raw, offset, limit)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting file content: {str(e)}"
|
return f"Error getting file content: {str(e)}"
|
||||||
|
|
||||||
def commit_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
|
def commit_file(
|
||||||
|
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
|
||||||
|
) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.update_file(owner, repo, path, message, content, branch)
|
self._client.files.update_file(owner, repo, path, message, content, branch)
|
||||||
return f"File '{path}' committed successfully to {owner}/{repo}."
|
return f"File '{path}' committed successfully to {owner}/{repo}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error committing file: {str(e)}"
|
return f"Error committing file: {str(e)}"
|
||||||
|
|
||||||
def update_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
|
def update_file(
|
||||||
|
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
|
||||||
|
) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.update_file(owner, repo, path, message, content, branch)
|
self._client.files.update_file(owner, repo, path, message, content, branch)
|
||||||
return f"File '{path}' updated in {owner}/{repo}."
|
return f"File '{path}' updated in {owner}/{repo}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error updating file: {str(e)}"
|
return f"Error updating file: {str(e)}"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class GitTools:
|
|||||||
|
|
||||||
def create_branch(self, owner: str, repo: str, ref: str, sha: str) -> str:
|
def create_branch(self, owner: str, repo: str, ref: str, sha: str) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.create_ref(owner, repo, ref, sha)
|
self._client.files.create_ref(owner, repo, ref, sha)
|
||||||
return f"Branch '{ref}' created successfully in {owner}/{repo}."
|
return f"Branch '{ref}' created successfully in {owner}/{repo}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error creating branch: {str(e)}"
|
return f"Error creating branch: {str(e)}"
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ class IssueTools:
|
|||||||
|
|
||||||
def get_issue(self, owner: str, repo: str, issue_number: int) -> str:
|
def get_issue(self, owner: str, repo: str, issue_number: int) -> str:
|
||||||
try:
|
try:
|
||||||
issue: IssueModel = self._client.get_issue(owner, repo, issue_number)
|
issue: IssueModel = self._client.issues.get_issue(owner, repo, issue_number)
|
||||||
return issue.model_dump_json(indent=2)
|
return issue.model_dump_json(indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting issue: {str(e)}"
|
return f"Error getting issue: {str(e)}"
|
||||||
|
|
||||||
def close_issue(self, owner: str, repo: str, issue_number: int) -> str:
|
def close_issue(self, owner: str, repo: str, issue_number: int) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.close_issue(owner, repo, issue_number)
|
self._client.issues.close_issue(owner, repo, issue_number)
|
||||||
return f"Issue #{issue_number} closed successfully."
|
return f"Issue #{issue_number} closed successfully."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error closing issue: {str(e)}"
|
return f"Error closing issue: {str(e)}"
|
||||||
@@ -44,7 +44,7 @@ class IssueTools:
|
|||||||
offset: Zero-based comment index to start from (default 0).
|
offset: Zero-based comment index to start from (default 0).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
comments: list[CommentModel] = self._client.get_issue_comments(
|
comments: list[CommentModel] = self._client.issues.get_issue_comments(
|
||||||
owner, repo, issue_number
|
owner, repo, issue_number
|
||||||
)
|
)
|
||||||
total: int = len(comments)
|
total: int = len(comments)
|
||||||
@@ -62,12 +62,12 @@ class IssueTools:
|
|||||||
|
|
||||||
def list_assigned_issues(self) -> list[dict[str, Any]]:
|
def list_assigned_issues(self) -> list[dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
repos = self._client.list_all_user_repos()
|
repos = self._client.repos.list_all_user_repos()
|
||||||
all_issues: list[dict[str, Any]] = []
|
all_issues: list[dict[str, Any]] = []
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
owner = repo.owner
|
owner = repo.owner
|
||||||
repo_name = repo.name
|
repo_name = repo.name
|
||||||
issues = self._client.list_assigned_issues(owner, repo_name)
|
issues = self._client.issues.list_assigned_issues(owner, repo_name)
|
||||||
if issues:
|
if issues:
|
||||||
all_issues.extend(
|
all_issues.extend(
|
||||||
[
|
[
|
||||||
@@ -84,7 +84,7 @@ class IssueTools:
|
|||||||
|
|
||||||
def list_issues(self, owner: str, repo: str, state: str = "open") -> str:
|
def list_issues(self, owner: str, repo: str, state: str = "open") -> str:
|
||||||
try:
|
try:
|
||||||
issues = self._client.list_repo_issues(owner, repo, state)
|
issues = self._client.issues.list_repo_issues(owner, repo, state)
|
||||||
if not issues:
|
if not issues:
|
||||||
return f"No issues in {owner}/{repo}."
|
return f"No issues in {owner}/{repo}."
|
||||||
summary = [f"#{issue.number}: {issue.title}" for issue in issues]
|
summary = [f"#{issue.number}: {issue.title}" for issue in issues]
|
||||||
@@ -102,7 +102,7 @@ class IssueTools:
|
|||||||
assignees: list[str] | None = None,
|
assignees: list[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
try:
|
try:
|
||||||
issue = self._client.create_issue(
|
issue = self._client.issues.create_issue(
|
||||||
owner, repo, title, body, labels, assignees
|
owner, repo, title, body, labels, assignees
|
||||||
)
|
)
|
||||||
return f"Issue #{issue.number} created successfully in {owner}/{repo}."
|
return f"Issue #{issue.number} created successfully in {owner}/{repo}."
|
||||||
@@ -113,7 +113,7 @@ class IssueTools:
|
|||||||
self, owner: str, repo: str, issue_number: int, label: str
|
self, owner: str, repo: str, issue_number: int, label: str
|
||||||
) -> str:
|
) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.add_label(owner, repo, issue_number, label)
|
self._client.issues.add_label(owner, repo, issue_number, label)
|
||||||
return f"Label '{label}' added to issue #{issue_number}."
|
return f"Label '{label}' added to issue #{issue_number}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error adding label to issue #{issue_number}: {e}"
|
return f"Error adding label to issue #{issue_number}: {e}"
|
||||||
@@ -122,7 +122,7 @@ class IssueTools:
|
|||||||
self, owner: str, repo: str, issue_number: int, body: str
|
self, owner: str, repo: str, issue_number: int, body: str
|
||||||
) -> str:
|
) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.add_comment(owner, repo, issue_number, body)
|
self._client.issues.add_comment(owner, repo, issue_number, body)
|
||||||
return f"Comment added to issue #{issue_number}."
|
return f"Comment added to issue #{issue_number}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error adding comment to issue #{issue_number}: {e}"
|
return f"Error adding comment to issue #{issue_number}: {e}"
|
||||||
|
|||||||
+44
-17
@@ -42,14 +42,16 @@ class PRTools:
|
|||||||
|
|
||||||
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
|
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
|
||||||
try:
|
try:
|
||||||
pr: PullRequestModel = self._client.get_pull_request(owner, repo, pull_number)
|
pr: PullRequestModel = self._client.prs.get_pull_request(
|
||||||
|
owner, repo, pull_number
|
||||||
|
)
|
||||||
return pr.model_dump_json(indent=2)
|
return pr.model_dump_json(indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting pull request: {str(e)}"
|
return f"Error getting pull request: {str(e)}"
|
||||||
|
|
||||||
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
|
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.close_pull_request(owner, repo, pull_number)
|
self._client.prs.close_pull_request(owner, repo, pull_number)
|
||||||
return f"Pull request #{pull_number} closed successfully."
|
return f"Pull request #{pull_number} closed successfully."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error closing pull request: {str(e)}"
|
return f"Error closing pull request: {str(e)}"
|
||||||
@@ -69,7 +71,7 @@ class PRTools:
|
|||||||
offset: Zero-based comment index to start from (default 0).
|
offset: Zero-based comment index to start from (default 0).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
comments: list[CommentModel] = self._client.get_pull_request_comments(
|
comments: list[CommentModel] = self._client.prs.get_pull_request_comments(
|
||||||
owner, repo, pull_number
|
owner, repo, pull_number
|
||||||
)
|
)
|
||||||
total: int = len(comments)
|
total: int = len(comments)
|
||||||
@@ -87,14 +89,21 @@ class PRTools:
|
|||||||
|
|
||||||
def list_assigned_pull_requests(self) -> list[dict[str, Any]]:
|
def list_assigned_pull_requests(self) -> list[dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
repos = self._client.list_all_user_repos()
|
repos = self._client.repos.list_all_user_repos()
|
||||||
all_prs: list[dict[str, Any]] = []
|
all_prs: list[dict[str, Any]] = []
|
||||||
for repo_info in repos:
|
for repo_info in repos:
|
||||||
repo_owner = repo_info.owner
|
repo_owner = repo_info.owner
|
||||||
repo_name = repo_info.name
|
repo_name = repo_info.name
|
||||||
prs = self._client.list_assigned_pull_requests(repo_owner, repo_name)
|
prs = self._client.prs.list_assigned_pull_requests(
|
||||||
|
repo_owner, repo_name
|
||||||
|
)
|
||||||
if prs:
|
if prs:
|
||||||
all_prs.extend([pr.model_dump() if hasattr(pr, 'model_dump') else pr for pr in prs])
|
all_prs.extend(
|
||||||
|
[
|
||||||
|
pr.model_dump() if hasattr(pr, "model_dump") else pr
|
||||||
|
for pr in prs
|
||||||
|
]
|
||||||
|
)
|
||||||
return all_prs
|
return all_prs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error listing assigned pull requests: {e}", exc_info=True)
|
logger.error(f"Error listing assigned pull requests: {e}", exc_info=True)
|
||||||
@@ -102,7 +111,7 @@ class PRTools:
|
|||||||
|
|
||||||
def list_pull_requests(self, owner: str, repo: str, state: str = "open") -> str:
|
def list_pull_requests(self, owner: str, repo: str, state: str = "open") -> str:
|
||||||
try:
|
try:
|
||||||
prs = self._client.list_repo_pull_requests(owner, repo, state)
|
prs = self._client.prs.list_repo_pull_requests(owner, repo, state)
|
||||||
if not prs:
|
if not prs:
|
||||||
return f"No PRs in {owner}/{repo}."
|
return f"No PRs in {owner}/{repo}."
|
||||||
summary = [f"#{pr.number}: {pr.title}" for pr in prs]
|
summary = [f"#{pr.number}: {pr.title}" for pr in prs]
|
||||||
@@ -110,9 +119,19 @@ class PRTools:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error listing PRs: {str(e)}"
|
return f"Error listing PRs: {str(e)}"
|
||||||
|
|
||||||
def create_pull_request(self, owner: str, repo: str, head: str, base: str, title: str, description: str = "") -> str:
|
def create_pull_request(
|
||||||
|
self,
|
||||||
|
owner: str,
|
||||||
|
repo: str,
|
||||||
|
head: str,
|
||||||
|
base: str,
|
||||||
|
title: str,
|
||||||
|
description: str = "",
|
||||||
|
) -> str:
|
||||||
try:
|
try:
|
||||||
pr = self._client.create_pr_via_tea(owner, repo, title, description, head, base)
|
pr = self._client.prs.create_pr_via_tea(
|
||||||
|
owner, repo, title, description, head, base
|
||||||
|
)
|
||||||
return pr.model_dump_json(indent=2)
|
return pr.model_dump_json(indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error creating PR: {str(e)}"
|
return f"Error creating PR: {str(e)}"
|
||||||
@@ -127,14 +146,16 @@ class PRTools:
|
|||||||
state: str | None = None,
|
state: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
try:
|
try:
|
||||||
pr = self._client.update_pull_request(owner, repo, pull_number, title, body, state)
|
pr = self._client.prs.update_pull_request(
|
||||||
|
owner, repo, pull_number, title, body, state
|
||||||
|
)
|
||||||
return pr.model_dump_json(indent=2)
|
return pr.model_dump_json(indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error updating PR #{pull_number}: {str(e)}"
|
return f"Error updating PR #{pull_number}: {str(e)}"
|
||||||
|
|
||||||
def add_label_to_pr(self, owner: str, repo: str, pr_number: int, label: str) -> str:
|
def add_label_to_pr(self, owner: str, repo: str, pr_number: int, label: str) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.add_label_pr(owner, repo, pr_number, label)
|
self._client.prs.add_label_pr(owner, repo, pr_number, label)
|
||||||
return f"Label '{label}' added to PR #{pr_number}."
|
return f"Label '{label}' added to PR #{pr_number}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error adding label to PR #{pr_number}: {e}"
|
return f"Error adding label to PR #{pr_number}: {e}"
|
||||||
@@ -155,7 +176,7 @@ class PRTools:
|
|||||||
Increment by max_chars to page through a large diff.
|
Increment by max_chars to page through a large diff.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
diff: str = self._client.get_pull_request_diff(owner, repo, pull_number)
|
diff: str = self._client.prs.get_pull_request_diff(owner, repo, pull_number)
|
||||||
return _truncate_diff(diff, max_chars, char_offset)
|
return _truncate_diff(diff, max_chars, char_offset)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting PR diff: {str(e)}"
|
return f"Error getting PR diff: {str(e)}"
|
||||||
@@ -176,21 +197,27 @@ class PRTools:
|
|||||||
Increment by max_chars to page through a large patch.
|
Increment by max_chars to page through a large patch.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
patch: str = self._client.get_pull_request_patch(owner, repo, pull_number)
|
patch: str = self._client.prs.get_pull_request_patch(
|
||||||
|
owner, repo, pull_number
|
||||||
|
)
|
||||||
return _truncate_diff(patch, max_chars, char_offset)
|
return _truncate_diff(patch, max_chars, char_offset)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting PR patch: {str(e)}"
|
return f"Error getting PR patch: {str(e)}"
|
||||||
|
|
||||||
def approve_pull_request(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
|
def approve_pull_request(
|
||||||
|
self, owner: str, repo: str, pull_number: int, comment: str
|
||||||
|
) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.approve_pr(owner, repo, pull_number, comment)
|
self._client.prs.approve_pr(owner, repo, pull_number, comment)
|
||||||
return f"Approved PR #{pull_number}."
|
return f"Approved PR #{pull_number}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error approving PR: {str(e)}"
|
return f"Error approving PR: {str(e)}"
|
||||||
|
|
||||||
def request_changes(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
|
def request_changes(
|
||||||
|
self, owner: str, repo: str, pull_number: int, comment: str
|
||||||
|
) -> str:
|
||||||
try:
|
try:
|
||||||
self._client.request_changes_pr(owner, repo, pull_number, comment)
|
self._client.prs.request_changes_pr(owner, repo, pull_number, comment)
|
||||||
return f"Requested changes on PR #{pull_number}."
|
return f"Requested changes on PR #{pull_number}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error requesting changes: {str(e)}"
|
return f"Error requesting changes: {str(e)}"
|
||||||
|
|||||||
+64
-21
@@ -21,7 +21,7 @@ class WorkspaceManager:
|
|||||||
def _configure_repo_user(self, repo_path: Path) -> None:
|
def _configure_repo_user(self, repo_path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
client = GiteaClient()
|
client = GiteaClient()
|
||||||
user = client.get_authenticated_user()
|
user = client.repos.get_authenticated_user()
|
||||||
if not user or not user.login:
|
if not user or not user.login:
|
||||||
raise RuntimeError("No authenticated user found.")
|
raise RuntimeError("No authenticated user found.")
|
||||||
username: str = user.login
|
username: str = user.login
|
||||||
@@ -32,19 +32,29 @@ class WorkspaceManager:
|
|||||||
|
|
||||||
# Configure extraHeader locally for the repo
|
# Configure extraHeader locally for the repo
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "config", "http.extraHeader", f"Authorization: Basic {auth_b64}"],
|
[
|
||||||
check=True, capture_output=True
|
"git",
|
||||||
|
"-C",
|
||||||
|
str(repo_path),
|
||||||
|
"config",
|
||||||
|
"http.extraHeader",
|
||||||
|
f"Authorization: Basic {auth_b64}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
name: str = user.full_name or user.login
|
name: str = user.full_name or user.login
|
||||||
email: str = user.email or f"{user.login}@noreply.gitea"
|
email: str = user.email or f"{user.login}@noreply.gitea"
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "config", "user.name", name],
|
["git", "-C", str(repo_path), "config", "user.name", name],
|
||||||
check=True, capture_output=True
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "config", "user.email", email],
|
["git", "-C", str(repo_path), "config", "user.email", email],
|
||||||
check=True, capture_output=True
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error configuring local git user: {e}")
|
logger.error(f"Error configuring local git user: {e}")
|
||||||
@@ -64,59 +74,84 @@ class WorkspaceManager:
|
|||||||
auth_url = self._get_authenticated_url(repo_full_name)
|
auth_url = self._get_authenticated_url(repo_full_name)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "remote", "set-url", "origin", auth_url],
|
["git", "-C", str(repo_path), "remote", "set-url", "origin", auth_url],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
self._configure_repo_user(repo_path)
|
self._configure_repo_user(repo_path)
|
||||||
|
|
||||||
# Check for any uncommitted changes or untracked files
|
# Check for any uncommitted changes or untracked files
|
||||||
status_res = subprocess.run(
|
status_res = subprocess.run(
|
||||||
["git", "-C", str(repo_path), "status", "--porcelain"],
|
["git", "-C", str(repo_path), "status", "--porcelain"],
|
||||||
check=True, capture_output=True, text=True
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
)
|
)
|
||||||
if status_res.stdout.strip():
|
if status_res.stdout.strip():
|
||||||
logger.info(f"Uncommitted changes detected in {repo_path}. Stashing before sanitization.")
|
logger.info(
|
||||||
|
f"Uncommitted changes detected in {repo_path}. Stashing before sanitization."
|
||||||
|
)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "stash", "push", "-u", "-m", "Auto-backup before agent sanitization"],
|
[
|
||||||
check=True, capture_output=True
|
"git",
|
||||||
|
"-C",
|
||||||
|
str(repo_path),
|
||||||
|
"stash",
|
||||||
|
"push",
|
||||||
|
"-u",
|
||||||
|
"-m",
|
||||||
|
"Auto-backup before agent sanitization",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "reset", "--hard", "HEAD"],
|
["git", "-C", str(repo_path), "reset", "--hard", "HEAD"],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "clean", "-fdx"],
|
["git", "-C", str(repo_path), "clean", "-fdx"],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "checkout", "main"],
|
["git", "-C", str(repo_path), "checkout", "main"],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "checkout", "master"],
|
["git", "-C", str(repo_path), "checkout", "master"],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "pull", "origin", "main"],
|
["git", "-C", str(repo_path), "pull", "origin", "main"],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "-C", str(repo_path), "pull", "origin", "master"],
|
["git", "-C", str(repo_path), "pull", "origin", "master"],
|
||||||
check=True, capture_output=True,
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during sanitization: {e}", exc_info=True)
|
logger.error(f"Error during sanitization: {e}", exc_info=True)
|
||||||
raise RuntimeError(f"Failed to sanitize repository {repo_full_name} at {repo_path}: {e}") from e
|
raise RuntimeError(
|
||||||
|
f"Failed to sanitize repository {repo_full_name} at {repo_path}: {e}"
|
||||||
|
) from e
|
||||||
|
|
||||||
def clone_repo(self, repo_full_name: str, clone_url: str | None = None) -> Path:
|
def clone_repo(self, repo_full_name: str, clone_url: str | None = None) -> Path:
|
||||||
repo_path: Path = self.get_repo_path(repo_full_name)
|
repo_path: Path = self.get_repo_path(repo_full_name)
|
||||||
if repo_path.exists():
|
if repo_path.exists():
|
||||||
if not (repo_path / ".git").exists():
|
if not (repo_path / ".git").exists():
|
||||||
new_path: Path = repo_path.parent / f"{repo_full_name.replace('/', '_')}_old"
|
new_path: Path = (
|
||||||
|
repo_path.parent / f"{repo_full_name.replace('/', '_')}_old"
|
||||||
|
)
|
||||||
if new_path.exists():
|
if new_path.exists():
|
||||||
shutil.rmtree(new_path)
|
shutil.rmtree(new_path)
|
||||||
repo_path.rename(new_path)
|
repo_path.rename(new_path)
|
||||||
@@ -126,7 +161,7 @@ class WorkspaceManager:
|
|||||||
auth_url = self._get_authenticated_url(repo_full_name)
|
auth_url = self._get_authenticated_url(repo_full_name)
|
||||||
|
|
||||||
client = GiteaClient()
|
client = GiteaClient()
|
||||||
user = client.get_authenticated_user()
|
user = client.repos.get_authenticated_user()
|
||||||
if not user or not user.login:
|
if not user or not user.login:
|
||||||
raise RuntimeError("No authenticated user found.")
|
raise RuntimeError("No authenticated user found.")
|
||||||
username: str = user.login
|
username: str = user.login
|
||||||
@@ -135,8 +170,16 @@ class WorkspaceManager:
|
|||||||
auth_bytes: bytes = auth_str.encode("utf-8")
|
auth_bytes: bytes = auth_str.encode("utf-8")
|
||||||
auth_b64: str = base64.b64encode(auth_bytes).decode("utf-8")
|
auth_b64: str = base64.b64encode(auth_bytes).decode("utf-8")
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["git", "clone", "-c", f"http.extraHeader=Authorization: Basic {auth_b64}", auth_url, str(repo_path)],
|
[
|
||||||
check=True, capture_output=True
|
"git",
|
||||||
|
"clone",
|
||||||
|
"-c",
|
||||||
|
f"http.extraHeader=Authorization: Basic {auth_b64}",
|
||||||
|
auth_url,
|
||||||
|
str(repo_path),
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
)
|
)
|
||||||
self._configure_repo_user(repo_path)
|
self._configure_repo_user(repo_path)
|
||||||
return repo_path
|
return repo_path
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ async def main() -> None:
|
|||||||
# Initialize Gitea components
|
# Initialize Gitea components
|
||||||
client: GiteaClient = GiteaClient()
|
client: GiteaClient = GiteaClient()
|
||||||
try:
|
try:
|
||||||
user = client.get_authenticated_user()
|
user = client.repos.get_authenticated_user()
|
||||||
if not user or not user.login:
|
if not user or not user.login:
|
||||||
raise RuntimeError("No authenticated user found.")
|
raise RuntimeError("No authenticated user found.")
|
||||||
logger.info(f"Authenticated as user: {user.login}")
|
logger.info(f"Authenticated as user: {user.login}")
|
||||||
|
|||||||
+44
-20
@@ -11,7 +11,7 @@ def test_gitea_client_list_repo_issues() -> None:
|
|||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
# Test default parameter ("open")
|
# Test default parameter ("open")
|
||||||
client.list_repo_issues("owner", "repo")
|
client.issues.list_repo_issues("owner", "repo")
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
args, _ = mock_get.call_args
|
args, _ = mock_get.call_args
|
||||||
assert "type=issues" in args[0]
|
assert "type=issues" in args[0]
|
||||||
@@ -20,7 +20,7 @@ def test_gitea_client_list_repo_issues() -> None:
|
|||||||
mock_get.reset_mock()
|
mock_get.reset_mock()
|
||||||
|
|
||||||
# Test custom parameter ("closed")
|
# Test custom parameter ("closed")
|
||||||
client.list_repo_issues("owner", "repo", state="closed")
|
client.issues.list_repo_issues("owner", "repo", state="closed")
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
args, _ = mock_get.call_args
|
args, _ = mock_get.call_args
|
||||||
assert "type=issues" in args[0]
|
assert "type=issues" in args[0]
|
||||||
@@ -36,7 +36,7 @@ def test_gitea_client_list_repo_pull_requests() -> None:
|
|||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
# Test default parameter ("open")
|
# Test default parameter ("open")
|
||||||
client.list_repo_pull_requests("owner", "repo")
|
client.prs.list_repo_pull_requests("owner", "repo")
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
args, _ = mock_get.call_args
|
args, _ = mock_get.call_args
|
||||||
assert "state=open" in args[0]
|
assert "state=open" in args[0]
|
||||||
@@ -44,7 +44,7 @@ def test_gitea_client_list_repo_pull_requests() -> None:
|
|||||||
mock_get.reset_mock()
|
mock_get.reset_mock()
|
||||||
|
|
||||||
# Test custom parameter ("closed")
|
# Test custom parameter ("closed")
|
||||||
client.list_repo_pull_requests("owner", "repo", state="closed")
|
client.prs.list_repo_pull_requests("owner", "repo", state="closed")
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
args, _ = mock_get.call_args
|
args, _ = mock_get.call_args
|
||||||
assert "state=closed" in args[0]
|
assert "state=closed" in args[0]
|
||||||
@@ -55,14 +55,17 @@ def test_gitea_client_list_assigned_issues() -> None:
|
|||||||
user_mock: MagicMock = MagicMock()
|
user_mock: MagicMock = MagicMock()
|
||||||
user_mock.login = "testuser"
|
user_mock.login = "testuser"
|
||||||
|
|
||||||
with patch.object(client, "get_authenticated_user", return_value=user_mock), \
|
with (
|
||||||
patch("httpx.Client.get") as mock_get:
|
patch.object(client.repos, "get_authenticated_user", return_value=user_mock),
|
||||||
|
patch.object(client.issues, "_get_user", return_value=user_mock),
|
||||||
|
patch("httpx.Client.get") as mock_get,
|
||||||
|
):
|
||||||
mock_response: MagicMock = MagicMock()
|
mock_response: MagicMock = MagicMock()
|
||||||
mock_response.status_code = 200
|
mock_response.status_code = 200
|
||||||
mock_response.json.return_value = []
|
mock_response.json.return_value = []
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
client.list_assigned_issues("owner", "repo")
|
client.issues.list_assigned_issues("owner", "repo")
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
args, _ = mock_get.call_args
|
args, _ = mock_get.call_args
|
||||||
assert "type=issues" in args[0]
|
assert "type=issues" in args[0]
|
||||||
@@ -74,18 +77,36 @@ def test_gitea_client_list_assigned_pull_requests() -> None:
|
|||||||
user_mock: MagicMock = MagicMock()
|
user_mock: MagicMock = MagicMock()
|
||||||
user_mock.login = "testuser"
|
user_mock.login = "testuser"
|
||||||
|
|
||||||
with patch.object(client, "get_authenticated_user", return_value=user_mock), \
|
with (
|
||||||
patch("httpx.Client.get") as mock_get:
|
patch.object(client.repos, "get_authenticated_user", return_value=user_mock),
|
||||||
|
patch.object(client.prs, "_get_user", return_value=user_mock),
|
||||||
|
patch("httpx.Client.get") as mock_get,
|
||||||
|
):
|
||||||
mock_response: MagicMock = MagicMock()
|
mock_response: MagicMock = MagicMock()
|
||||||
mock_response.status_code = 200
|
mock_response.status_code = 200
|
||||||
mock_response.json.return_value = [
|
mock_response.json.return_value = [
|
||||||
{"number": 1, "title": "PR 1", "assignee": {"login": "testuser"}, "user": {"login": "otheruser"}},
|
{
|
||||||
{"number": 2, "title": "PR 2", "assignee": None, "user": {"login": "testuser"}},
|
"number": 1,
|
||||||
{"number": 3, "title": "PR 3", "assignee": {"login": "otheruser"}, "user": {"login": "otheruser"}}
|
"title": "PR 1",
|
||||||
|
"assignee": {"login": "testuser"},
|
||||||
|
"user": {"login": "otheruser"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 2,
|
||||||
|
"title": "PR 2",
|
||||||
|
"assignee": None,
|
||||||
|
"user": {"login": "testuser"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 3,
|
||||||
|
"title": "PR 3",
|
||||||
|
"assignee": {"login": "otheruser"},
|
||||||
|
"user": {"login": "otheruser"},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
res = client.list_assigned_pull_requests("owner", "repo")
|
res = client.prs.list_assigned_pull_requests("owner", "repo")
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
numbers = [pr.number for pr in res]
|
numbers = [pr.number for pr in res]
|
||||||
@@ -106,7 +127,7 @@ def test_gitea_client_list_unread_notifications() -> None:
|
|||||||
mock_get.return_value = mock_response
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
# Test without since
|
# Test without since
|
||||||
res = client.list_unread_notifications()
|
res = client.notifications.list_unread_notifications()
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
_, kwargs = mock_get.call_args
|
_, kwargs = mock_get.call_args
|
||||||
assert kwargs.get("params") == {"all": "false"}
|
assert kwargs.get("params") == {"all": "false"}
|
||||||
@@ -116,20 +137,23 @@ def test_gitea_client_list_unread_notifications() -> None:
|
|||||||
mock_get.reset_mock()
|
mock_get.reset_mock()
|
||||||
|
|
||||||
# Test with since
|
# Test with since
|
||||||
res = client.list_unread_notifications(since="2026-06-30T21:41:16+02:00")
|
res = client.notifications.list_unread_notifications(
|
||||||
|
since="2026-06-30T21:41:16+02:00"
|
||||||
|
)
|
||||||
mock_get.assert_called_once()
|
mock_get.assert_called_once()
|
||||||
_, kwargs = mock_get.call_args
|
_, kwargs = mock_get.call_args
|
||||||
assert kwargs.get("params") == {"all": "false", "since": "2026-06-30T21:41:16+02:00"}
|
assert kwargs.get("params") == {
|
||||||
|
"all": "false",
|
||||||
|
"since": "2026-06-30T21:41:16+02:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def test_gitea_client_get_authenticated_user_failure() -> None:
|
def test_gitea_client_get_authenticated_user_failure() -> None:
|
||||||
client: GiteaClient = GiteaClient()
|
client: GiteaClient = GiteaClient()
|
||||||
with patch("httpx.Client.get") as mock_get:
|
with patch("httpx.Client.get") as mock_get:
|
||||||
mock_get.side_effect = Exception("Connection error")
|
mock_get.side_effect = Exception("Connection error")
|
||||||
with pytest.raises(RuntimeError, match="Could not get authenticated user"):
|
with pytest.raises(RuntimeError, match="Could not get authenticated user"):
|
||||||
client.get_authenticated_user()
|
client.repos.get_authenticated_user()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+60
-31
@@ -3,19 +3,28 @@ from gitea.client import GiteaClient
|
|||||||
from gitea.tools.file_tools import FileTools
|
from gitea.tools.file_tools import FileTools
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_content_string_success() -> None:
|
def _create_mock_client() -> MagicMock:
|
||||||
|
"""Create a mock GiteaClient with sub-client attributes."""
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||||
mock_client.get_file_content.return_value = "file content here"
|
mock_client.files = MagicMock()
|
||||||
|
return mock_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_file_content_string_success() -> None:
|
||||||
|
mock_client = _create_mock_client()
|
||||||
|
mock_client.files.get_file_content.return_value = "file content here"
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
|
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
|
||||||
assert res == "1: file content here"
|
assert res == "1: file content here"
|
||||||
mock_client.get_file_content.assert_called_once_with("owner", "repo", "path/to/file")
|
mock_client.files.get_file_content.assert_called_once_with(
|
||||||
|
"owner", "repo", "path/to/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_content_list_success() -> None:
|
def test_get_file_content_list_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_file_content.return_value = ["line1", "line2"]
|
mock_client.files.get_file_content.return_value = ["line1", "line2"]
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
|
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
|
||||||
@@ -23,8 +32,8 @@ def test_get_file_content_list_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_file_content_failure() -> None:
|
def test_get_file_content_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_file_content.side_effect = Exception("API Error")
|
mock_client.files.get_file_content.side_effect = Exception("API Error")
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
|
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
|
||||||
@@ -32,66 +41,86 @@ def test_get_file_content_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_file_content_with_ref_string_success() -> None:
|
def test_get_file_content_with_ref_string_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_file_content.return_value = "file content here"
|
mock_client.files.get_file_content.return_value = "file content here"
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.get_file_content_with_ref("owner", "repo", "path/to/file", "main")
|
res: str = file_tools.get_file_content_with_ref(
|
||||||
|
"owner", "repo", "path/to/file", "main"
|
||||||
|
)
|
||||||
assert res == "1: file content here"
|
assert res == "1: file content here"
|
||||||
mock_client.get_file_content.assert_called_once_with("owner", "repo", "path/to/file", "main")
|
mock_client.files.get_file_content.assert_called_once_with(
|
||||||
|
"owner", "repo", "path/to/file", "main"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_content_with_ref_list_success() -> None:
|
def test_get_file_content_with_ref_list_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_file_content.return_value = ["line1", "line2"]
|
mock_client.files.get_file_content.return_value = ["line1", "line2"]
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.get_file_content_with_ref("owner", "repo", "path/to/file", "main")
|
res: str = file_tools.get_file_content_with_ref(
|
||||||
|
"owner", "repo", "path/to/file", "main"
|
||||||
|
)
|
||||||
assert res == "1: line1\n2: line2"
|
assert res == "1: line1\n2: line2"
|
||||||
|
|
||||||
|
|
||||||
def test_get_file_content_with_ref_failure() -> None:
|
def test_get_file_content_with_ref_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_file_content.side_effect = Exception("API Error")
|
mock_client.files.get_file_content.side_effect = Exception("API Error")
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.get_file_content_with_ref("owner", "repo", "path/to/file", "main")
|
res: str = file_tools.get_file_content_with_ref(
|
||||||
|
"owner", "repo", "path/to/file", "main"
|
||||||
|
)
|
||||||
assert "Error getting file content: API Error" in res
|
assert "Error getting file content: API Error" in res
|
||||||
|
|
||||||
|
|
||||||
def test_commit_file_success() -> None:
|
def test_commit_file_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.update_file.return_value = {}
|
mock_client.files.update_file.return_value = {}
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.commit_file("owner", "repo", "path/to/file", "msg", "content", "branch")
|
res: str = file_tools.commit_file(
|
||||||
|
"owner", "repo", "path/to/file", "msg", "content", "branch"
|
||||||
|
)
|
||||||
assert "committed successfully" in res
|
assert "committed successfully" in res
|
||||||
mock_client.update_file.assert_called_once_with("owner", "repo", "path/to/file", "msg", "content", "branch")
|
mock_client.files.update_file.assert_called_once_with(
|
||||||
|
"owner", "repo", "path/to/file", "msg", "content", "branch"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_commit_file_failure() -> None:
|
def test_commit_file_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.update_file.side_effect = Exception("API Error")
|
mock_client.files.update_file.side_effect = Exception("API Error")
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.commit_file("owner", "repo", "path/to/file", "msg", "content", "branch")
|
res: str = file_tools.commit_file(
|
||||||
|
"owner", "repo", "path/to/file", "msg", "content", "branch"
|
||||||
|
)
|
||||||
assert "Error committing file: API Error" in res
|
assert "Error committing file: API Error" in res
|
||||||
|
|
||||||
|
|
||||||
def test_update_file_success() -> None:
|
def test_update_file_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.update_file.return_value = {}
|
mock_client.files.update_file.return_value = {}
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.update_file("owner", "repo", "path/to/file", "msg", "content", "branch")
|
res: str = file_tools.update_file(
|
||||||
|
"owner", "repo", "path/to/file", "msg", "content", "branch"
|
||||||
|
)
|
||||||
assert "updated in" in res
|
assert "updated in" in res
|
||||||
mock_client.update_file.assert_called_once_with("owner", "repo", "path/to/file", "msg", "content", "branch")
|
mock_client.files.update_file.assert_called_once_with(
|
||||||
|
"owner", "repo", "path/to/file", "msg", "content", "branch"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_update_file_failure() -> None:
|
def test_update_file_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.update_file.side_effect = Exception("API Error")
|
mock_client.files.update_file.side_effect = Exception("API Error")
|
||||||
|
|
||||||
file_tools: FileTools = FileTools(mock_client)
|
file_tools: FileTools = FileTools(mock_client)
|
||||||
res: str = file_tools.update_file("owner", "repo", "path/to/file", "msg", "content", "branch")
|
res: str = file_tools.update_file(
|
||||||
|
"owner", "repo", "path/to/file", "msg", "content", "branch"
|
||||||
|
)
|
||||||
assert "Error updating file: API Error" in res
|
assert "Error updating file: API Error" in res
|
||||||
|
|||||||
+12
-5
@@ -3,19 +3,26 @@ from gitea.client import GiteaClient
|
|||||||
from gitea.tools.git_tools import GitTools
|
from gitea.tools.git_tools import GitTools
|
||||||
|
|
||||||
|
|
||||||
def test_create_branch_success() -> None:
|
def _create_mock_client() -> MagicMock:
|
||||||
|
"""Create a mock GiteaClient with sub-client attributes."""
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||||
mock_client.create_ref.return_value = {}
|
mock_client.files = MagicMock()
|
||||||
|
return mock_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_branch_success() -> None:
|
||||||
|
mock_client = _create_mock_client()
|
||||||
|
mock_client.files.create_ref.return_value = {}
|
||||||
|
|
||||||
git_tools: GitTools = GitTools(mock_client)
|
git_tools: GitTools = GitTools(mock_client)
|
||||||
res: str = git_tools.create_branch("owner", "repo", "ref", "sha")
|
res: str = git_tools.create_branch("owner", "repo", "ref", "sha")
|
||||||
assert res == "Branch 'ref' created successfully in owner/repo."
|
assert res == "Branch 'ref' created successfully in owner/repo."
|
||||||
mock_client.create_ref.assert_called_once_with("owner", "repo", "ref", "sha")
|
mock_client.files.create_ref.assert_called_once_with("owner", "repo", "ref", "sha")
|
||||||
|
|
||||||
|
|
||||||
def test_create_branch_failure() -> None:
|
def test_create_branch_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.create_ref.side_effect = Exception("API Error")
|
mock_client.files.create_ref.side_effect = Exception("API Error")
|
||||||
|
|
||||||
git_tools: GitTools = GitTools(mock_client)
|
git_tools: GitTools = GitTools(mock_client)
|
||||||
res: str = git_tools.create_branch("owner", "repo", "ref", "sha")
|
res: str = git_tools.create_branch("owner", "repo", "ref", "sha")
|
||||||
|
|||||||
+48
-40
@@ -6,10 +6,18 @@ from gitea.models import IssueModel, CommentModel, LabelModel, RepositoryModel
|
|||||||
from gitea.tools.issue_tools import IssueTools
|
from gitea.tools.issue_tools import IssueTools
|
||||||
|
|
||||||
|
|
||||||
def test_get_issue_success() -> None:
|
def _create_mock_client() -> MagicMock:
|
||||||
|
"""Create a mock GiteaClient with sub-client attributes."""
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||||
|
mock_client.issues = MagicMock()
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
|
return mock_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_issue_success() -> None:
|
||||||
|
mock_client = _create_mock_client()
|
||||||
issue: IssueModel = IssueModel(number=1, title="Test Issue", state="open")
|
issue: IssueModel = IssueModel(number=1, title="Test Issue", state="open")
|
||||||
mock_client.get_issue.return_value = issue
|
mock_client.issues.get_issue.return_value = issue
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.get_issue("owner", "repo", 1)
|
res: str = issue_tools.get_issue("owner", "repo", 1)
|
||||||
@@ -17,12 +25,12 @@ def test_get_issue_success() -> None:
|
|||||||
data: dict[str, Any] = json.loads(res)
|
data: dict[str, Any] = json.loads(res)
|
||||||
assert data["number"] == 1
|
assert data["number"] == 1
|
||||||
assert data["title"] == "Test Issue"
|
assert data["title"] == "Test Issue"
|
||||||
mock_client.get_issue.assert_called_once_with("owner", "repo", 1)
|
mock_client.issues.get_issue.assert_called_once_with("owner", "repo", 1)
|
||||||
|
|
||||||
|
|
||||||
def test_get_issue_failure() -> None:
|
def test_get_issue_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_issue.side_effect = Exception("API Error")
|
mock_client.issues.get_issue.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.get_issue("owner", "repo", 1)
|
res: str = issue_tools.get_issue("owner", "repo", 1)
|
||||||
@@ -30,18 +38,18 @@ def test_get_issue_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_close_issue_success() -> None:
|
def test_close_issue_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.close_issue.return_value = IssueModel(number=1, state="closed")
|
mock_client.issues.close_issue.return_value = IssueModel(number=1, state="closed")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.close_issue("owner", "repo", 1)
|
res: str = issue_tools.close_issue("owner", "repo", 1)
|
||||||
assert res == "Issue #1 closed successfully."
|
assert res == "Issue #1 closed successfully."
|
||||||
mock_client.close_issue.assert_called_once_with("owner", "repo", 1)
|
mock_client.issues.close_issue.assert_called_once_with("owner", "repo", 1)
|
||||||
|
|
||||||
|
|
||||||
def test_close_issue_failure() -> None:
|
def test_close_issue_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.close_issue.side_effect = Exception("API Error")
|
mock_client.issues.close_issue.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.close_issue("owner", "repo", 1)
|
res: str = issue_tools.close_issue("owner", "repo", 1)
|
||||||
@@ -49,9 +57,9 @@ def test_close_issue_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_issue_comments_success() -> None:
|
def test_get_issue_comments_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
comment: CommentModel = CommentModel(id=123, body="Comment body")
|
comment: CommentModel = CommentModel(id=123, body="Comment body")
|
||||||
mock_client.get_issue_comments.return_value = [comment]
|
mock_client.issues.get_issue_comments.return_value = [comment]
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.get_issue_comments("owner", "repo", 1)
|
res: str = issue_tools.get_issue_comments("owner", "repo", 1)
|
||||||
@@ -61,8 +69,8 @@ def test_get_issue_comments_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_issue_comments_failure() -> None:
|
def test_get_issue_comments_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_issue_comments.side_effect = Exception("API Error")
|
mock_client.issues.get_issue_comments.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.get_issue_comments("owner", "repo", 1)
|
res: str = issue_tools.get_issue_comments("owner", "repo", 1)
|
||||||
@@ -70,23 +78,23 @@ def test_get_issue_comments_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_assigned_issues_success() -> None:
|
def test_list_assigned_issues_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
repo: RepositoryModel = RepositoryModel(name="repo1", owner="owner1")
|
repo: RepositoryModel = RepositoryModel(name="repo1", owner="owner1")
|
||||||
issue: IssueModel = IssueModel(number=1, title="Test Issue")
|
issue: IssueModel = IssueModel(number=1, title="Test Issue")
|
||||||
mock_client.list_all_user_repos.return_value = [repo]
|
mock_client.repos.list_all_user_repos.return_value = [repo]
|
||||||
mock_client.list_assigned_issues.return_value = [issue]
|
mock_client.issues.list_assigned_issues.return_value = [issue]
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: list[dict[str, Any]] = issue_tools.list_assigned_issues()
|
res: list[dict[str, Any]] = issue_tools.list_assigned_issues()
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]["number"] == 1
|
assert res[0]["number"] == 1
|
||||||
mock_client.list_all_user_repos.assert_called_once()
|
mock_client.repos.list_all_user_repos.assert_called_once()
|
||||||
mock_client.list_assigned_issues.assert_called_once_with("owner1", "repo1")
|
mock_client.issues.list_assigned_issues.assert_called_once_with("owner1", "repo1")
|
||||||
|
|
||||||
|
|
||||||
def test_list_assigned_issues_failure() -> None:
|
def test_list_assigned_issues_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.list_all_user_repos.side_effect = Exception("API Error")
|
mock_client.repos.list_all_user_repos.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: list[dict[str, Any]] = issue_tools.list_assigned_issues()
|
res: list[dict[str, Any]] = issue_tools.list_assigned_issues()
|
||||||
@@ -94,9 +102,9 @@ def test_list_assigned_issues_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_issues_success() -> None:
|
def test_list_issues_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
issue: IssueModel = IssueModel(number=1, title="Test Issue")
|
issue: IssueModel = IssueModel(number=1, title="Test Issue")
|
||||||
mock_client.list_repo_issues.return_value = [issue]
|
mock_client.issues.list_repo_issues.return_value = [issue]
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.list_issues("owner", "repo")
|
res: str = issue_tools.list_issues("owner", "repo")
|
||||||
@@ -104,8 +112,8 @@ def test_list_issues_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_issues_empty() -> None:
|
def test_list_issues_empty() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.list_repo_issues.return_value = []
|
mock_client.issues.list_repo_issues.return_value = []
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.list_issues("owner", "repo")
|
res: str = issue_tools.list_issues("owner", "repo")
|
||||||
@@ -113,8 +121,8 @@ def test_list_issues_empty() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_issues_failure() -> None:
|
def test_list_issues_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.list_repo_issues.side_effect = Exception("API Error")
|
mock_client.issues.list_repo_issues.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.list_issues("owner", "repo")
|
res: str = issue_tools.list_issues("owner", "repo")
|
||||||
@@ -122,23 +130,23 @@ def test_list_issues_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_create_issue_success() -> None:
|
def test_create_issue_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
issue: IssueModel = IssueModel(number=2)
|
issue: IssueModel = IssueModel(number=2)
|
||||||
mock_client.create_issue.return_value = issue
|
mock_client.issues.create_issue.return_value = issue
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.create_issue(
|
res: str = issue_tools.create_issue(
|
||||||
"owner", "repo", "Title", "Body", ["label1"], ["assignee1"]
|
"owner", "repo", "Title", "Body", ["label1"], ["assignee1"]
|
||||||
)
|
)
|
||||||
assert res == "Issue #2 created successfully in owner/repo."
|
assert res == "Issue #2 created successfully in owner/repo."
|
||||||
mock_client.create_issue.assert_called_once_with(
|
mock_client.issues.create_issue.assert_called_once_with(
|
||||||
"owner", "repo", "Title", "Body", ["label1"], ["assignee1"]
|
"owner", "repo", "Title", "Body", ["label1"], ["assignee1"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_issue_failure() -> None:
|
def test_create_issue_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.create_issue.side_effect = Exception("API Error")
|
mock_client.issues.create_issue.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.create_issue("owner", "repo", "Title", "Body")
|
res: str = issue_tools.create_issue("owner", "repo", "Title", "Body")
|
||||||
@@ -146,8 +154,8 @@ def test_create_issue_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_add_label_to_issue_success() -> None:
|
def test_add_label_to_issue_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.add_label.return_value = LabelModel(name="bug")
|
mock_client.issues.add_label.return_value = LabelModel(name="bug")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.add_label_to_issue("owner", "repo", 1, "bug")
|
res: str = issue_tools.add_label_to_issue("owner", "repo", 1, "bug")
|
||||||
@@ -155,8 +163,8 @@ def test_add_label_to_issue_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_add_label_to_issue_failure() -> None:
|
def test_add_label_to_issue_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.add_label.side_effect = Exception("API Error")
|
mock_client.issues.add_label.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.add_label_to_issue("owner", "repo", 1, "bug")
|
res: str = issue_tools.add_label_to_issue("owner", "repo", 1, "bug")
|
||||||
@@ -164,8 +172,8 @@ def test_add_label_to_issue_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_add_comment_to_issue_success() -> None:
|
def test_add_comment_to_issue_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.add_comment.return_value = CommentModel(id=1)
|
mock_client.issues.add_comment.return_value = CommentModel(id=1)
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.add_comment_to_issue("owner", "repo", 1, "body")
|
res: str = issue_tools.add_comment_to_issue("owner", "repo", 1, "body")
|
||||||
@@ -173,8 +181,8 @@ def test_add_comment_to_issue_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_add_comment_to_issue_failure() -> None:
|
def test_add_comment_to_issue_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.add_comment.side_effect = Exception("API Error")
|
mock_client.issues.add_comment.side_effect = Exception("API Error")
|
||||||
|
|
||||||
issue_tools: IssueTools = IssueTools(mock_client)
|
issue_tools: IssueTools = IssueTools(mock_client)
|
||||||
res: str = issue_tools.add_comment_to_issue("owner", "repo", 1, "body")
|
res: str = issue_tools.add_comment_to_issue("owner", "repo", 1, "body")
|
||||||
|
|||||||
+69
-53
@@ -6,10 +6,18 @@ from gitea.models import PullRequestModel, CommentModel, RepositoryModel
|
|||||||
from gitea.tools.pr_tools import PRTools
|
from gitea.tools.pr_tools import PRTools
|
||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_success() -> None:
|
def _create_mock_client() -> MagicMock:
|
||||||
|
"""Create a mock GiteaClient with sub-client attributes."""
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
||||||
|
mock_client.prs = MagicMock()
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
|
return mock_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_pull_request_success() -> None:
|
||||||
|
mock_client = _create_mock_client()
|
||||||
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR", state="open")
|
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR", state="open")
|
||||||
mock_client.get_pull_request.return_value = pr
|
mock_client.prs.get_pull_request.return_value = pr
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request("owner", "repo", 1)
|
||||||
@@ -17,12 +25,12 @@ def test_get_pull_request_success() -> None:
|
|||||||
data: dict[str, Any] = json.loads(res)
|
data: dict[str, Any] = json.loads(res)
|
||||||
assert data["number"] == 1
|
assert data["number"] == 1
|
||||||
assert data["title"] == "Test PR"
|
assert data["title"] == "Test PR"
|
||||||
mock_client.get_pull_request.assert_called_once_with("owner", "repo", 1)
|
mock_client.prs.get_pull_request.assert_called_once_with("owner", "repo", 1)
|
||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_failure() -> None:
|
def test_get_pull_request_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_pull_request.side_effect = Exception("API Error")
|
mock_client.prs.get_pull_request.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request("owner", "repo", 1)
|
||||||
@@ -30,18 +38,20 @@ def test_get_pull_request_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_close_pull_request_success() -> None:
|
def test_close_pull_request_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.close_pull_request.return_value = PullRequestModel(number=1, state="closed")
|
mock_client.prs.close_pull_request.return_value = PullRequestModel(
|
||||||
|
number=1, state="closed"
|
||||||
|
)
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.close_pull_request("owner", "repo", 1)
|
res: str = pr_tools.close_pull_request("owner", "repo", 1)
|
||||||
assert res == "Pull request #1 closed successfully."
|
assert res == "Pull request #1 closed successfully."
|
||||||
mock_client.close_pull_request.assert_called_once_with("owner", "repo", 1)
|
mock_client.prs.close_pull_request.assert_called_once_with("owner", "repo", 1)
|
||||||
|
|
||||||
|
|
||||||
def test_close_pull_request_failure() -> None:
|
def test_close_pull_request_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.close_pull_request.side_effect = Exception("API Error")
|
mock_client.prs.close_pull_request.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.close_pull_request("owner", "repo", 1)
|
res: str = pr_tools.close_pull_request("owner", "repo", 1)
|
||||||
@@ -49,9 +59,9 @@ def test_close_pull_request_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_comments_success() -> None:
|
def test_get_pull_request_comments_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
comment: CommentModel = CommentModel(id=123, body="Comment body")
|
comment: CommentModel = CommentModel(id=123, body="Comment body")
|
||||||
mock_client.get_pull_request_comments.return_value = [comment]
|
mock_client.prs.get_pull_request_comments.return_value = [comment]
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request_comments("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request_comments("owner", "repo", 1)
|
||||||
@@ -61,8 +71,8 @@ def test_get_pull_request_comments_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_comments_failure() -> None:
|
def test_get_pull_request_comments_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_pull_request_comments.side_effect = Exception("API Error")
|
mock_client.prs.get_pull_request_comments.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request_comments("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request_comments("owner", "repo", 1)
|
||||||
@@ -70,23 +80,25 @@ def test_get_pull_request_comments_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_assigned_pull_requests_success() -> None:
|
def test_list_assigned_pull_requests_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
repo: RepositoryModel = RepositoryModel(name="repo1", owner="owner1")
|
repo: RepositoryModel = RepositoryModel(name="repo1", owner="owner1")
|
||||||
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR")
|
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR")
|
||||||
mock_client.list_all_user_repos.return_value = [repo]
|
mock_client.repos.list_all_user_repos.return_value = [repo]
|
||||||
mock_client.list_assigned_pull_requests.return_value = [pr]
|
mock_client.prs.list_assigned_pull_requests.return_value = [pr]
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: list[dict[str, Any]] = pr_tools.list_assigned_pull_requests()
|
res: list[dict[str, Any]] = pr_tools.list_assigned_pull_requests()
|
||||||
assert len(res) == 1
|
assert len(res) == 1
|
||||||
assert res[0]["number"] == 1
|
assert res[0]["number"] == 1
|
||||||
mock_client.list_all_user_repos.assert_called_once()
|
mock_client.repos.list_all_user_repos.assert_called_once()
|
||||||
mock_client.list_assigned_pull_requests.assert_called_once_with("owner1", "repo1")
|
mock_client.prs.list_assigned_pull_requests.assert_called_once_with(
|
||||||
|
"owner1", "repo1"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_assigned_pull_requests_failure() -> None:
|
def test_list_assigned_pull_requests_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.list_all_user_repos.side_effect = Exception("API Error")
|
mock_client.repos.list_all_user_repos.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: list[dict[str, Any]] = pr_tools.list_assigned_pull_requests()
|
res: list[dict[str, Any]] = pr_tools.list_assigned_pull_requests()
|
||||||
@@ -94,9 +106,9 @@ def test_list_assigned_pull_requests_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_pull_requests_success() -> None:
|
def test_list_pull_requests_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR")
|
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR")
|
||||||
mock_client.list_repo_pull_requests.return_value = [pr]
|
mock_client.prs.list_repo_pull_requests.return_value = [pr]
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.list_pull_requests("owner", "repo")
|
res: str = pr_tools.list_pull_requests("owner", "repo")
|
||||||
@@ -104,8 +116,8 @@ def test_list_pull_requests_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_pull_requests_empty() -> None:
|
def test_list_pull_requests_empty() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.list_repo_pull_requests.return_value = []
|
mock_client.prs.list_repo_pull_requests.return_value = []
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.list_pull_requests("owner", "repo")
|
res: str = pr_tools.list_pull_requests("owner", "repo")
|
||||||
@@ -113,8 +125,8 @@ def test_list_pull_requests_empty() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_list_pull_requests_failure() -> None:
|
def test_list_pull_requests_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.list_repo_pull_requests.side_effect = Exception("API Error")
|
mock_client.prs.list_repo_pull_requests.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.list_pull_requests("owner", "repo")
|
res: str = pr_tools.list_pull_requests("owner", "repo")
|
||||||
@@ -122,20 +134,24 @@ def test_list_pull_requests_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_create_pull_request_success() -> None:
|
def test_create_pull_request_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
pr: PullRequestModel = PullRequestModel(number=2, title="Title")
|
pr: PullRequestModel = PullRequestModel(number=2, title="Title")
|
||||||
mock_client.create_pr_via_tea.return_value = pr
|
mock_client.prs.create_pr_via_tea.return_value = pr
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.create_pull_request("owner", "repo", "head", "base", "Title", "Desc")
|
res: str = pr_tools.create_pull_request(
|
||||||
|
"owner", "repo", "head", "base", "Title", "Desc"
|
||||||
|
)
|
||||||
data: dict[str, Any] = json.loads(res)
|
data: dict[str, Any] = json.loads(res)
|
||||||
assert data["number"] == 2
|
assert data["number"] == 2
|
||||||
mock_client.create_pr_via_tea.assert_called_once_with("owner", "repo", "Title", "Desc", "head", "base")
|
mock_client.prs.create_pr_via_tea.assert_called_once_with(
|
||||||
|
"owner", "repo", "Title", "Desc", "head", "base"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_pull_request_failure() -> None:
|
def test_create_pull_request_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.create_pr_via_tea.side_effect = Exception("API Error")
|
mock_client.prs.create_pr_via_tea.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.create_pull_request("owner", "repo", "head", "base", "Title")
|
res: str = pr_tools.create_pull_request("owner", "repo", "head", "base", "Title")
|
||||||
@@ -143,8 +159,8 @@ def test_create_pull_request_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_add_label_to_pr_success() -> None:
|
def test_add_label_to_pr_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.add_label_pr.return_value = {}
|
mock_client.prs.add_label_pr.return_value = {}
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.add_label_to_pr("owner", "repo", 1, "bug")
|
res: str = pr_tools.add_label_to_pr("owner", "repo", 1, "bug")
|
||||||
@@ -152,8 +168,8 @@ def test_add_label_to_pr_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_add_label_to_pr_failure() -> None:
|
def test_add_label_to_pr_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.add_label_pr.side_effect = Exception("API Error")
|
mock_client.prs.add_label_pr.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.add_label_to_pr("owner", "repo", 1, "bug")
|
res: str = pr_tools.add_label_to_pr("owner", "repo", 1, "bug")
|
||||||
@@ -161,8 +177,8 @@ def test_add_label_to_pr_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_diff_success() -> None:
|
def test_get_pull_request_diff_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_pull_request_diff.return_value = "diff content"
|
mock_client.prs.get_pull_request_diff.return_value = "diff content"
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request_diff("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request_diff("owner", "repo", 1)
|
||||||
@@ -170,8 +186,8 @@ def test_get_pull_request_diff_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_diff_failure() -> None:
|
def test_get_pull_request_diff_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_pull_request_diff.side_effect = Exception("API Error")
|
mock_client.prs.get_pull_request_diff.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request_diff("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request_diff("owner", "repo", 1)
|
||||||
@@ -179,8 +195,8 @@ def test_get_pull_request_diff_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_patch_success() -> None:
|
def test_get_pull_request_patch_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_pull_request_patch.return_value = "patch content"
|
mock_client.prs.get_pull_request_patch.return_value = "patch content"
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request_patch("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request_patch("owner", "repo", 1)
|
||||||
@@ -188,8 +204,8 @@ def test_get_pull_request_patch_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_get_pull_request_patch_failure() -> None:
|
def test_get_pull_request_patch_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.get_pull_request_patch.side_effect = Exception("API Error")
|
mock_client.prs.get_pull_request_patch.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.get_pull_request_patch("owner", "repo", 1)
|
res: str = pr_tools.get_pull_request_patch("owner", "repo", 1)
|
||||||
@@ -197,8 +213,8 @@ def test_get_pull_request_patch_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_approve_pull_request_success() -> None:
|
def test_approve_pull_request_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.approve_pr.return_value = {}
|
mock_client.prs.approve_pr.return_value = {}
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.approve_pull_request("owner", "repo", 1, "good")
|
res: str = pr_tools.approve_pull_request("owner", "repo", 1, "good")
|
||||||
@@ -206,8 +222,8 @@ def test_approve_pull_request_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_approve_pull_request_failure() -> None:
|
def test_approve_pull_request_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.approve_pr.side_effect = Exception("API Error")
|
mock_client.prs.approve_pr.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.approve_pull_request("owner", "repo", 1, "good")
|
res: str = pr_tools.approve_pull_request("owner", "repo", 1, "good")
|
||||||
@@ -215,8 +231,8 @@ def test_approve_pull_request_failure() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_request_changes_success() -> None:
|
def test_request_changes_success() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.request_changes_pr.return_value = {}
|
mock_client.prs.request_changes_pr.return_value = {}
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.request_changes("owner", "repo", 1, "bad")
|
res: str = pr_tools.request_changes("owner", "repo", 1, "bad")
|
||||||
@@ -224,8 +240,8 @@ def test_request_changes_success() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_request_changes_failure() -> None:
|
def test_request_changes_failure() -> None:
|
||||||
mock_client: MagicMock = MagicMock(spec=GiteaClient)
|
mock_client = _create_mock_client()
|
||||||
mock_client.request_changes_pr.side_effect = Exception("API Error")
|
mock_client.prs.request_changes_pr.side_effect = Exception("API Error")
|
||||||
|
|
||||||
pr_tools: PRTools = PRTools(mock_client)
|
pr_tools: PRTools = PRTools(mock_client)
|
||||||
res: str = pr_tools.request_changes("owner", "repo", 1, "bad")
|
res: str = pr_tools.request_changes("owner", "repo", 1, "bad")
|
||||||
|
|||||||
+30
-25
@@ -9,25 +9,25 @@ from gitea.workspace import WorkspaceManager
|
|||||||
@patch("gitea.workspace.subprocess.run")
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_configure_repo_user(
|
def test_workspace_manager_configure_repo_user(
|
||||||
mock_client_class: MagicMock,
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
mock_run: MagicMock
|
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
mock_user = MagicMock()
|
mock_user = MagicMock()
|
||||||
mock_user.full_name = "Agent Tester"
|
mock_user.full_name = "Agent Tester"
|
||||||
mock_user.login = "agent-test"
|
mock_user.login = "agent-test"
|
||||||
mock_user.email = "agent-test@example.com"
|
mock_user.email = "agent-test@example.com"
|
||||||
mock_client.get_authenticated_user.return_value = mock_user
|
mock_client.repos.get_authenticated_user.return_value = mock_user
|
||||||
|
|
||||||
workspace = WorkspaceManager()
|
workspace = WorkspaceManager()
|
||||||
repo_path = Path("/tmp/mock-repo")
|
repo_path = Path("/tmp/mock-repo")
|
||||||
|
|
||||||
workspace._configure_repo_user(repo_path)
|
workspace._configure_repo_user(repo_path)
|
||||||
|
|
||||||
assert mock_run.call_count >= 3
|
assert mock_run.call_count >= 3
|
||||||
calls = [c[0][0] for c in mock_run.call_args_list]
|
calls = [c[0][0] for c in mock_run.call_args_list]
|
||||||
|
|
||||||
assert any("http.extraHeader" in call for call in calls)
|
assert any("http.extraHeader" in call for call in calls)
|
||||||
assert any("user.name" in call for call in calls)
|
assert any("user.name" in call for call in calls)
|
||||||
assert any("user.email" in call for call in calls)
|
assert any("user.email" in call for call in calls)
|
||||||
@@ -36,27 +36,27 @@ def test_workspace_manager_configure_repo_user(
|
|||||||
@patch("gitea.workspace.subprocess.run")
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_clone_repo(
|
def test_workspace_manager_clone_repo(
|
||||||
mock_client_class: MagicMock,
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
mock_run: MagicMock
|
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
mock_user = MagicMock()
|
mock_user = MagicMock()
|
||||||
mock_user.full_name = "Agent Tester"
|
mock_user.full_name = "Agent Tester"
|
||||||
mock_user.login = "agent-test"
|
mock_user.login = "agent-test"
|
||||||
mock_user.email = "agent-test@example.com"
|
mock_user.email = "agent-test@example.com"
|
||||||
mock_client.get_authenticated_user.return_value = mock_user
|
mock_client.repos.get_authenticated_user.return_value = mock_user
|
||||||
|
|
||||||
workspace = WorkspaceManager()
|
workspace = WorkspaceManager()
|
||||||
|
|
||||||
with patch.object(workspace, "_configure_repo_user") as mock_configure:
|
with patch.object(workspace, "_configure_repo_user") as mock_configure:
|
||||||
with patch.object(workspace, "get_repo_path") as mock_get_path:
|
with patch.object(workspace, "get_repo_path") as mock_get_path:
|
||||||
mock_repo_path = MagicMock(spec=Path)
|
mock_repo_path = MagicMock(spec=Path)
|
||||||
mock_repo_path.exists.return_value = False
|
mock_repo_path.exists.return_value = False
|
||||||
mock_get_path.return_value = mock_repo_path
|
mock_get_path.return_value = mock_repo_path
|
||||||
|
|
||||||
workspace.clone_repo("meeks/repo1")
|
workspace.clone_repo("meeks/repo1")
|
||||||
|
|
||||||
mock_run.assert_called_once()
|
mock_run.assert_called_once()
|
||||||
args = mock_run.call_args[0][0]
|
args = mock_run.call_args[0][0]
|
||||||
assert "clone" in args
|
assert "clone" in args
|
||||||
@@ -64,13 +64,15 @@ def test_workspace_manager_clone_repo(
|
|||||||
mock_configure.assert_called_once_with(mock_repo_path)
|
mock_configure.assert_called_once_with(mock_repo_path)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_fails_if_no_authenticated_user(
|
def test_workspace_manager_fails_if_no_authenticated_user(
|
||||||
mock_client_class: MagicMock
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
mock_client.get_authenticated_user.return_value = None
|
mock_client.repos = MagicMock()
|
||||||
|
mock_client.repos.get_authenticated_user.return_value = None
|
||||||
|
|
||||||
workspace = WorkspaceManager()
|
workspace = WorkspaceManager()
|
||||||
with pytest.raises(RuntimeError, match="No authenticated user found."):
|
with pytest.raises(RuntimeError, match="No authenticated user found."):
|
||||||
@@ -80,15 +82,17 @@ def test_workspace_manager_fails_if_no_authenticated_user(
|
|||||||
workspace._configure_repo_user(Path("/tmp/mock-repo"))
|
workspace._configure_repo_user(Path("/tmp/mock-repo"))
|
||||||
|
|
||||||
|
|
||||||
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_fails_if_authenticated_user_has_no_login(
|
def test_workspace_manager_fails_if_authenticated_user_has_no_login(
|
||||||
mock_client_class: MagicMock
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
) -> None:
|
) -> None:
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
mock_user = MagicMock()
|
mock_user = MagicMock()
|
||||||
mock_user.login = ""
|
mock_user.login = ""
|
||||||
mock_client.get_authenticated_user.return_value = mock_user
|
mock_client.repos.get_authenticated_user.return_value = mock_user
|
||||||
|
|
||||||
workspace = WorkspaceManager()
|
workspace = WorkspaceManager()
|
||||||
with pytest.raises(RuntimeError, match="No authenticated user found."):
|
with pytest.raises(RuntimeError, match="No authenticated user found."):
|
||||||
@@ -101,17 +105,17 @@ def test_workspace_manager_fails_if_authenticated_user_has_no_login(
|
|||||||
@patch("gitea.workspace.subprocess.run")
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_sanitize_repo_no_changes(
|
def test_workspace_manager_sanitize_repo_no_changes(
|
||||||
mock_client_class: MagicMock,
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
mock_run: MagicMock
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# Setup Gitea client mock
|
# Setup Gitea client mock
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
mock_user = MagicMock()
|
mock_user = MagicMock()
|
||||||
mock_user.full_name = "Agent Tester"
|
mock_user.full_name = "Agent Tester"
|
||||||
mock_user.login = "agent-test"
|
mock_user.login = "agent-test"
|
||||||
mock_user.email = "agent-test@example.com"
|
mock_user.email = "agent-test@example.com"
|
||||||
mock_client.get_authenticated_user.return_value = mock_user
|
mock_client.repos.get_authenticated_user.return_value = mock_user
|
||||||
|
|
||||||
# Mock subprocess.run for status check and others
|
# Mock subprocess.run for status check and others
|
||||||
def mock_run_side_effect(args: list[str], **kwargs: Any) -> MagicMock:
|
def mock_run_side_effect(args: list[str], **kwargs: Any) -> MagicMock:
|
||||||
@@ -144,17 +148,17 @@ def test_workspace_manager_sanitize_repo_no_changes(
|
|||||||
@patch("gitea.workspace.subprocess.run")
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_sanitize_repo_with_changes(
|
def test_workspace_manager_sanitize_repo_with_changes(
|
||||||
mock_client_class: MagicMock,
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
mock_run: MagicMock
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# Setup Gitea client mock
|
# Setup Gitea client mock
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
mock_user = MagicMock()
|
mock_user = MagicMock()
|
||||||
mock_user.full_name = "Agent Tester"
|
mock_user.full_name = "Agent Tester"
|
||||||
mock_user.login = "agent-test"
|
mock_user.login = "agent-test"
|
||||||
mock_user.email = "agent-test@example.com"
|
mock_user.email = "agent-test@example.com"
|
||||||
mock_client.get_authenticated_user.return_value = mock_user
|
mock_client.repos.get_authenticated_user.return_value = mock_user
|
||||||
|
|
||||||
# Mock subprocess.run to show modified files
|
# Mock subprocess.run to show modified files
|
||||||
def mock_run_side_effect(args: list[str], **kwargs: Any) -> MagicMock:
|
def mock_run_side_effect(args: list[str], **kwargs: Any) -> MagicMock:
|
||||||
@@ -183,20 +187,21 @@ def test_workspace_manager_sanitize_repo_with_changes(
|
|||||||
@patch("gitea.workspace.subprocess.run")
|
@patch("gitea.workspace.subprocess.run")
|
||||||
@patch("gitea.workspace.GiteaClient")
|
@patch("gitea.workspace.GiteaClient")
|
||||||
def test_workspace_manager_sanitize_repo_fails(
|
def test_workspace_manager_sanitize_repo_fails(
|
||||||
mock_client_class: MagicMock,
|
mock_client_class: MagicMock, mock_run: MagicMock
|
||||||
mock_run: MagicMock
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# Setup Gitea client mock
|
# Setup Gitea client mock
|
||||||
mock_client = MagicMock()
|
mock_client = MagicMock()
|
||||||
mock_client_class.return_value = mock_client
|
mock_client_class.return_value = mock_client
|
||||||
|
mock_client.repos = MagicMock()
|
||||||
mock_user = MagicMock()
|
mock_user = MagicMock()
|
||||||
mock_user.full_name = "Agent Tester"
|
mock_user.full_name = "Agent Tester"
|
||||||
mock_user.login = "agent-test"
|
mock_user.login = "agent-test"
|
||||||
mock_user.email = "agent-test@example.com"
|
mock_user.email = "agent-test@example.com"
|
||||||
mock_client.get_authenticated_user.return_value = mock_user
|
mock_client.repos.get_authenticated_user.return_value = mock_user
|
||||||
|
|
||||||
# Mock remote set-url to fail
|
# Mock remote set-url to fail
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
mock_run.side_effect = subprocess.CalledProcessError(1, "git remote set-url")
|
mock_run.side_effect = subprocess.CalledProcessError(1, "git remote set-url")
|
||||||
|
|
||||||
workspace = WorkspaceManager()
|
workspace = WorkspaceManager()
|
||||||
|
|||||||
Reference in New Issue
Block a user