log-center/app/gitea_client.py
repair-agent 25c9b2d18e
All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 1m39s
fix bug
2026-03-02 17:46:33 +08:00

175 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Gitea API 客户端
"""
import re
import httpx
import os
from typing import Tuple, Optional
class GiteaClient:
"""Gitea API 操作客户端"""
def __init__(self, gitea_url: str = None, token: str = None):
self.gitea_url = (gitea_url or os.getenv("GITEA_URL", "")).rstrip("/")
self.token = token or os.getenv("GITEA_TOKEN", "")
self.base_api_url = f"{self.gitea_url}/api/v1" if self.gitea_url else ""
self.client = httpx.Client(timeout=30)
def _headers(self) -> dict:
"""生成请求头"""
return {
"Authorization": f"token {self.token}",
"Content-Type": "application/json",
}
def _parse_pr_url(self, pr_url: str) -> Tuple[str, str, str, int]:
"""
从 PR URL 提取 base_url, owner, repo, pr_number
例如: https://gitea.airlabs.art/owner/repo/pulls/45
返回: ("https://gitea.airlabs.art", "owner", "repo", 45)
"""
match = re.search(r'(https?://[^/]+)/([^/]+)/([^/]+)/pulls/(\d+)', pr_url)
if not match:
raise ValueError(f"无法解析 PR URL: {pr_url}")
base_url, owner, repo, pr_number = match.groups()
return base_url, owner, repo, int(pr_number)
def _get_api_url(self, pr_url: Optional[str] = None) -> str:
"""获取 API base URL优先从 pr_url 解析,否则用 self.base_api_url"""
if pr_url:
match = re.search(r'(https?://[^/]+)/', pr_url)
if match:
return f"{match.group(1)}/api/v1"
if self.base_api_url:
return self.base_api_url
raise ValueError("无法确定 Gitea API 地址:未配置 GITEA_URL 且 PR URL 中无法提取")
def merge_pr(
self, owner: str, repo: str, pr_number: int, api_base_url: str = None
) -> Tuple[bool, str]:
"""
合并 PR
Args:
owner: 仓库所有者
repo: 仓库名称
pr_number: PR 编号
api_base_url: 可选API base URL从 pr_url 解析得到)
Returns:
(是否成功, 消息)
"""
base = api_base_url or self.base_api_url
url = f"{base}/repos/{owner}/{repo}/pulls/{pr_number}/merge"
payload = {
"Do": "merge", # merge / squash / rebase
"MergeMessageField": f"Merge PR #{pr_number} (approved via Log Center)",
}
try:
response = self.client.post(url, json=payload, headers=self._headers())
response.raise_for_status()
return True, "PR 合并成功"
except httpx.HTTPStatusError as e:
if e.response.status_code == 405:
# PR 已经被合并或关闭,视为成功(期望的结果已达成)
return True, "PR 已经被合并或关闭"
error_msg = f"HTTP {e.response.status_code}"
if e.response.status_code == 409:
error_msg += ": 存在合并冲突"
elif e.response.status_code == 403:
error_msg += ": Token 权限不足"
else:
error_msg += f": {e.response.text[:200]}"
return False, error_msg
except Exception as e:
return False, str(e)
def close_pr(
self, owner: str, repo: str, pr_number: int, reason: str = "", api_base_url: str = None
) -> Tuple[bool, str]:
"""
关闭 PR可选添加评论说明原因
Args:
owner: 仓库所有者
repo: 仓库名称
pr_number: PR 编号
reason: 关闭原因(将作为评论添加)
api_base_url: 可选API base URL从 pr_url 解析得到)
Returns:
(是否成功, 消息)
"""
base = api_base_url or self.base_api_url
# Step 1: 如果提供了原因,先添加评论
if reason:
comment_success, comment_msg = self.add_pr_comment(
owner, repo, pr_number, f"## ❌ 修复被拒绝\n\n**原因:**\n{reason}",
api_base_url=base,
)
if not comment_success:
return False, f"添加评论失败: {comment_msg}"
# Step 2: 关闭 PR
url = f"{base}/repos/{owner}/{repo}/pulls/{pr_number}"
payload = {"state": "closed"}
try:
response = self.client.patch(url, json=payload, headers=self._headers())
response.raise_for_status()
return True, "PR 已关闭"
except httpx.HTTPStatusError as e:
return False, f"HTTP {e.response.status_code}: {e.response.text[:200]}"
except Exception as e:
return False, str(e)
def add_pr_comment(
self, owner: str, repo: str, pr_number: int, comment: str, api_base_url: str = None
) -> Tuple[bool, str]:
"""
添加 PR 评论
Args:
owner: 仓库所有者
repo: 仓库名称
pr_number: PR 编号
comment: 评论内容
api_base_url: 可选API base URL
Returns:
(是否成功, 消息)
"""
base = api_base_url or self.base_api_url
# 注意: Gitea 中 PR 和 Issue 共用评论 API
url = f"{base}/repos/{owner}/{repo}/issues/{pr_number}/comments"
payload = {"body": comment}
try:
response = self.client.post(url, json=payload, headers=self._headers())
response.raise_for_status()
return True, "评论添加成功"
except Exception as e:
return False, str(e)
def merge_pr_by_url(self, pr_url: str) -> Tuple[bool, str]:
"""通过 PR URL 直接合并(从 URL 自动解析 Gitea 地址)"""
try:
base_url, owner, repo, pr_number = self._parse_pr_url(pr_url)
api_base_url = f"{base_url}/api/v1"
return self.merge_pr(owner, repo, pr_number, api_base_url=api_base_url)
except Exception as e:
return False, str(e)
def close_pr_by_url(self, pr_url: str, reason: str = "") -> Tuple[bool, str]:
"""通过 PR URL 直接关闭(从 URL 自动解析 Gitea 地址)"""
try:
base_url, owner, repo, pr_number = self._parse_pr_url(pr_url)
api_base_url = f"{base_url}/api/v1"
return self.close_pr(owner, repo, pr_number, reason, api_base_url=api_base_url)
except Exception as e:
return False, str(e)