fix: handle missing/corrupt repos when fetching branches + clearer manual fallback

Backend (git_repositories.py):
- get_repository_branches now checks for .git subdirectory (not just dir existence)
- If local repo is corrupt/missing but has remote_url, falls back to git ls-remote
  to list branches from the remote
- Returns 404 with actionable message instead of 400 with raw git stderr
- Pre-existing test failure in test_git_repository_clone_preflight.py unchanged

Frontend (workspace-create-form.tsx):
- When branch API fails, auto-switches to manual text input (no dropdown selection needed)
- Shows hint text: 'Couldn't load branches — type one manually'
- useGitRepo hook auto-fetches branches when projectId/repoId change

Quality gates: ruff clean, tsc --noEmit clean, 93 passed + 1 pre-existing failure
This commit is contained in:
2026-06-01 19:00:59 +02:00
parent b02cd978c3
commit ee3c5af7a4
3 changed files with 95 additions and 35 deletions
+68 -23
View File
@@ -882,30 +882,75 @@ async def get_repository_branches(
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 local repo first
is_valid_git_repo = os.path.isdir(os.path.join(repo.path, ".git"))
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))
if is_valid_git_repo:
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_500_INTERNAL_SERVER_ERROR, detail=str(e)) from e
# Local repo missing/corrupt — try remote if available
if repo.remote_url:
try:
result = subprocess.run(
["git", "ls-remote", "--heads", repo.remote_url],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode == 0:
remote_branches = []
default_branch = "main"
for line in result.stdout.strip().split("\n"):
if line:
parts = line.split("\t")
if len(parts) == 2:
ref = parts[1]
if ref.startswith("refs/heads/"):
branch_name = ref[len("refs/heads/"):]
remote_branches.append(branch_name)
if branch_name == "main" or branch_name == "master":
default_branch = branch_name
if remote_branches:
return BranchesResponse(
branches=[
{
"name": b,
"is_default": b == default_branch,
"last_commit": None,
}
for b in remote_branches
],
default_branch=default_branch,
)
except subprocess.TimeoutExpired:
logger.warning("ls-remote timed out for repo %s", repo_id)
except Exception as e:
logger.warning("ls-remote failed for repo %s: %s", repo_id, str(e))
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="repository not found on disk — re-clone or re-create the repository",
)
@router.post(