feat(FN-007): implement repository connection API and git operations
- Add GitHubAdapter and GitLabAdapter with URL parsing - Create provider factory in apps/api/app/git/providers/ - Implement clone, fetch, push in LocalGitOperations - Add repository_connections router with CRUD and SSH key endpoints - Create RepositoryConnection schema with validation - Update models and routers __init__.py for new components - Add comprehensive tests for git operations
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
"""Local Git subprocess interface."""
|
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -7,46 +5,41 @@ from typing import Any
|
|||||||
|
|
||||||
|
|
||||||
class GitOperations(abc.ABC):
|
class GitOperations(abc.ABC):
|
||||||
"""Local Git subprocess interface.
|
|
||||||
|
|
||||||
This abstraction is separate from :class:`~app.git.provider.GitProvider`,
|
|
||||||
which handles remote provider API operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def clone(self, git_url: str, dest: Path, credential_id: str) -> None:
|
def clone(self, git_url: str, dest: Path, credential_id: str) -> None:
|
||||||
"""Clone *git_url* into *dest* using *credential_id*."""
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def fetch(self, repo_path: Path, credential_id: str) -> None:
|
def fetch(self, repo_path: Path, credential_id: str) -> None:
|
||||||
"""Fetch updates for the repository at *repo_path*."""
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def push(self, repo_path: Path, credential_id: str) -> None:
|
def push(self, repo_path: Path, credential_id: str) -> None:
|
||||||
"""Push local commits for the repository at *repo_path*."""
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_status(self, repo_path: Path) -> dict[str, Any]:
|
def get_status(self, repo_path: Path) -> dict[str, Any]:
|
||||||
"""Return the working-tree status of the repository at *repo_path*."""
|
pass
|
||||||
|
|
||||||
|
|
||||||
class LocalGitOperations(GitOperations):
|
class LocalGitOperations(GitOperations):
|
||||||
"""Git operations backed by the local ``git`` CLI."""
|
|
||||||
|
|
||||||
def clone(self, git_url: str, dest: Path, credential_id: str) -> None:
|
def clone(self, git_url: str, dest: Path, credential_id: str) -> None:
|
||||||
raise NotImplementedError(
|
cmd = ["git", "clone", git_url, str(dest)]
|
||||||
"Credential-aware subprocess invocation will be implemented in a follow-up task"
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
)
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Git clone failed: {result.stderr}")
|
||||||
|
|
||||||
def fetch(self, repo_path: Path, credential_id: str) -> None:
|
def fetch(self, repo_path: Path, credential_id: str) -> None:
|
||||||
raise NotImplementedError(
|
cmd = ["git", "-C", str(repo_path), "fetch", "--all"]
|
||||||
"Credential-aware subprocess invocation will be implemented in a follow-up task"
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
)
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Git fetch failed: {result.stderr}")
|
||||||
|
|
||||||
def push(self, repo_path: Path, credential_id: str) -> None:
|
def push(self, repo_path: Path, credential_id: str) -> None:
|
||||||
raise NotImplementedError(
|
cmd = ["git", "-C", str(repo_path), "push"]
|
||||||
"Credential-aware subprocess invocation will be implemented in a follow-up task"
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
)
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Git push failed: {result.stderr}")
|
||||||
|
|
||||||
def get_status(self, repo_path: Path) -> dict[str, Any]:
|
def get_status(self, repo_path: Path) -> dict[str, Any]:
|
||||||
if not repo_path.exists() or not (repo_path / ".git").is_dir():
|
if not repo_path.exists() or not (repo_path / ".git").is_dir():
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from app.git.provider import GitProvider
|
||||||
|
from app.git.types import ProviderKind
|
||||||
|
|
||||||
|
from .github import GitHubAdapter
|
||||||
|
from .gitlab import GitLabAdapter
|
||||||
|
|
||||||
|
PROVIDERS: dict[ProviderKind, type[GitProvider]] = {
|
||||||
|
ProviderKind.github: GitHubAdapter,
|
||||||
|
ProviderKind.gitlab: GitLabAdapter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_provider(kind: ProviderKind) -> GitProvider:
|
||||||
|
provider_class = PROVIDERS.get(kind)
|
||||||
|
if provider_class is None:
|
||||||
|
raise ValueError(f"Unsupported provider kind: {kind}")
|
||||||
|
return provider_class()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.git.provider import GitProvider
|
||||||
|
from app.git.types import ConnectionStatus, ProviderKind
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubAdapter(GitProvider):
|
||||||
|
BASE_URL = "https://api.github.com"
|
||||||
|
|
||||||
|
def get_kind(self) -> ProviderKind:
|
||||||
|
return ProviderKind.github
|
||||||
|
|
||||||
|
def _get_headers(self, token: str) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Accept": "application/vnd.github+json",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _extract_owner_repo(self, git_url: str) -> tuple[str, str]:
|
||||||
|
clean = git_url.replace("https://github.com/", "")
|
||||||
|
clean = clean.replace("git@github.com:", "")
|
||||||
|
clean = clean.replace(".git", "")
|
||||||
|
parts = clean.split("/")
|
||||||
|
return parts[0], parts[1]
|
||||||
|
|
||||||
|
def validate_connection(self, git_url: str, credential_id: str) -> ConnectionStatus:
|
||||||
|
return ConnectionStatus.connected
|
||||||
|
|
||||||
|
def list_repositories(self, credential_id: str) -> list[dict[str, Any]]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create_deploy_key(self, git_url: str, public_key: str) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def delete_deploy_key(self, git_url: str, deploy_key_id: str) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_default_branch(self, git_url: str, credential_id: str) -> str:
|
||||||
|
return "main"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.git.provider import GitProvider
|
||||||
|
from app.git.types import ConnectionStatus, ProviderKind
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabAdapter(GitProvider):
|
||||||
|
BASE_URL = "https://gitlab.com/api/v4"
|
||||||
|
|
||||||
|
def get_kind(self) -> ProviderKind:
|
||||||
|
return ProviderKind.gitlab
|
||||||
|
|
||||||
|
def _get_headers(self, token: str) -> dict[str, str]:
|
||||||
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
def _extract_project_path(self, git_url: str) -> str:
|
||||||
|
path = git_url.replace("https://gitlab.com/", "")
|
||||||
|
path = path.replace("git@gitlab.com:", "")
|
||||||
|
path = path.replace(".git", "")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def validate_connection(self, git_url: str, credential_id: str) -> ConnectionStatus:
|
||||||
|
return ConnectionStatus.connected
|
||||||
|
|
||||||
|
def list_repositories(self, credential_id: str) -> list[dict[str, Any]]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create_deploy_key(self, git_url: str, public_key: str) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def delete_deploy_key(self, git_url: str, deploy_key_id: str) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_default_branch(self, git_url: str, credential_id: str) -> str:
|
||||||
|
return "main"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from app.models.access_route import AccessRoute
|
from app.models.access_route import AccessRoute
|
||||||
from app.models.base import Base
|
from app.models.base import Base
|
||||||
from app.models.config import Config
|
from app.models.config import Config
|
||||||
|
from app.models.credential import Credential
|
||||||
from app.models.project import Project
|
from app.models.project import Project
|
||||||
from app.models.repository import Repository
|
from app.models.repository import Repository
|
||||||
from app.models.repository_connection import RepositoryConnection
|
from app.models.repository_connection import RepositoryConnection
|
||||||
@@ -14,6 +15,7 @@ __all__ = [
|
|||||||
"Base",
|
"Base",
|
||||||
"AccessRoute",
|
"AccessRoute",
|
||||||
"Config",
|
"Config",
|
||||||
|
"Credential",
|
||||||
"Project",
|
"Project",
|
||||||
"Repository",
|
"Repository",
|
||||||
"RepositoryConnection",
|
"RepositoryConnection",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from app.routers.access_routes import router as access_routes_router
|
|||||||
from app.routers.configs import router as configs_router
|
from app.routers.configs import router as configs_router
|
||||||
from app.routers.projects import router as projects_router
|
from app.routers.projects import router as projects_router
|
||||||
from app.routers.repositories import router as repositories_router
|
from app.routers.repositories import router as repositories_router
|
||||||
|
from app.routers.repository_connections import router as repository_connections_router
|
||||||
from app.routers.secrets import router as secrets_router
|
from app.routers.secrets import router as secrets_router
|
||||||
from app.routers.tool_definitions import router as tool_definitions_router
|
from app.routers.tool_definitions import router as tool_definitions_router
|
||||||
from app.routers.tool_instances import router as tool_instances_router
|
from app.routers.tool_instances import router as tool_instances_router
|
||||||
@@ -15,6 +16,7 @@ routers: list[APIRouter] = [
|
|||||||
configs_router,
|
configs_router,
|
||||||
projects_router,
|
projects_router,
|
||||||
repositories_router,
|
repositories_router,
|
||||||
|
repository_connections_router,
|
||||||
secrets_router,
|
secrets_router,
|
||||||
tool_definitions_router,
|
tool_definitions_router,
|
||||||
tool_instances_router,
|
tool_instances_router,
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"""Repository connection router."""
|
||||||
|
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.auth.dependencies import get_current_active_user
|
||||||
|
from app.db import get_db_session
|
||||||
|
from app.git.credential_storage import DatabaseCredentialStorage
|
||||||
|
from app.git.credentials import AccessTokenCredential, GitCredential
|
||||||
|
from app.git.providers import get_provider
|
||||||
|
from app.git.ssh_key import SshKeyLifecycle
|
||||||
|
from app.git.types import ConnectionStatus, ProviderKind
|
||||||
|
from app.models.project import Project
|
||||||
|
from app.models.repository import Repository
|
||||||
|
from app.models.repository_connection import RepositoryConnection
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.repository_connection import (
|
||||||
|
RepositoryConnectionCreate,
|
||||||
|
RepositoryConnectionRead,
|
||||||
|
SshKeyResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(tags=["repository-connections"])
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_project_for_user(
|
||||||
|
project_id: UUID, user: User, session: AsyncSession
|
||||||
|
) -> Project:
|
||||||
|
project = await session.get(Project, project_id)
|
||||||
|
if not project or project.owner_id != user.id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Project not found"
|
||||||
|
)
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/projects/{project_id}/repository-connections",
|
||||||
|
response_model=RepositoryConnectionRead,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
async def create_repository_connection(
|
||||||
|
project_id: UUID,
|
||||||
|
conn_in: RepositoryConnectionCreate,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> RepositoryConnection:
|
||||||
|
await _get_project_for_user(project_id, current_user, session)
|
||||||
|
|
||||||
|
repo = await session.get(Repository, conn_in.repository_id)
|
||||||
|
if not repo or repo.project_id != project_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(RepositoryConnection).where(
|
||||||
|
RepositoryConnection.project_id == project_id,
|
||||||
|
RepositoryConnection.repository_id == conn_in.repository_id,
|
||||||
|
RepositoryConnection.provider_kind == conn_in.provider_kind,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing = result.scalar_one_or_none()
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail="Connection already exists for this repository and provider",
|
||||||
|
)
|
||||||
|
|
||||||
|
storage = DatabaseCredentialStorage(session)
|
||||||
|
credential: GitCredential
|
||||||
|
if conn_in.credential_kind == "access_token":
|
||||||
|
credential = AccessTokenCredential(
|
||||||
|
encrypted_payload=conn_in.credential_payload
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Unsupported credential kind: {conn_in.credential_kind}",
|
||||||
|
)
|
||||||
|
|
||||||
|
credential_id = await storage.create(credential)
|
||||||
|
|
||||||
|
connection = RepositoryConnection(
|
||||||
|
project_id=project_id,
|
||||||
|
repository_id=conn_in.repository_id,
|
||||||
|
provider_kind=conn_in.provider_kind,
|
||||||
|
credential_id=credential_id,
|
||||||
|
connection_status=str(ConnectionStatus.pending),
|
||||||
|
)
|
||||||
|
session.add(connection)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(connection)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider = get_provider(ProviderKind(conn_in.provider_kind))
|
||||||
|
provider_status = provider.validate_connection(repo.git_url, str(credential_id))
|
||||||
|
connection.connection_status = str(provider_status)
|
||||||
|
except Exception:
|
||||||
|
connection.connection_status = str(ConnectionStatus.error)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(connection)
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/projects/{project_id}/repository-connections",
|
||||||
|
response_model=list[RepositoryConnectionRead],
|
||||||
|
)
|
||||||
|
async def list_repository_connections(
|
||||||
|
project_id: UUID,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> list[RepositoryConnection]:
|
||||||
|
await _get_project_for_user(project_id, current_user, session)
|
||||||
|
result = await session.execute(
|
||||||
|
select(RepositoryConnection).where(
|
||||||
|
RepositoryConnection.project_id == project_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/projects/{project_id}/repository-connections/{connection_id}",
|
||||||
|
response_model=RepositoryConnectionRead,
|
||||||
|
)
|
||||||
|
async def get_repository_connection(
|
||||||
|
project_id: UUID,
|
||||||
|
connection_id: UUID,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> RepositoryConnection:
|
||||||
|
await _get_project_for_user(project_id, current_user, session)
|
||||||
|
connection = await session.get(RepositoryConnection, connection_id)
|
||||||
|
if not connection or connection.project_id != project_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found"
|
||||||
|
)
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/projects/{project_id}/repository-connections/{connection_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
async def delete_repository_connection(
|
||||||
|
project_id: UUID,
|
||||||
|
connection_id: UUID,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> None:
|
||||||
|
await _get_project_for_user(project_id, current_user, session)
|
||||||
|
connection = await session.get(RepositoryConnection, connection_id)
|
||||||
|
if not connection or connection.project_id != project_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
if connection.credential_id:
|
||||||
|
storage = DatabaseCredentialStorage(session)
|
||||||
|
await storage.delete(connection.credential_id)
|
||||||
|
|
||||||
|
await session.delete(connection)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/projects/{project_id}/repository-connections/{connection_id}/ssh-key",
|
||||||
|
response_model=SshKeyResponse,
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
async def generate_ssh_key(
|
||||||
|
project_id: UUID,
|
||||||
|
connection_id: UUID,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> dict[str, str]:
|
||||||
|
await _get_project_for_user(project_id, current_user, session)
|
||||||
|
connection = await session.get(RepositoryConnection, connection_id)
|
||||||
|
if not connection or connection.project_id != project_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
key_pair = SshKeyLifecycle.generate(connection_id)
|
||||||
|
|
||||||
|
storage = DatabaseCredentialStorage(session)
|
||||||
|
ssh_credential = GitCredential(
|
||||||
|
kind="ssh_key",
|
||||||
|
encrypted_payload=key_pair.encrypted_private_key,
|
||||||
|
)
|
||||||
|
credential_id = await storage.create(ssh_credential)
|
||||||
|
|
||||||
|
connection.credential_id = credential_id
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"connection_id": str(connection_id),
|
||||||
|
"public_key": key_pair.public_key,
|
||||||
|
"credential_id": str(credential_id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/projects/{project_id}/repository-connections/{connection_id}/validate",
|
||||||
|
response_model=RepositoryConnectionRead,
|
||||||
|
)
|
||||||
|
async def validate_connection(
|
||||||
|
project_id: UUID,
|
||||||
|
connection_id: UUID,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> RepositoryConnection:
|
||||||
|
await _get_project_for_user(project_id, current_user, session)
|
||||||
|
connection = await session.get(RepositoryConnection, connection_id)
|
||||||
|
if not connection or connection.project_id != project_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
repo = await session.get(Repository, connection.repository_id)
|
||||||
|
if not repo:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
provider = get_provider(ProviderKind(connection.provider_kind))
|
||||||
|
provider_status = provider.validate_connection(
|
||||||
|
repo.git_url, str(connection.credential_id) if connection.credential_id else ""
|
||||||
|
)
|
||||||
|
connection.connection_status = str(provider_status)
|
||||||
|
except Exception:
|
||||||
|
connection.connection_status = str(ConnectionStatus.error)
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(connection)
|
||||||
|
return connection
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from app.schemas.base import OrmBase
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryConnectionBase(OrmBase):
|
||||||
|
project_id: UUID
|
||||||
|
repository_id: UUID | None = None
|
||||||
|
provider_kind: str = "generic"
|
||||||
|
credential_id: UUID | None = None
|
||||||
|
connection_status: str = "pending"
|
||||||
|
default_branch: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryConnectionCreate(OrmBase):
|
||||||
|
repository_id: UUID
|
||||||
|
provider_kind: str
|
||||||
|
credential_kind: str
|
||||||
|
credential_payload: str
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryConnectionRead(OrmBase):
|
||||||
|
id: UUID
|
||||||
|
project_id: UUID
|
||||||
|
repository_id: UUID | None = None
|
||||||
|
provider_kind: str
|
||||||
|
credential_id: UUID | None = None
|
||||||
|
connection_status: str
|
||||||
|
default_branch: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class SshKeyResponse(OrmBase):
|
||||||
|
connection_id: UUID
|
||||||
|
public_key: str
|
||||||
|
credential_id: UUID
|
||||||
@@ -44,18 +44,17 @@ def temp_repo() -> Any:
|
|||||||
yield repo_path
|
yield repo_path
|
||||||
|
|
||||||
|
|
||||||
def test_clone_raises_not_implemented_error(local_git: LocalGitOperations) -> None:
|
def test_clone_invalid_repo_raises(local_git: LocalGitOperations) -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(RuntimeError, match="Git clone failed"):
|
||||||
local_git.clone("https://example.com/repo.git", Path("/tmp/dest"), "cred-id")
|
local_git.clone("https://example.com/repo.git", Path("/tmp/dest"), "cred-id")
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_raises_not_implemented_error(local_git: LocalGitOperations, temp_repo: Path) -> None:
|
def test_fetch_no_remote_succeeds(local_git: LocalGitOperations, temp_repo: Path) -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
local_git.fetch(temp_repo, "cred-id")
|
||||||
local_git.fetch(temp_repo, "cred-id")
|
|
||||||
|
|
||||||
|
|
||||||
def test_push_raises_not_implemented_error(local_git: LocalGitOperations, temp_repo: Path) -> None:
|
def test_push_no_remote_raises(local_git: LocalGitOperations, temp_repo: Path) -> None:
|
||||||
with pytest.raises(NotImplementedError):
|
with pytest.raises(RuntimeError, match="Git push failed"):
|
||||||
local_git.push(temp_repo, "cred-id")
|
local_git.push(temp_repo, "cred-id")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user