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:
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user