init
This commit is contained in:
14
backend/app/api/__init__.py
Normal file
14
backend/app/api/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""API endpoints"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import repositories, reviews, webhooks
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(repositories.router, prefix="/repositories", tags=["repositories"])
|
||||
api_router.include_router(reviews.router, prefix="/reviews", tags=["reviews"])
|
||||
api_router.include_router(webhooks.router, prefix="/webhooks", tags=["webhooks"])
|
||||
|
||||
__all__ = ["api_router"]
|
||||
|
||||
419
backend/app/api/repositories.py
Normal file
419
backend/app/api/repositories.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""Repository management endpoints"""
|
||||
|
||||
import secrets
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from typing import List
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Repository
|
||||
from app.schemas.repository import (
|
||||
RepositoryCreate,
|
||||
RepositoryUpdate,
|
||||
RepositoryResponse,
|
||||
RepositoryList
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def get_cipher():
|
||||
"""Get Fernet cipher for encryption"""
|
||||
# Use first 32 bytes of encryption key, base64 encoded
|
||||
key = settings.encryption_key.encode()[:32]
|
||||
# Pad to 32 bytes if needed
|
||||
key = key.ljust(32, b'0')
|
||||
# Base64 encode for Fernet
|
||||
import base64
|
||||
key_b64 = base64.urlsafe_b64encode(key)
|
||||
return Fernet(key_b64)
|
||||
|
||||
|
||||
def encrypt_token(token: str) -> str:
|
||||
"""Encrypt API token"""
|
||||
cipher = get_cipher()
|
||||
return cipher.encrypt(token.encode()).decode()
|
||||
|
||||
|
||||
def decrypt_token(encrypted_token: str) -> str:
|
||||
"""Decrypt API token"""
|
||||
cipher = get_cipher()
|
||||
return cipher.decrypt(encrypted_token.encode()).decode()
|
||||
|
||||
|
||||
@router.get("", response_model=RepositoryList)
|
||||
async def list_repositories(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List all repositories"""
|
||||
# Get total count
|
||||
count_result = await db.execute(select(func.count(Repository.id)))
|
||||
total = count_result.scalar()
|
||||
|
||||
# Get repositories
|
||||
result = await db.execute(
|
||||
select(Repository)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.order_by(Repository.created_at.desc())
|
||||
)
|
||||
repositories = result.scalars().all()
|
||||
|
||||
# Add webhook URL to each repository
|
||||
items = []
|
||||
for repo in repositories:
|
||||
repo_dict = {
|
||||
"id": repo.id,
|
||||
"name": repo.name,
|
||||
"platform": repo.platform,
|
||||
"url": repo.url,
|
||||
"config": repo.config,
|
||||
"is_active": repo.is_active,
|
||||
"created_at": repo.created_at,
|
||||
"updated_at": repo.updated_at,
|
||||
"webhook_url": f"http://{settings.host}:{settings.port}/api/webhooks/{repo.platform.value}/{repo.id}"
|
||||
}
|
||||
items.append(RepositoryResponse(**repo_dict))
|
||||
|
||||
return RepositoryList(items=items, total=total)
|
||||
|
||||
|
||||
@router.post("", response_model=RepositoryResponse)
|
||||
async def create_repository(
|
||||
repository: RepositoryCreate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create a new repository"""
|
||||
# Generate webhook secret if not provided
|
||||
webhook_secret = repository.webhook_secret or secrets.token_urlsafe(32)
|
||||
|
||||
# Encrypt API token (если указан)
|
||||
encrypted_token = encrypt_token(repository.api_token) if repository.api_token else None
|
||||
|
||||
# Create repository
|
||||
db_repository = Repository(
|
||||
name=repository.name,
|
||||
platform=repository.platform,
|
||||
url=repository.url,
|
||||
api_token=encrypted_token,
|
||||
webhook_secret=webhook_secret,
|
||||
config=repository.config or {}
|
||||
)
|
||||
|
||||
db.add(db_repository)
|
||||
await db.commit()
|
||||
await db.refresh(db_repository)
|
||||
|
||||
# Prepare response
|
||||
webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{db_repository.platform.value}/{db_repository.id}"
|
||||
|
||||
return RepositoryResponse(
|
||||
id=db_repository.id,
|
||||
name=db_repository.name,
|
||||
platform=db_repository.platform,
|
||||
url=db_repository.url,
|
||||
config=db_repository.config,
|
||||
is_active=db_repository.is_active,
|
||||
created_at=db_repository.created_at,
|
||||
updated_at=db_repository.updated_at,
|
||||
webhook_url=webhook_url
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{repository_id}", response_model=RepositoryResponse)
|
||||
async def get_repository(
|
||||
repository_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get repository by ID"""
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.id == repository_id)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{repository.platform.value}/{repository.id}"
|
||||
|
||||
return RepositoryResponse(
|
||||
id=repository.id,
|
||||
name=repository.name,
|
||||
platform=repository.platform,
|
||||
url=repository.url,
|
||||
config=repository.config,
|
||||
is_active=repository.is_active,
|
||||
created_at=repository.created_at,
|
||||
updated_at=repository.updated_at,
|
||||
webhook_url=webhook_url
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{repository_id}", response_model=RepositoryResponse)
|
||||
async def update_repository(
|
||||
repository_id: int,
|
||||
repository_update: RepositoryUpdate,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update repository"""
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.id == repository_id)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
# Update fields
|
||||
update_data = repository_update.model_dump(exclude_unset=True)
|
||||
|
||||
# Encrypt API token if provided and not empty
|
||||
if "api_token" in update_data and update_data["api_token"]:
|
||||
update_data["api_token"] = encrypt_token(update_data["api_token"])
|
||||
elif "api_token" in update_data and not update_data["api_token"]:
|
||||
# If empty string provided, don't update token
|
||||
del update_data["api_token"]
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(repository, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(repository)
|
||||
|
||||
webhook_url = f"http://{settings.host}:{settings.port}/api/webhooks/{repository.platform.value}/{repository.id}"
|
||||
|
||||
return RepositoryResponse(
|
||||
id=repository.id,
|
||||
name=repository.name,
|
||||
platform=repository.platform,
|
||||
url=repository.url,
|
||||
config=repository.config,
|
||||
is_active=repository.is_active,
|
||||
created_at=repository.created_at,
|
||||
updated_at=repository.updated_at,
|
||||
webhook_url=webhook_url
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{repository_id}")
|
||||
async def delete_repository(
|
||||
repository_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Delete repository"""
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.id == repository_id)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
await db.delete(repository)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Repository deleted"}
|
||||
|
||||
|
||||
@router.post("/{repository_id}/scan")
|
||||
async def scan_repository(
|
||||
repository_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Scan repository for new pull requests and start reviews"""
|
||||
from app.models import PullRequest, Review
|
||||
from app.models.pull_request import PRStatusEnum
|
||||
from app.models.review import ReviewStatusEnum
|
||||
from app.services import GiteaService, GitHubService, BitbucketService
|
||||
from app.utils import decrypt_token
|
||||
|
||||
# Get repository
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.id == repository_id)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
if not repository.is_active:
|
||||
raise HTTPException(status_code=400, detail="Repository is not active")
|
||||
|
||||
# Parse repository URL to get owner and name
|
||||
parts = repository.url.rstrip('/').split('/')
|
||||
repo_name = parts[-1].replace('.git', '')
|
||||
repo_owner = parts[-2]
|
||||
base_url = '/'.join(parts[:-2])
|
||||
|
||||
# Get appropriate Git service
|
||||
from app.config import settings
|
||||
|
||||
if repository.api_token:
|
||||
try:
|
||||
decrypted_token = decrypt_token(repository.api_token)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
else:
|
||||
# Используем мастер токен
|
||||
platform = repository.platform.value.lower()
|
||||
if platform == "gitea":
|
||||
decrypted_token = settings.master_gitea_token
|
||||
elif platform == "github":
|
||||
decrypted_token = settings.master_github_token
|
||||
elif platform == "bitbucket":
|
||||
decrypted_token = settings.master_bitbucket_token
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported platform: {repository.platform}")
|
||||
|
||||
if not decrypted_token:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"API токен не указан и мастер токен для {platform} не настроен"
|
||||
)
|
||||
|
||||
if repository.platform.value == "gitea":
|
||||
git_service = GiteaService(base_url, decrypted_token, repo_owner, repo_name)
|
||||
elif repository.platform.value == "github":
|
||||
git_service = GitHubService(base_url, decrypted_token, repo_owner, repo_name)
|
||||
elif repository.platform.value == "bitbucket":
|
||||
git_service = BitbucketService(base_url, decrypted_token, repo_owner, repo_name)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported platform: {repository.platform}")
|
||||
|
||||
try:
|
||||
# For Gitea, get list of open PRs
|
||||
import httpx
|
||||
if repository.platform.value == "gitea":
|
||||
url = f"{base_url}/api/v1/repos/{repo_owner}/{repo_name}/pulls"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers={"Authorization": f"token {decrypted_token}"},
|
||||
params={"state": "open"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
prs = response.json()
|
||||
elif repository.platform.value == "github":
|
||||
url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"token {decrypted_token}",
|
||||
"Accept": "application/vnd.github.v3+json"
|
||||
},
|
||||
params={"state": "open"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
prs = response.json()
|
||||
else:
|
||||
# Bitbucket
|
||||
url = f"https://api.bitbucket.org/2.0/repositories/{repo_owner}/{repo_name}/pullrequests"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {decrypted_token}"},
|
||||
params={"state": "OPEN"}
|
||||
)
|
||||
response.raise_for_status()
|
||||
prs = response.json().get("values", [])
|
||||
|
||||
new_reviews = []
|
||||
|
||||
for pr_data in prs:
|
||||
# Get PR number based on platform
|
||||
if repository.platform.value == "bitbucket":
|
||||
pr_number = pr_data["id"]
|
||||
pr_title = pr_data["title"]
|
||||
pr_author = pr_data["author"]["display_name"]
|
||||
pr_url = pr_data["links"]["html"]["href"]
|
||||
source_branch = pr_data["source"]["branch"]["name"]
|
||||
target_branch = pr_data["destination"]["branch"]["name"]
|
||||
else:
|
||||
pr_number = pr_data["number"]
|
||||
pr_title = pr_data["title"]
|
||||
pr_author = pr_data["user"]["login"]
|
||||
pr_url = pr_data["html_url"]
|
||||
source_branch = pr_data["head"]["ref"]
|
||||
target_branch = pr_data["base"]["ref"]
|
||||
|
||||
# Check if PR already exists
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == pr_number
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
|
||||
if not pr:
|
||||
# Create new PR
|
||||
pr = PullRequest(
|
||||
repository_id=repository.id,
|
||||
pr_number=pr_number,
|
||||
title=pr_title,
|
||||
author=pr_author,
|
||||
source_branch=source_branch,
|
||||
target_branch=target_branch,
|
||||
url=pr_url,
|
||||
status=PRStatusEnum.OPEN
|
||||
)
|
||||
db.add(pr)
|
||||
await db.commit()
|
||||
await db.refresh(pr)
|
||||
|
||||
# Check if there's already a review for this PR
|
||||
result = await db.execute(
|
||||
select(Review).where(
|
||||
Review.pull_request_id == pr.id,
|
||||
Review.status.in_([
|
||||
ReviewStatusEnum.PENDING,
|
||||
ReviewStatusEnum.FETCHING,
|
||||
ReviewStatusEnum.ANALYZING,
|
||||
ReviewStatusEnum.COMMENTING
|
||||
])
|
||||
)
|
||||
)
|
||||
existing_review = result.scalar_one_or_none()
|
||||
|
||||
if not existing_review:
|
||||
# Create new review
|
||||
review = Review(
|
||||
pull_request_id=pr.id,
|
||||
status=ReviewStatusEnum.PENDING
|
||||
)
|
||||
db.add(review)
|
||||
await db.commit()
|
||||
await db.refresh(review)
|
||||
|
||||
# Start review in background
|
||||
from app.api.webhooks import start_review_task
|
||||
background_tasks.add_task(
|
||||
start_review_task,
|
||||
review.id,
|
||||
pr.pr_number,
|
||||
repository.id
|
||||
)
|
||||
|
||||
new_reviews.append({
|
||||
"review_id": review.id,
|
||||
"pr_number": pr.pr_number,
|
||||
"pr_title": pr.title
|
||||
})
|
||||
|
||||
return {
|
||||
"message": f"Found {len(prs)} open PR(s), started {len(new_reviews)} new review(s)",
|
||||
"total_prs": len(prs),
|
||||
"new_reviews": len(new_reviews),
|
||||
"reviews": new_reviews
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error scanning repository: {str(e)}")
|
||||
|
||||
218
backend/app/api/reviews.py
Normal file
218
backend/app/api/reviews.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""Review management endpoints"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Review, Comment, PullRequest
|
||||
from app.schemas.review import ReviewResponse, ReviewList, ReviewStats, PullRequestInfo, CommentResponse
|
||||
from app.agents import ReviewerAgent
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=ReviewList)
|
||||
async def list_reviews(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
repository_id: int = None,
|
||||
status: str = None,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""List all reviews with filters"""
|
||||
query = select(Review).options(joinedload(Review.pull_request))
|
||||
|
||||
# Apply filters
|
||||
if repository_id:
|
||||
query = query.join(PullRequest).where(PullRequest.repository_id == repository_id)
|
||||
|
||||
if status:
|
||||
query = query.where(Review.status == status)
|
||||
|
||||
# Get total count
|
||||
count_query = select(func.count(Review.id))
|
||||
if repository_id:
|
||||
count_query = count_query.join(PullRequest).where(PullRequest.repository_id == repository_id)
|
||||
if status:
|
||||
count_query = count_query.where(Review.status == status)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
total = count_result.scalar()
|
||||
|
||||
# Get reviews
|
||||
query = query.offset(skip).limit(limit).order_by(Review.started_at.desc())
|
||||
result = await db.execute(query)
|
||||
reviews = result.scalars().all()
|
||||
|
||||
# Convert to response models
|
||||
items = []
|
||||
for review in reviews:
|
||||
pr_info = PullRequestInfo(
|
||||
id=review.pull_request.id,
|
||||
pr_number=review.pull_request.pr_number,
|
||||
title=review.pull_request.title,
|
||||
author=review.pull_request.author,
|
||||
source_branch=review.pull_request.source_branch,
|
||||
target_branch=review.pull_request.target_branch,
|
||||
url=review.pull_request.url
|
||||
)
|
||||
|
||||
items.append(ReviewResponse(
|
||||
id=review.id,
|
||||
pull_request_id=review.pull_request_id,
|
||||
pull_request=pr_info,
|
||||
status=review.status,
|
||||
started_at=review.started_at,
|
||||
completed_at=review.completed_at,
|
||||
files_analyzed=review.files_analyzed,
|
||||
comments_generated=review.comments_generated,
|
||||
error_message=review.error_message
|
||||
))
|
||||
|
||||
return ReviewList(items=items, total=total)
|
||||
|
||||
|
||||
@router.get("/{review_id}", response_model=ReviewResponse)
|
||||
async def get_review(
|
||||
review_id: int,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get review by ID with comments"""
|
||||
result = await db.execute(
|
||||
select(Review)
|
||||
.options(joinedload(Review.pull_request), joinedload(Review.comments))
|
||||
.where(Review.id == review_id)
|
||||
)
|
||||
review = result.unique().scalar_one_or_none()
|
||||
|
||||
if not review:
|
||||
raise HTTPException(status_code=404, detail="Review not found")
|
||||
|
||||
pr_info = PullRequestInfo(
|
||||
id=review.pull_request.id,
|
||||
pr_number=review.pull_request.pr_number,
|
||||
title=review.pull_request.title,
|
||||
author=review.pull_request.author,
|
||||
source_branch=review.pull_request.source_branch,
|
||||
target_branch=review.pull_request.target_branch,
|
||||
url=review.pull_request.url
|
||||
)
|
||||
|
||||
comments = [
|
||||
CommentResponse(
|
||||
id=comment.id,
|
||||
file_path=comment.file_path,
|
||||
line_number=comment.line_number,
|
||||
content=comment.content,
|
||||
severity=comment.severity,
|
||||
posted=comment.posted,
|
||||
posted_at=comment.posted_at,
|
||||
created_at=comment.created_at
|
||||
)
|
||||
for comment in review.comments
|
||||
]
|
||||
|
||||
return ReviewResponse(
|
||||
id=review.id,
|
||||
pull_request_id=review.pull_request_id,
|
||||
pull_request=pr_info,
|
||||
status=review.status,
|
||||
started_at=review.started_at,
|
||||
completed_at=review.completed_at,
|
||||
files_analyzed=review.files_analyzed,
|
||||
comments_generated=review.comments_generated,
|
||||
error_message=review.error_message,
|
||||
comments=comments
|
||||
)
|
||||
|
||||
|
||||
async def run_review_task(review_id: int, pr_number: int, repository_id: int, db: AsyncSession):
|
||||
"""Background task to run review"""
|
||||
agent = ReviewerAgent(db)
|
||||
await agent.run_review(review_id, pr_number, repository_id)
|
||||
|
||||
|
||||
@router.post("/{review_id}/retry")
|
||||
async def retry_review(
|
||||
review_id: int,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Retry a failed review"""
|
||||
result = await db.execute(
|
||||
select(Review).options(joinedload(Review.pull_request)).where(Review.id == review_id)
|
||||
)
|
||||
review = result.scalar_one_or_none()
|
||||
|
||||
if not review:
|
||||
raise HTTPException(status_code=404, detail="Review not found")
|
||||
|
||||
# Reset review status
|
||||
from app.models.review import ReviewStatusEnum
|
||||
review.status = ReviewStatusEnum.PENDING
|
||||
review.error_message = None
|
||||
await db.commit()
|
||||
|
||||
# Run review in background
|
||||
background_tasks.add_task(
|
||||
run_review_task,
|
||||
review.id,
|
||||
review.pull_request.pr_number,
|
||||
review.pull_request.repository_id,
|
||||
db
|
||||
)
|
||||
|
||||
return {"message": "Review queued"}
|
||||
|
||||
|
||||
@router.get("/stats/dashboard", response_model=ReviewStats)
|
||||
async def get_review_stats(db: AsyncSession = Depends(get_db)):
|
||||
"""Get review statistics for dashboard"""
|
||||
# Total reviews
|
||||
total_result = await db.execute(select(func.count(Review.id)))
|
||||
total_reviews = total_result.scalar()
|
||||
|
||||
# Active reviews
|
||||
from app.models.review import ReviewStatusEnum
|
||||
active_result = await db.execute(
|
||||
select(func.count(Review.id)).where(
|
||||
Review.status.in_([
|
||||
ReviewStatusEnum.PENDING,
|
||||
ReviewStatusEnum.FETCHING,
|
||||
ReviewStatusEnum.ANALYZING,
|
||||
ReviewStatusEnum.COMMENTING
|
||||
])
|
||||
)
|
||||
)
|
||||
active_reviews = active_result.scalar()
|
||||
|
||||
# Completed reviews
|
||||
completed_result = await db.execute(
|
||||
select(func.count(Review.id)).where(Review.status == ReviewStatusEnum.COMPLETED)
|
||||
)
|
||||
completed_reviews = completed_result.scalar()
|
||||
|
||||
# Failed reviews
|
||||
failed_result = await db.execute(
|
||||
select(func.count(Review.id)).where(Review.status == ReviewStatusEnum.FAILED)
|
||||
)
|
||||
failed_reviews = failed_result.scalar()
|
||||
|
||||
# Total comments
|
||||
comments_result = await db.execute(select(func.count(Comment.id)))
|
||||
total_comments = comments_result.scalar()
|
||||
|
||||
# Average comments per review
|
||||
avg_comments = total_comments / total_reviews if total_reviews > 0 else 0
|
||||
|
||||
return ReviewStats(
|
||||
total_reviews=total_reviews,
|
||||
active_reviews=active_reviews,
|
||||
completed_reviews=completed_reviews,
|
||||
failed_reviews=failed_reviews,
|
||||
total_comments=total_comments,
|
||||
avg_comments_per_review=round(avg_comments, 2)
|
||||
)
|
||||
|
||||
110
backend/app/api/webhooks.py
Normal file
110
backend/app/api/webhooks.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Webhook endpoints"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, Header, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas.webhook import GiteaWebhook, GitHubWebhook, BitbucketWebhook
|
||||
from app.webhooks import handle_gitea_webhook, handle_github_webhook, handle_bitbucket_webhook
|
||||
from app.agents import ReviewerAgent
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
async def start_review_task(review_id: int, pr_number: int, repository_id: int):
|
||||
"""Background task to start review"""
|
||||
from app.database import async_session_maker
|
||||
async with async_session_maker() as db:
|
||||
agent = ReviewerAgent(db)
|
||||
await agent.run_review(review_id, pr_number, repository_id)
|
||||
|
||||
|
||||
@router.post("/gitea/{repository_id}")
|
||||
async def gitea_webhook(
|
||||
repository_id: int,
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
x_gitea_signature: Optional[str] = Header(None),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Handle Gitea webhook"""
|
||||
raw_payload = await request.body()
|
||||
webhook_data = GiteaWebhook(**await request.json())
|
||||
|
||||
result = await handle_gitea_webhook(
|
||||
webhook_data=webhook_data,
|
||||
signature=x_gitea_signature or "",
|
||||
raw_payload=raw_payload,
|
||||
db=db
|
||||
)
|
||||
|
||||
# Start review in background if created
|
||||
if "review_id" in result:
|
||||
background_tasks.add_task(
|
||||
start_review_task,
|
||||
result["review_id"],
|
||||
webhook_data.number,
|
||||
repository_id
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/github/{repository_id}")
|
||||
async def github_webhook(
|
||||
repository_id: int,
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
x_hub_signature_256: Optional[str] = Header(None),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Handle GitHub webhook"""
|
||||
raw_payload = await request.body()
|
||||
webhook_data = GitHubWebhook(**await request.json())
|
||||
|
||||
result = await handle_github_webhook(
|
||||
webhook_data=webhook_data,
|
||||
signature=x_hub_signature_256 or "",
|
||||
raw_payload=raw_payload,
|
||||
db=db
|
||||
)
|
||||
|
||||
# Start review in background if created
|
||||
if "review_id" in result:
|
||||
background_tasks.add_task(
|
||||
start_review_task,
|
||||
result["review_id"],
|
||||
webhook_data.number,
|
||||
repository_id
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/bitbucket/{repository_id}")
|
||||
async def bitbucket_webhook(
|
||||
repository_id: int,
|
||||
request: Request,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Handle Bitbucket webhook"""
|
||||
webhook_data = BitbucketWebhook(**await request.json())
|
||||
|
||||
result = await handle_bitbucket_webhook(
|
||||
webhook_data=webhook_data,
|
||||
db=db
|
||||
)
|
||||
|
||||
# Start review in background if created
|
||||
if "review_id" in result:
|
||||
background_tasks.add_task(
|
||||
start_review_task,
|
||||
result["review_id"],
|
||||
webhook_data.pullrequest.id,
|
||||
repository_id
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user