Files
headquarter/apps/api/src/api/git_repositories.py
T
Developer c527393d2e refactor: extract schemas and update routers (Task 3.2)
- Create schemas/ directory with Pydantic request/response models
  - tool_instance.py, tool_type.py, git_repository.py, config_profile.py
  - config_folder.py, tool_config.py, project.py, ssh_key.py
- Update api/tool_instances.py to import CreateInstanceRequest from schemas
- Update api/git_repositories.py to import from schemas
- Update api/config_profiles.py to import from schemas
- Update api/tool_types.py to import all schemas from schemas/tool_type.py

Quality gates: Python syntax check (pass)
Refs: repo-restructure Task 3.2
2026-06-02 19:37:25 +00:00

1051 lines
33 KiB
Python

import logging
import os
import shutil
import subprocess
import uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user, get_db_session, get_owned_project
from src.schemas.git_repository import (
GitRepositoryCreate,
GitRepositoryResponse,
URLParseRequest,
URLParseResponse,
FileListResponse,
FileContentResponse,
BranchesResponse,
FileUpdateRequest,
FileUpdateResponse,
StatusResponse,
BranchCreateRequest,
CheckoutRequest,
CommitRequest,
CommitResponse,
FetchResponse,
PullResponse,
PushResponse,
MergeRequest,
MergeResponse,
)
from src.config import Settings
from src.models.git_repository import GitRepository
from src.models.project import Project
from src.models.user import User
from src.utils.git_files import (
commit_file,
get_file_content,
list_branches,
list_tree,
)
from src.utils.git_control import (
checkout_branch,
commit_changes,
create_branch,
delete_branch,
fetch,
get_status,
merge,
pull,
push,
)
from src.utils.git_history import get_commit_detail, get_commit_history
from src.utils.git_url_parser import parse_git_url
router = APIRouter(prefix="/projects", tags=["git-repositories"])
logger = logging.getLogger(__name__)
def _get_repo_path(user_id: uuid.UUID, project_id: uuid.UUID, name: str) -> str:
"""Generate the filesystem path for a repository.
Args:
user_id: UUID of the repository owner.
project_id: UUID of the project.
name: Repository name.
Returns:
Absolute path to the repository directory.
"""
base = Settings().repo_base_path or "/data/repos"
return os.path.join(base, str(user_id), str(project_id), f"{name}.git")
def _build_provider_clone_url(owner: str, repo: str) -> str:
"""Build the SSH clone URL for the fixed git provider."""
return f"git@git.commumedia.org:{owner}/{repo}.git"
def _preflight_remote_repository(remote_url: str) -> None:
"""Verify a remote repository is reachable before cloning."""
try:
result = subprocess.run(
["git", "ls-remote", remote_url],
capture_output=True,
text=True,
timeout=60,
)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="remote repository check timed out")
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
if result.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="repository not found or inaccessible",
)
def _clone_working_repository(remote_url: str, repo_path: str) -> None:
try:
result = subprocess.run(
["git", "clone", remote_url, repo_path],
capture_output=True,
text=True,
timeout=300,
)
except subprocess.TimeoutExpired:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out")
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
if result.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to clone repository: {result.stderr}",
)
def _init_working_repository(repo_path: str) -> None:
try:
result = subprocess.run(
["git", "init", "-b", "main", repo_path],
capture_output=True,
text=True,
)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
if result.returncode == 0:
return
fallback = subprocess.run(
["git", "init", repo_path],
capture_output=True,
text=True,
)
if fallback.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to initialize repository: {fallback.stderr}",
)
ref_result = subprocess.run(
["git", "-C", repo_path, "symbolic-ref", "HEAD", "refs/heads/main"],
capture_output=True,
text=True,
)
if ref_result.returncode != 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"failed to set initial branch: {ref_result.stderr}",
)
@router.get(
"/{project_id}/repositories",
response_model=list[GitRepositoryResponse],
summary="List repositories",
description="List all git repositories in a project.",
)
async def list_repositories(
project_id: uuid.UUID,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> list[GitRepository]:
"""List all repositories in a project.
Args:
project_id: UUID of the project.
user_id: ID of the authenticated user.
session: Database session.
Returns:
List of repositories in the project.
"""
result = await session.execute(
select(GitRepository).where(GitRepository.project_id == project_id)
)
return list(result.scalars().all())
@router.delete(
"/{project_id}/repositories/{repo_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a repository",
description="Delete a git repository from the project and remove it from disk.",
)
async def delete_repository(
project_id: uuid.UUID,
repo_id: uuid.UUID,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> Response:
"""Delete a repository.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository to delete.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Empty response with 204 status code.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
# Remove from disk
if os.path.exists(repo.path):
shutil.rmtree(repo.path)
await session.delete(repo)
await session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/repositories/parse-url",
response_model=URLParseResponse,
summary="Parse a git URL",
description="Parse a git URL and detect if it's a browser URL that needs correction.",
)
async def parse_repository_url(data: URLParseRequest) -> URLParseResponse:
"""Parse a git URL and detect if it's a browser URL that needs correction.
Args:
data: Request containing the URL to parse.
Returns:
Parsed URL information including whether it needs parsing and suggested corrections.
"""
result = parse_git_url(data.url)
return URLParseResponse(**result)
@router.post(
"/{project_id}/repositories",
response_model=GitRepositoryResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a repository",
description="Create a new git repository in a project. Can clone from remote or initialize a working repository.",
)
async def create_repository(
project_id: uuid.UUID,
data: GitRepositoryCreate,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> GitRepository:
"""Create a new git repository.
Args:
project_id: UUID of the project.
data: Repository creation data including name and optional remote URL.
user_id: ID of the authenticated user.
session: Database session.
Returns:
The newly created repository.
"""
# Check for duplicate name
existing = await session.execute(
select(GitRepository).where(
GitRepository.project_id == project_id,
GitRepository.name == data.name,
)
)
if existing.scalar_one_or_none():
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="repository name already exists")
# Validate and potentially correct the URL
remote_url = data.remote_url
if remote_url and not data.force_original_url:
parse_result = parse_git_url(remote_url)
if parse_result["needs_parsing"] and parse_result["base_url"]:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail={
"message": "The provided URL appears to be a browser URL, not a git clone URL",
"suggested_url": parse_result["base_url"],
"original_url": remote_url,
"error_code": "URL_NEEDS_PARSING",
},
)
# Use base_url if it was extracted (for URLs without .git suffix)
if parse_result["base_url"]:
remote_url = parse_result["base_url"]
if remote_url:
_preflight_remote_repository(remote_url)
repo_path = _get_repo_path(user.id, project_id, data.name)
# Ensure parent directory exists
os.makedirs(os.path.dirname(repo_path), exist_ok=True)
if remote_url:
_clone_working_repository(remote_url, repo_path)
else:
_init_working_repository(repo_path)
repo = GitRepository(
name=data.name,
path=repo_path,
project_id=project_id,
owner_id=user.id,
is_mirror=False,
remote_url=remote_url,
)
session.add(repo)
await session.commit()
await session.refresh(repo)
return repo
@router.get(
"/{project_id}/repositories/{repo_id}/history",
summary="Get repository history",
description="Get commit history for a repository with optional branch filtering.",
)
async def get_repository_history(
project_id: uuid.UUID,
repo_id: uuid.UUID,
view: str = "graph",
branch: str | None = None,
limit: int = 100,
offset: int = 0,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Get commit history for a repository.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
view: View type for history display (default: graph).
branch: Optional branch name to filter commits.
limit: Maximum number of commits to return (default: 100).
offset: Number of commits to skip (default: 0).
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary containing commit history data.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
history = get_commit_history(repo.path, branch=branch, limit=limit, offset=offset)
return history
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
@router.get(
"/{project_id}/repositories/{repo_id}/commits/{commit_hash}",
summary="Get commit details",
description="Get detailed information about a specific commit.",
)
async def get_repository_commit(
project_id: uuid.UUID,
repo_id: uuid.UUID,
commit_hash: str,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Get detailed information about a specific commit.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
commit_hash: Hash of the commit to retrieve.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary containing commit details.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
detail = get_commit_detail(repo.path, commit_hash)
return detail
except (RuntimeError, ValueError) as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# File browsing endpoints
@router.get(
"/{project_id}/repositories/{repo_id}/files",
response_model=FileListResponse,
summary="List repository files",
description="List files and directories in a repository path.",
)
async def list_repository_files(
project_id: uuid.UUID,
repo_id: uuid.UUID,
branch: str = "main",
path: str = "",
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> FileListResponse:
"""List files and directories in a repository path.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
branch: Branch name to browse (default: main).
path: Directory path within the repository (default: root).
user_id: ID of the authenticated user.
session: Database session.
Returns:
List of files and directories in the specified path.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
entries = list_tree(repo.path, branch=branch, path=path)
return FileListResponse(
path=path,
branch=branch,
entries=[
{
"name": e.name,
"type": e.type,
"path": e.path,
"size": e.size,
"mode": e.mode,
"last_commit": e.last_commit,
}
for e in entries
],
)
except RuntimeError as e:
logger.error(
"Failed to list files for repo %s (path=%s, branch=%s): %s",
repo_id,
path,
branch,
str(e),
exc_info=True,
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.get(
"/{project_id}/repositories/{repo_id}/files/content",
response_model=FileContentResponse,
summary="Get file content",
description="Get the content of a file in a repository.",
)
async def get_repository_file_content(
project_id: uuid.UUID,
repo_id: uuid.UUID,
branch: str,
path: str,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> FileContentResponse:
"""Get the content of a file.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
branch: Branch name where the file is located.
path: File path within the repository.
user_id: ID of the authenticated user.
session: Database session.
Returns:
File content and metadata.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
file_content = get_file_content(repo.path, branch=branch, path=path)
return FileContentResponse(
path=file_content.path,
branch=file_content.branch,
content=file_content.content,
size=file_content.size,
encoding=file_content.encoding,
language=file_content.language,
is_binary=file_content.is_binary,
last_commit=file_content.last_commit,
)
except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="file not found")
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.get(
"/{project_id}/repositories/{repo_id}/branches",
response_model=BranchesResponse,
summary="List branches",
description="List all branches in the repository.",
)
async def get_repository_branches(
project_id: uuid.UUID,
repo_id: uuid.UUID,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> BranchesResponse:
"""List all branches in the repository.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
user_id: ID of the authenticated user.
session: Database session.
Returns:
List of branches and the default branch name.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
branches, default_branch = list_branches(repo.path)
return BranchesResponse(
branches=[
{
"name": b.name,
"is_default": b.is_default,
"last_commit": b.last_commit,
}
for b in branches
],
default_branch=default_branch,
)
except RuntimeError as e:
logger.error(
"Failed to list branches for repo %s: %s",
repo_id,
str(e),
exc_info=True,
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/files/content",
response_model=FileUpdateResponse,
summary="Update file content",
description="Update a file and create a commit.",
)
async def update_repository_file(
project_id: uuid.UUID,
repo_id: uuid.UUID,
data: FileUpdateRequest,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> FileUpdateResponse:
"""Update a file and create a commit.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
data: File update data including path, branch, content, and commit message.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Commit information for the file update.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
# Get user info for commit
author_name = user.name or "Unknown"
author_email = user.email or "unknown@example.com"
try:
commit_hash = commit_file(
repo_path=repo.path,
branch=data.branch,
path=data.path,
content=data.content,
commit_message=data.commit_message,
author_name=author_name,
author_email=author_email,
)
return FileUpdateResponse(
commit_hash=commit_hash,
message=data.commit_message,
branch=data.branch,
)
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# Git Control Endpoints
@router.get(
"/{project_id}/repositories/{repo_id}/status",
response_model=StatusResponse,
summary="Get repository status",
description="Get the working directory status including modified, added, and deleted files.",
)
async def get_repository_status(
project_id: uuid.UUID,
repo_id: uuid.UUID,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> StatusResponse:
"""Get the working directory status.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Repository status including branch, modified files, and ahead/behind counts.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
status_result = get_status(repo.path)
return StatusResponse(
branch=status_result.branch,
modified=status_result.modified,
added=status_result.added,
deleted=status_result.deleted,
untracked=status_result.untracked,
renamed=status_result.renamed,
ahead=status_result.ahead,
behind=status_result.behind,
)
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/branches",
summary="Create a branch",
description="Create a new branch in the repository.",
)
async def create_repository_branch(
project_id: uuid.UUID,
repo_id: uuid.UUID,
data: BranchCreateRequest,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Create a new branch.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
data: Branch creation data including name and optional base branch.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary with success message and branch name.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
create_branch(repo.path, data.name, data.base_branch)
return {"message": f"Branch '{data.name}' created", "branch": data.name}
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.delete(
"/{project_id}/repositories/{repo_id}/branches/{branch_name}",
summary="Delete a branch",
description="Delete a branch from the repository.",
)
async def delete_repository_branch(
project_id: uuid.UUID,
repo_id: uuid.UUID,
branch_name: str,
force: bool = False,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Delete a branch.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
branch_name: Name of the branch to delete.
force: Whether to force delete the branch.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary with success message.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
delete_branch(repo.path, branch_name, force)
return {"message": f"Branch '{branch_name}' deleted"}
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/checkout",
summary="Checkout a branch",
description="Checkout a branch in the repository.",
)
async def checkout_repository_branch(
project_id: uuid.UUID,
repo_id: uuid.UUID,
data: CheckoutRequest,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Checkout a branch.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
data: Checkout request containing the branch name.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary with success message and checked out branch name.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
checkout_branch(repo.path, data.branch)
return {"message": f"Checked out branch '{data.branch}'", "branch": data.branch}
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/commit",
response_model=CommitResponse,
summary="Commit changes",
description="Commit changes to the repository.",
)
async def commit_repository_changes(
project_id: uuid.UUID,
repo_id: uuid.UUID,
data: CommitRequest,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> CommitResponse:
"""Commit changes to the repository.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
data: Commit request containing message and optional files to commit.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Commit information including hash and message.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
# Get user info for commit
author_name = user.name or "Unknown"
author_email = user.email or "unknown@example.com"
try:
commit_hash = commit_changes(
repo_path=repo.path,
message=data.message,
author_name=author_name,
author_email=author_email,
files=data.files,
)
return CommitResponse(
commit_hash=commit_hash,
message=data.message,
)
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/fetch",
response_model=FetchResponse,
summary="Fetch from remote",
description="Fetch updates from the remote repository.",
)
async def fetch_repository(
project_id: uuid.UUID,
repo_id: uuid.UUID,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> FetchResponse:
"""Fetch from remote.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Success message.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
fetch(repo.path)
return FetchResponse(message="Fetched from remote")
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/pull",
response_model=PullResponse,
summary="Pull from remote",
description="Pull updates from the remote repository.",
)
async def pull_repository(
project_id: uuid.UUID,
repo_id: uuid.UUID,
branch: str | None = None,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> PullResponse:
"""Pull updates from remote.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
branch: Optional branch name to pull.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Success message.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
pull(repo.path, branch)
return PullResponse(message="Pulled from remote")
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/push",
response_model=PushResponse,
summary="Push to remote",
description="Push changes to the remote repository.",
)
async def push_repository(
project_id: uuid.UUID,
repo_id: uuid.UUID,
branch: str | None = None,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> PushResponse:
"""Push changes to remote.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
branch: Optional branch name to push.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Success message.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
push(repo.path, branch)
return PushResponse(message="Pushed to remote")
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@router.post(
"/{project_id}/repositories/{repo_id}/merge",
response_model=MergeResponse,
summary="Merge branches",
description="Merge one branch into another.",
)
async def merge_repository_branches(
project_id: uuid.UUID,
repo_id: uuid.UUID,
data: MergeRequest,
user: User = Depends(get_current_user),
project: Project = Depends(get_owned_project),
session: AsyncSession = Depends(get_db_session),
) -> MergeResponse:
"""Merge branches.
Args:
project_id: UUID of the project.
repo_id: UUID of the repository.
data: Merge request containing source branch, optional target branch, and message.
user_id: ID of the authenticated user.
session: Database session.
Returns:
Merge result with commit hash and message.
"""
repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found")
if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk")
try:
commit_hash = merge(
repo_path=repo.path,
source_branch=data.source_branch,
target_branch=data.target_branch,
message=data.message,
)
return MergeResponse(
commit_hash=commit_hash,
message=data.message or f"Merge {data.source_branch}",
)
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))