diff --git a/apps/api/src/api/git_repositories.py b/apps/api/src/api/git_repositories.py index abd24d6..05b00f0 100644 --- a/apps/api/src/api/git_repositories.py +++ b/apps/api/src/api/git_repositories.py @@ -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( diff --git a/apps/web/src/components/workspace-create-form.tsx b/apps/web/src/components/workspace-create-form.tsx index 7d62a00..983f90b 100644 --- a/apps/web/src/components/workspace-create-form.tsx +++ b/apps/web/src/components/workspace-create-form.tsx @@ -57,8 +57,12 @@ export function WorkspaceCreateForm({ : git.branches[0]; setSelectedBranch(preferred); setIsNewBranch(false); + } else if (git.error && git.branches.length === 0 && !isNewBranch) { + // API failed — default to manual entry so user can type a branch + setIsNewBranch(true); + setSelectedBranch("__manual__"); } - }, [git.branches, git.defaultBranch, selectedBranch]); + }, [git.branches, git.defaultBranch, git.error, selectedBranch, isNewBranch]); /* ── Load projects (standalone mode only) ── */ const loadProjects = useCallback(async () => { @@ -236,6 +240,20 @@ export function WorkspaceCreateForm({ + + {/* Show a hint when branches couldn’t be loaded */} + {git.error && git.branches.length === 0 && selectedRepo && ( +

+ Couldn’t load branches — type one manually. +

+ )} + - {/* New branch text input */} + {/* Text input for new branch or manual entry */} {isNewBranch && ( Promise; /** Merge source into current (or target) branch. */ - merge: (sourceBranch: string, targetBranch?: string, message?: string) => Promise; + merge: ( + sourceBranch: string, + targetBranch?: string, + message?: string, + ) => Promise; /** Clear the current error. */ clearError: () => void; } @@ -90,7 +94,7 @@ export function useGitRepo( const [error, setError] = useState(null); const withLoading = useCallback( - async (fn: () => Promise): Promise => { + async (fn: () => Promise): Promise => { setLoading(true); setError(null); try { @@ -157,9 +161,7 @@ export function useGitRepo( const commit = useCallback( async (message: string, files?: string[]) => { if (!projectId || !repoId) return; - await withLoading(() => - commitChanges(projectId, repoId, message, files), - ); + await withLoading(() => commitChanges(projectId, repoId, message, files)); await refreshStatus(); }, [projectId, repoId, withLoading, refreshStatus],