init
This commit is contained in:
9
backend/app/services/__init__.py
Normal file
9
backend/app/services/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Git platform services"""
|
||||
|
||||
from app.services.base import BaseGitService
|
||||
from app.services.gitea import GiteaService
|
||||
from app.services.github import GitHubService
|
||||
from app.services.bitbucket import BitbucketService
|
||||
|
||||
__all__ = ["BaseGitService", "GiteaService", "GitHubService", "BitbucketService"]
|
||||
|
||||
77
backend/app/services/base.py
Normal file
77
backend/app/services/base.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Base service for Git platforms"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileChange:
|
||||
"""Represents a changed file in PR"""
|
||||
filename: str
|
||||
status: str # added, modified, removed
|
||||
additions: int
|
||||
deletions: int
|
||||
patch: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PRInfo:
|
||||
"""Pull request information"""
|
||||
number: int
|
||||
title: str
|
||||
description: str
|
||||
author: str
|
||||
source_branch: str
|
||||
target_branch: str
|
||||
url: str
|
||||
state: str
|
||||
|
||||
|
||||
class BaseGitService(ABC):
|
||||
"""Base class for Git platform services"""
|
||||
|
||||
def __init__(self, base_url: str, token: str, repo_owner: str, repo_name: str):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.token = token
|
||||
self.repo_owner = repo_owner
|
||||
self.repo_name = repo_name
|
||||
|
||||
@abstractmethod
|
||||
async def get_pull_request(self, pr_number: int) -> PRInfo:
|
||||
"""Get pull request information"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_pr_files(self, pr_number: int) -> List[FileChange]:
|
||||
"""Get list of changed files in PR"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_file_content(self, file_path: str, ref: str) -> str:
|
||||
"""Get file content at specific ref"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
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"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def create_review(
|
||||
self,
|
||||
pr_number: int,
|
||||
comments: List[Dict[str, Any]],
|
||||
body: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a review with multiple comments"""
|
||||
pass
|
||||
|
||||
181
backend/app/services/bitbucket.py
Normal file
181
backend/app/services/bitbucket.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""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)}
|
||||
|
||||
228
backend/app/services/gitea.py
Normal file
228
backend/app/services/gitea.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""Gitea API service"""
|
||||
|
||||
import httpx
|
||||
from typing import List, Dict, Any, Optional
|
||||
from app.services.base import BaseGitService, FileChange, PRInfo
|
||||
|
||||
|
||||
class GiteaService(BaseGitService):
|
||||
"""Service for interacting with Gitea API"""
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get headers for API requests"""
|
||||
return {
|
||||
"Authorization": f"token {self.token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def _get_repo_path(self) -> str:
|
||||
"""Get repository API path"""
|
||||
return f"{self.base_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}"
|
||||
|
||||
async def get_pull_request(self, pr_number: int) -> PRInfo:
|
||||
"""Get pull request information from Gitea"""
|
||||
url = f"{self._get_repo_path()}/pulls/{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["number"],
|
||||
title=data["title"],
|
||||
description=data.get("body", ""),
|
||||
author=data["user"]["login"],
|
||||
source_branch=data["head"]["ref"],
|
||||
target_branch=data["base"]["ref"],
|
||||
url=data["html_url"],
|
||||
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()}/pulls/{pr_number}/files"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=self._get_headers())
|
||||
response.raise_for_status()
|
||||
files_data = response.json()
|
||||
|
||||
changes = []
|
||||
for file in files_data:
|
||||
patch = file.get("patch")
|
||||
|
||||
# Если patch отсутствует, попробуем получить через diff API
|
||||
if not patch:
|
||||
print(f"⚠️ Patch отсутствует для {file['filename']}, попытка получить через .diff")
|
||||
try:
|
||||
diff_url = f"{self._get_repo_path()}/pulls/{pr_number}.diff"
|
||||
diff_response = await client.get(diff_url, headers=self._get_headers())
|
||||
if diff_response.status_code == 200:
|
||||
full_diff = diff_response.text
|
||||
# Извлекаем diff для конкретного файла
|
||||
patch = self._extract_file_diff(full_diff, file["filename"])
|
||||
print(f"✅ Получен diff через .diff API ({len(patch) if patch else 0} символов)")
|
||||
except Exception as e:
|
||||
print(f"❌ Не удалось получить diff: {e}")
|
||||
|
||||
changes.append(FileChange(
|
||||
filename=file["filename"],
|
||||
status=file["status"],
|
||||
additions=file.get("additions", 0),
|
||||
deletions=file.get("deletions", 0),
|
||||
patch=patch
|
||||
))
|
||||
|
||||
return changes
|
||||
|
||||
def _extract_file_diff(self, full_diff: str, filename: str) -> str:
|
||||
"""Extract diff for specific file from full diff"""
|
||||
lines = full_diff.split('\n')
|
||||
file_diff = []
|
||||
in_file = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
# Начало diff для файла
|
||||
if line.startswith('diff --git') and filename in line:
|
||||
in_file = True
|
||||
file_diff.append(line)
|
||||
continue
|
||||
|
||||
# Следующий файл - прекращаем
|
||||
if in_file and line.startswith('diff --git') and filename not in line:
|
||||
break
|
||||
|
||||
if in_file:
|
||||
file_diff.append(line)
|
||||
|
||||
return '\n'.join(file_diff) if file_diff else None
|
||||
|
||||
async def get_file_content(self, file_path: str, ref: str) -> str:
|
||||
"""Get file content at specific ref"""
|
||||
url = f"{self._get_repo_path()}/contents/{file_path}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
params={"ref": ref}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Gitea returns base64 encoded content
|
||||
import base64
|
||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
||||
return content
|
||||
|
||||
async def get_pr_commits(self, pr_number: int) -> List[Dict[str, Any]]:
|
||||
"""Get commits in PR"""
|
||||
url = f"{self._get_repo_path()}/pulls/{pr_number}/commits"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=self._get_headers())
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
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()}/pulls/{pr_number}/reviews"
|
||||
|
||||
payload = {
|
||||
"body": comment,
|
||||
"commit_id": commit_id,
|
||||
"comments": [{
|
||||
"path": file_path,
|
||||
"body": comment,
|
||||
"new_position": 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 with file_path, line_number, content, severity
|
||||
body: Overall review summary (markdown supported)
|
||||
event: Review event (не используется, для совместимости)
|
||||
|
||||
Note: Gitea не поддерживает inline комментарии через API,
|
||||
поэтому создаем отдельный комментарий для каждой проблемы.
|
||||
"""
|
||||
print(f"\n📤 Публикация ревью в Gitea PR #{pr_number}")
|
||||
print(f" Комментариев для публикации: {len(comments)}")
|
||||
|
||||
url = f"{self._get_repo_path()}/issues/{pr_number}/comments"
|
||||
|
||||
# 1. Сначала публикуем общий summary
|
||||
if body:
|
||||
print(f"\n 📝 Публикация общего summary ({len(body)} символов)...")
|
||||
payload = {"body": 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(), "💬")
|
||||
|
||||
# Создаем ссылку на строку
|
||||
file_url = f"{self.base_url}/{self.repo_owner}/{self.repo_name}/pulls/{pr_number}/files#L{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 = {"body": 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)}
|
||||
|
||||
181
backend/app/services/github.py
Normal file
181
backend/app/services/github.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""GitHub API service"""
|
||||
|
||||
import httpx
|
||||
from typing import List, Dict, Any
|
||||
from app.services.base import BaseGitService, FileChange, PRInfo
|
||||
|
||||
|
||||
class GitHubService(BaseGitService):
|
||||
"""Service for interacting with GitHub API"""
|
||||
|
||||
def __init__(self, base_url: str, token: str, repo_owner: str, repo_name: str):
|
||||
# GitHub always uses api.github.com
|
||||
super().__init__("https://api.github.com", token, repo_owner, repo_name)
|
||||
|
||||
def _get_headers(self) -> Dict[str, str]:
|
||||
"""Get headers for API requests"""
|
||||
return {
|
||||
"Authorization": f"token {self.token}",
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def _get_repo_path(self) -> str:
|
||||
"""Get repository API path"""
|
||||
return f"{self.base_url}/repos/{self.repo_owner}/{self.repo_name}"
|
||||
|
||||
async def get_pull_request(self, pr_number: int) -> PRInfo:
|
||||
"""Get pull request information from GitHub"""
|
||||
url = f"{self._get_repo_path()}/pulls/{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["number"],
|
||||
title=data["title"],
|
||||
description=data.get("body", ""),
|
||||
author=data["user"]["login"],
|
||||
source_branch=data["head"]["ref"],
|
||||
target_branch=data["base"]["ref"],
|
||||
url=data["html_url"],
|
||||
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()}/pulls/{pr_number}/files"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, headers=self._get_headers())
|
||||
response.raise_for_status()
|
||||
files_data = response.json()
|
||||
|
||||
changes = []
|
||||
for file in files_data:
|
||||
changes.append(FileChange(
|
||||
filename=file["filename"],
|
||||
status=file["status"],
|
||||
additions=file.get("additions", 0),
|
||||
deletions=file.get("deletions", 0),
|
||||
patch=file.get("patch")
|
||||
))
|
||||
|
||||
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()}/contents/{file_path}"
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers=self._get_headers(),
|
||||
params={"ref": ref}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# GitHub returns base64 encoded content
|
||||
import base64
|
||||
content = base64.b64decode(data["content"]).decode("utf-8")
|
||||
return content
|
||||
|
||||
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()}/pulls/{pr_number}/comments"
|
||||
|
||||
payload = {
|
||||
"body": comment,
|
||||
"commit_id": commit_id,
|
||||
"path": file_path,
|
||||
"line": line_number,
|
||||
"side": "RIGHT"
|
||||
}
|
||||
|
||||
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📤 Публикация ревью в GitHub PR #{pr_number}")
|
||||
print(f" Комментариев для публикации: {len(comments)}")
|
||||
|
||||
url = f"{self._get_repo_path()}/issues/{pr_number}/comments"
|
||||
|
||||
# 1. Сначала публикуем общий summary
|
||||
if body:
|
||||
print(f"\n 📝 Публикация общего summary ({len(body)} символов)...")
|
||||
payload = {"body": 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(), "💬")
|
||||
|
||||
# GitHub ссылка на строку
|
||||
file_url = f"https://github.com/{self.repo_owner}/{self.repo_name}/pull/{pr_number}/files#L{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 = {"body": 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)}
|
||||
|
||||
Reference in New Issue
Block a user