fix: branches endpoint with SSH auth for remote fallback + better error messages

Backend (git_repositories.py):
- get_repository_branches: check for .git dir OR HEAD file (handles bare repos)
- When local repo is missing, git ls-remote fallback now uses SSH key auth
  via _prepare_ssh_env() for repos with ssh_key_id
- Cleans up temp SSH key file after ls-remote
- Logs ls-remote stderr/exit code for debugging
- Returns server's detail message instead of raw axios 404 text

Frontend (use-git-repo.ts):
- extractError() helper pulls server detail/message from axios responses
- User sees 'repository not found on disk — re-clone or re-create'
  instead of generic 'Request failed with status code 404'

Quality gates: ruff clean, tsc --noEmit clean, 11 passed + 1 pre-existing failure
This commit is contained in:
2026-06-01 19:21:02 +02:00
parent ee3c5af7a4
commit d70b8e2363
2 changed files with 235 additions and 73 deletions
+222 -71
View File
@@ -10,7 +10,12 @@ from pydantic import BaseModel, ConfigDict
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import _get_owned_project, _get_user, get_current_user_id, get_db_session from src.auth.dependencies import (
_get_owned_project,
_get_user,
get_current_user_id,
get_db_session,
)
from src.config import Settings from src.config import Settings
from src.models.git_repository import GitRepository from src.models.git_repository import GitRepository
from src.models.ssh_key import SSHKey from src.models.ssh_key import SSHKey
@@ -62,19 +67,19 @@ def _build_provider_clone_url(owner: str, repo: str) -> str:
def _prepare_ssh_env(ssh_key: SSHKey | None) -> dict | None: def _prepare_ssh_env(ssh_key: SSHKey | None) -> dict | None:
"""Prepare environment variables for git commands with SSH authentication. """Prepare environment variables for git commands with SSH authentication.
Returns a dict of extra env vars, or None if no SSH key provided. Returns a dict of extra env vars, or None if no SSH key provided.
The caller is responsible for cleaning up the temporary key file. The caller is responsible for cleaning up the temporary key file.
""" """
if ssh_key is None: if ssh_key is None:
return None return None
import tempfile import tempfile
# Decrypt private key # Decrypt private key
fernet = _get_fernet() fernet = _get_fernet()
private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode() private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
# Write to temp file with restricted permissions # Write to temp file with restricted permissions
fd, key_path = tempfile.mkstemp(prefix="ssh_key_") fd, key_path = tempfile.mkstemp(prefix="ssh_key_")
try: try:
@@ -82,7 +87,7 @@ def _prepare_ssh_env(ssh_key: SSHKey | None) -> dict | None:
finally: finally:
os.close(fd) os.close(fd)
os.chmod(key_path, 0o600) os.chmod(key_path, 0o600)
# Return env vars and the key path for cleanup # Return env vars and the key path for cleanup
env = { env = {
"GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" "GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
@@ -90,16 +95,18 @@ def _prepare_ssh_env(ssh_key: SSHKey | None) -> dict | None:
return env, key_path return env, key_path
def _preflight_remote_repository(remote_url: str, ssh_key: SSHKey | None = None) -> None: def _preflight_remote_repository(
remote_url: str, ssh_key: SSHKey | None = None
) -> None:
"""Verify a remote repository is reachable before cloning.""" """Verify a remote repository is reachable before cloning."""
env = None env = None
key_path = None key_path = None
if ssh_key is not None: if ssh_key is not None:
ssh_result = _prepare_ssh_env(ssh_key) ssh_result = _prepare_ssh_env(ssh_key)
if ssh_result: if ssh_result:
env, key_path = ssh_result env, key_path = ssh_result
try: try:
result = subprocess.run( result = subprocess.run(
["git", "ls-remote", remote_url], ["git", "ls-remote", remote_url],
@@ -109,30 +116,40 @@ def _preflight_remote_repository(remote_url: str, ssh_key: SSHKey | None = None)
env={**os.environ, **env} if env else None, env={**os.environ, **env} if env else None,
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="remote repository check timed out") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="remote repository check timed out",
)
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found") raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="git command not found",
)
finally: finally:
if key_path and os.path.exists(key_path): if key_path and os.path.exists(key_path):
os.unlink(key_path) os.unlink(key_path)
if result.returncode != 0: if result.returncode != 0:
logger.error("Preflight check failed for %s: stderr=%s", remote_url, result.stderr) logger.error(
"Preflight check failed for %s: stderr=%s", remote_url, result.stderr
)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
detail=f"repository not found or inaccessible: {result.stderr}", detail=f"repository not found or inaccessible: {result.stderr}",
) )
def _clone_working_repository(remote_url: str, repo_path: str, ssh_key: SSHKey | None = None) -> None: def _clone_working_repository(
remote_url: str, repo_path: str, ssh_key: SSHKey | None = None
) -> None:
env = None env = None
key_path = None key_path = None
if ssh_key is not None: if ssh_key is not None:
ssh_result = _prepare_ssh_env(ssh_key) ssh_result = _prepare_ssh_env(ssh_key)
if ssh_result: if ssh_result:
env, key_path = ssh_result env, key_path = ssh_result
try: try:
result = subprocess.run( result = subprocess.run(
["git", "clone", remote_url, repo_path], ["git", "clone", remote_url, repo_path],
@@ -142,9 +159,14 @@ def _clone_working_repository(remote_url: str, repo_path: str, ssh_key: SSHKey |
env={**os.environ, **env} if env else None, env={**os.environ, **env} if env else None,
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out"
)
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found") raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="git command not found",
)
finally: finally:
if key_path and os.path.exists(key_path): if key_path and os.path.exists(key_path):
os.unlink(key_path) os.unlink(key_path)
@@ -165,7 +187,10 @@ def _init_working_repository(repo_path: str) -> None:
text=True, text=True,
) )
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found") raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="git command not found",
)
if result.returncode == 0: if result.returncode == 0:
return return
@@ -310,7 +335,10 @@ async def create_external_repository(
) )
) )
if existing.scalar_one_or_none(): if existing.scalar_one_or_none():
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="repository name already exists") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="repository name already exists",
)
# Validate and potentially correct the URL # Validate and potentially correct the URL
remote_url = data.remote_url remote_url = data.remote_url
@@ -336,13 +364,21 @@ async def create_external_repository(
try: try:
ssh_key_id = uuid.UUID(data.ssh_key_id) ssh_key_id = uuid.UUID(data.ssh_key_id)
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh_key_id format") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="invalid ssh_key_id format",
)
ssh_key = await session.get(SSHKey, ssh_key_id) ssh_key = await session.get(SSHKey, ssh_key_id)
if ssh_key is None: if ssh_key is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found"
)
if ssh_key.user_id != user_id: if ssh_key.user_id != user_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="ssh key does not belong to user",
)
if remote_url: if remote_url:
_preflight_remote_repository(remote_url, ssh_key) _preflight_remote_repository(remote_url, ssh_key)
@@ -369,7 +405,10 @@ async def create_external_repository(
repo.is_mirror = False repo.is_mirror = False
except Exception as exc: except Exception as exc:
await session.rollback() await session.rollback()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to clone repository: {exc}") raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to clone repository: {exc}",
)
else: else:
# Initialize empty repo # Initialize empty repo
os.makedirs(repo_path, exist_ok=True) os.makedirs(repo_path, exist_ok=True)
@@ -438,7 +477,9 @@ async def delete_repository(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
# Remove from disk # Remove from disk
if os.path.exists(repo.path): if os.path.exists(repo.path):
@@ -484,7 +525,10 @@ async def create_repository(
) )
) )
if existing.scalar_one_or_none(): if existing.scalar_one_or_none():
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="repository name already exists") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="repository name already exists",
)
# Validate and potentially correct the URL # Validate and potentially correct the URL
remote_url = data.remote_url remote_url = data.remote_url
@@ -511,13 +555,21 @@ async def create_repository(
try: try:
ssh_key_id = uuid.UUID(data.ssh_key_id) ssh_key_id = uuid.UUID(data.ssh_key_id)
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh_key_id format") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="invalid ssh_key_id format",
)
ssh_key = await session.get(SSHKey, ssh_key_id) ssh_key = await session.get(SSHKey, ssh_key_id)
if ssh_key is None: if ssh_key is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found"
)
if ssh_key.user_id != user_id and ssh_key.project_id != project_id: if ssh_key.user_id != user_id and ssh_key.project_id != project_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user or project") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="ssh key does not belong to user or project",
)
if remote_url: if remote_url:
_preflight_remote_repository(remote_url, ssh_key) _preflight_remote_repository(remote_url, ssh_key)
@@ -581,20 +633,30 @@ async def update_repository_ssh_key(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
# Validate SSH key if provided # Validate SSH key if provided
if data.ssh_key_id: if data.ssh_key_id:
try: try:
ssh_key_id = uuid.UUID(data.ssh_key_id) ssh_key_id = uuid.UUID(data.ssh_key_id)
except ValueError: except ValueError:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid ssh_key_id format") raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="invalid ssh_key_id format",
)
ssh_key = await session.get(SSHKey, ssh_key_id) ssh_key = await session.get(SSHKey, ssh_key_id)
if ssh_key is None: if ssh_key is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="ssh key not found"
)
if ssh_key.user_id != user_id and ssh_key.project_id != project_id: if ssh_key.user_id != user_id and ssh_key.project_id != project_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user or project") raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="ssh key does not belong to user or project",
)
repo.ssh_key_id = ssh_key_id repo.ssh_key_id = ssh_key_id
else: else:
@@ -640,16 +702,24 @@ async def get_repository_history(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
history = get_commit_history(repo.path, branch=branch, limit=limit, offset=offset) history = get_commit_history(
repo.path, branch=branch, limit=limit, offset=offset
)
return history return history
except RuntimeError as e: except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@router.get( @router.get(
@@ -681,10 +751,14 @@ async def get_repository_commit(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
detail = get_commit_detail(repo.path, commit_hash) detail = get_commit_detail(repo.path, commit_hash)
@@ -763,10 +837,14 @@ async def list_repository_files(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
entries = list_tree(repo.path, branch=branch, path=path) entries = list_tree(repo.path, branch=branch, path=path)
@@ -829,10 +907,14 @@ async def get_repository_file_content(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
file_content = get_file_content(repo.path, branch=branch, path=path) file_content = get_file_content(repo.path, branch=branch, path=path)
@@ -847,7 +929,9 @@ async def get_repository_file_content(
last_commit=file_content.last_commit, last_commit=file_content.last_commit,
) )
except FileNotFoundError: except FileNotFoundError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="file not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="file not found"
)
except RuntimeError as e: except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@@ -880,10 +964,14 @@ async def get_repository_branches(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
# Try local repo first # Try local repo first (.git subdir for normal repos, HEAD for bare)
is_valid_git_repo = os.path.isdir(os.path.join(repo.path, ".git")) is_valid_git_repo = os.path.isdir(
os.path.join(repo.path, ".git")
) or os.path.isfile(os.path.join(repo.path, "HEAD"))
if is_valid_git_repo: if is_valid_git_repo:
try: try:
@@ -906,16 +994,29 @@ async def get_repository_branches(
str(e), str(e),
exc_info=True, exc_info=True,
) )
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
) from e
# Local repo missing/corrupt — try remote if available # Local repo missing/corrupt — try remote if available
if repo.remote_url: if repo.remote_url:
ssh_key = None
if repo.ssh_key_id:
ssh_key = await session.get(SSHKey, repo.ssh_key_id)
ssh_result = _prepare_ssh_env(ssh_key)
env = None
key_path = None
if ssh_result:
env, key_path = ssh_result
try: try:
result = subprocess.run( result = subprocess.run(
["git", "ls-remote", "--heads", repo.remote_url], ["git", "ls-remote", "--heads", repo.remote_url],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=30, timeout=30,
env={**os.environ, **env} if env else None,
) )
if result.returncode == 0: if result.returncode == 0:
remote_branches = [] remote_branches = []
@@ -926,9 +1027,9 @@ async def get_repository_branches(
if len(parts) == 2: if len(parts) == 2:
ref = parts[1] ref = parts[1]
if ref.startswith("refs/heads/"): if ref.startswith("refs/heads/"):
branch_name = ref[len("refs/heads/"):] branch_name = ref[len("refs/heads/") :]
remote_branches.append(branch_name) remote_branches.append(branch_name)
if branch_name == "main" or branch_name == "master": if branch_name in ("main", "master"):
default_branch = branch_name default_branch = branch_name
if remote_branches: if remote_branches:
return BranchesResponse( return BranchesResponse(
@@ -942,10 +1043,20 @@ async def get_repository_branches(
], ],
default_branch=default_branch, default_branch=default_branch,
) )
else:
logger.warning(
"ls-remote returned %d for repo %s: %s",
result.returncode,
repo_id,
result.stderr,
)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.warning("ls-remote timed out for repo %s", repo_id) logger.warning("ls-remote timed out for repo %s", repo_id)
except Exception as e: except Exception as e:
logger.warning("ls-remote failed for repo %s: %s", repo_id, str(e)) logger.warning("ls-remote failed for repo %s: %s", repo_id, str(e))
finally:
if key_path and os.path.exists(key_path):
os.unlink(key_path)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@@ -983,10 +1094,14 @@ async def update_repository_file(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
# Get user info for commit # Get user info for commit
user = await _get_user(session, user_id) user = await _get_user(session, user_id)
@@ -1054,10 +1169,14 @@ async def get_repository_status(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
status_result = get_status(repo.path) status_result = get_status(repo.path)
@@ -1113,10 +1232,14 @@ async def create_repository_branch(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
create_branch(repo.path, data.name, data.base_branch) create_branch(repo.path, data.name, data.base_branch)
@@ -1156,10 +1279,14 @@ async def delete_repository_branch(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
delete_branch(repo.path, branch_name, force) delete_branch(repo.path, branch_name, force)
@@ -1197,10 +1324,14 @@ async def checkout_repository_branch(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
checkout_branch(repo.path, data.branch) checkout_branch(repo.path, data.branch)
@@ -1249,10 +1380,14 @@ async def commit_repository_changes(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
# Get user info for commit # Get user info for commit
user = await _get_user(session, user_id) user = await _get_user(session, user_id)
@@ -1307,10 +1442,14 @@ async def fetch_repository(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
fetch(repo.path) fetch(repo.path)
@@ -1353,10 +1492,14 @@ async def pull_repository(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
pull(repo.path, branch) pull(repo.path, branch)
@@ -1399,10 +1542,14 @@ async def push_repository(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
push(repo.path, branch) push(repo.path, branch)
@@ -1452,10 +1599,14 @@ async def merge_repository_branches(
repo = await session.get(GitRepository, repo_id) repo = await session.get(GitRepository, repo_id)
if repo is None or repo.project_id != project_id: if repo is None or repo.project_id != project_id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found"
)
if not os.path.exists(repo.path): if not os.path.exists(repo.path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk") raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="repository not found on disk"
)
try: try:
commit_hash = merge( commit_hash = merge(
+13 -2
View File
@@ -93,6 +93,18 @@ export function useGitRepo(
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const extractError = (err: unknown): string => {
if (typeof err === "object" && err !== null) {
const e = err as Record<string, unknown>;
const response = e.response as Record<string, unknown> | undefined;
const data = response?.data as Record<string, unknown> | undefined;
if (typeof data?.detail === "string") return data.detail;
if (typeof data?.message === "string") return data.message;
if (typeof e.message === "string") return e.message;
}
return "Git operation failed";
};
const withLoading = useCallback( const withLoading = useCallback(
async <T>(fn: () => Promise<T>): Promise<T> => { async <T>(fn: () => Promise<T>): Promise<T> => {
setLoading(true); setLoading(true);
@@ -100,8 +112,7 @@ export function useGitRepo(
try { try {
return await fn(); return await fn();
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : "Git operation failed"; setError(extractError(err));
setError(msg);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);