182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
"""Bitbucket API service"""
|
||
|
||
import httpx
|
||
from typing import List, Dict, Any
|
||
from app.services.base import BaseGitService, FileChange, PRInfo
|
||
|
||
|
||
class BitbucketService(BaseGitService):
|
||
"""Service for interacting with Bitbucket API"""
|
||
|
||
def __init__(self, base_url: str, token: str, repo_owner: str, repo_name: str):
|
||
# Bitbucket Cloud uses api.bitbucket.org
|
||
super().__init__("https://api.bitbucket.org/2.0", token, repo_owner, repo_name)
|
||
|
||
def _get_headers(self) -> Dict[str, str]:
|
||
"""Get headers for API requests"""
|
||
return {
|
||
"Authorization": f"Bearer {self.token}",
|
||
"Content-Type": "application/json"
|
||
}
|
||
|
||
def _get_repo_path(self) -> str:
|
||
"""Get repository API path"""
|
||
return f"{self.base_url}/repositories/{self.repo_owner}/{self.repo_name}"
|
||
|
||
async def get_pull_request(self, pr_number: int) -> PRInfo:
|
||
"""Get pull request information from Bitbucket"""
|
||
url = f"{self._get_repo_path()}/pullrequests/{pr_number}"
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(url, headers=self._get_headers())
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
return PRInfo(
|
||
number=data["id"],
|
||
title=data["title"],
|
||
description=data.get("description", ""),
|
||
author=data["author"]["display_name"],
|
||
source_branch=data["source"]["branch"]["name"],
|
||
target_branch=data["destination"]["branch"]["name"],
|
||
url=data["links"]["html"]["href"],
|
||
state=data["state"]
|
||
)
|
||
|
||
async def get_pr_files(self, pr_number: int) -> List[FileChange]:
|
||
"""Get list of changed files in PR"""
|
||
url = f"{self._get_repo_path()}/pullrequests/{pr_number}/diffstat"
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(url, headers=self._get_headers())
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
changes = []
|
||
for file in data.get("values", []):
|
||
status = file.get("status", "modified")
|
||
changes.append(FileChange(
|
||
filename=file["new"]["path"] if file.get("new") else file["old"]["path"],
|
||
status=status,
|
||
additions=file.get("lines_added", 0),
|
||
deletions=file.get("lines_removed", 0)
|
||
))
|
||
|
||
return changes
|
||
|
||
async def get_file_content(self, file_path: str, ref: str) -> str:
|
||
"""Get file content at specific ref"""
|
||
url = f"{self._get_repo_path()}/src/{ref}/{file_path}"
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(url, headers=self._get_headers())
|
||
response.raise_for_status()
|
||
return response.text
|
||
|
||
async def create_review_comment(
|
||
self,
|
||
pr_number: int,
|
||
file_path: str,
|
||
line_number: int,
|
||
comment: str,
|
||
commit_id: str
|
||
) -> Dict[str, Any]:
|
||
"""Create a review comment on PR"""
|
||
url = f"{self._get_repo_path()}/pullrequests/{pr_number}/comments"
|
||
|
||
payload = {
|
||
"content": {
|
||
"raw": comment
|
||
},
|
||
"inline": {
|
||
"path": file_path,
|
||
"to": line_number
|
||
}
|
||
}
|
||
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.post(
|
||
url,
|
||
headers=self._get_headers(),
|
||
json=payload
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
async def create_review(
|
||
self,
|
||
pr_number: int,
|
||
comments: List[Dict[str, Any]],
|
||
body: str = "",
|
||
event: str = "COMMENT"
|
||
) -> Dict[str, Any]:
|
||
"""Create a review with separate comment for each issue
|
||
|
||
Args:
|
||
pr_number: PR number
|
||
comments: List of comments
|
||
body: Overall review summary (markdown supported)
|
||
event: Review event (не используется, для совместимости)
|
||
"""
|
||
print(f"\n📤 Публикация ревью в Bitbucket PR #{pr_number}")
|
||
print(f" Комментариев для публикации: {len(comments)}")
|
||
|
||
url = f"{self._get_repo_path()}/pullrequests/{pr_number}/comments"
|
||
|
||
# 1. Сначала публикуем общий summary
|
||
if body:
|
||
print(f"\n 📝 Публикация общего summary ({len(body)} символов)...")
|
||
payload = {
|
||
"content": {
|
||
"raw": body
|
||
}
|
||
}
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
response = await client.post(
|
||
url,
|
||
headers=self._get_headers(),
|
||
json=payload
|
||
)
|
||
response.raise_for_status()
|
||
print(f" ✅ Summary опубликован!")
|
||
|
||
# 2. Затем публикуем каждую проблему отдельным комментарием
|
||
if comments:
|
||
print(f"\n 💬 Публикация {len(comments)} отдельных комментариев...")
|
||
for i, comment in enumerate(comments, 1):
|
||
severity_emoji = {
|
||
"ERROR": "❌",
|
||
"WARNING": "⚠️",
|
||
"INFO": "ℹ️"
|
||
}.get(comment.get("severity", "INFO").upper(), "💬")
|
||
|
||
# Bitbucket ссылка на строку
|
||
file_url = f"https://bitbucket.org/{self.repo_owner}/{self.repo_name}/pull-requests/{pr_number}/diff#{comment['file_path']}T{comment['line_number']}"
|
||
|
||
# Форматируем комментарий
|
||
comment_body = f"{severity_emoji} **[`{comment['file_path']}:{comment['line_number']}`]({file_url})**\n\n"
|
||
comment_body += f"**{comment.get('severity', 'INFO').upper()}**: {comment['content']}"
|
||
|
||
payload = {
|
||
"content": {
|
||
"raw": comment_body
|
||
}
|
||
}
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
response = await client.post(
|
||
url,
|
||
headers=self._get_headers(),
|
||
json=payload
|
||
)
|
||
response.raise_for_status()
|
||
print(f" ✅ {i}/{len(comments)}: {comment['file_path']}:{comment['line_number']}")
|
||
except Exception as e:
|
||
print(f" ❌ {i}/{len(comments)}: Ошибка - {e}")
|
||
|
||
print(f"\n 🎉 Все комментарии опубликованы!")
|
||
return {"summary": "posted", "comments_count": len(comments)}
|
||
|