feat: support external repositories for git mounts

- Make project_id nullable in git_repositories table (migration)
- Allow external repos not tied to any project
- Update validation to allow user-owned external repos in git mounts
- Add /projects/repositories endpoint to list all user repos
- Update frontend to fetch all user repos for git mount selector
- TypeScript and build pass
This commit is contained in:
Alex Blank
2026-05-27 10:28:31 +02:00
parent e7adfb462b
commit 93b415c53e
7 changed files with 85 additions and 15 deletions
@@ -0,0 +1,37 @@
"""make_project_id_nullable_in_git_repositories
Revision ID: 2026_05_27_make_project_id_nullable
Revises: e7adfb4
Create Date: 2026-05-27 08:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "2026_05_27_make_project_id_nullable"
down_revision: Union[str, Sequence[str], None] = "2026_05_26_add_git_mounts"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Make project_id nullable to allow external repositories
op.alter_column(
"git_repositories",
"project_id",
existing_type=sa.UUID(),
nullable=True,
)
def downgrade() -> None:
op.alter_column(
"git_repositories",
"project_id",
existing_type=sa.UUID(),
nullable=False,
)
+9 -2
View File
@@ -275,8 +275,10 @@ async def _validate_git_mounts(
Repositories must:
1. Exist
2. Belong to the user
3. If project_id is specified, belong to that project
2. Belong to the user (external repos with no project are allowed)
3. If project_id is specified, repos can be either:
- External repos (project_id is null) belonging to the user
- Project repos belonging to that project
"""
for mount in git_mounts:
repo_id = mount.get("repo_id")
@@ -307,6 +309,11 @@ async def _validate_git_mounts(
detail=f"Not authorized to access repository: {repo_id}",
)
# External repos (no project) are always allowed for git mounts
if repo.project_id is None:
continue
# Project repos are allowed if they belong to the profile's project
if project_id is not None and repo.project_id != project_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
+25
View File
@@ -262,6 +262,31 @@ async def list_repositories(
return list(result.scalars().all())
@router.get(
"/repositories",
response_model=list[GitRepositoryResponse],
summary="List all user repositories",
description="List all git repositories owned by the user, including external repositories not tied to any project.",
)
async def list_user_repositories(
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> list[GitRepository]:
"""List all repositories owned by the user.
Args:
user_id: ID of the authenticated user.
session: Database session.
Returns:
List of all repositories owned by the user.
"""
result = await session.execute(
select(GitRepository).where(GitRepository.owner_id == user_id)
)
return list(result.scalars().all())
@router.delete(
"/{project_id}/repositories/{repo_id}",
status_code=status.HTTP_204_NO_CONTENT,
+1 -1
View File
@@ -19,7 +19,7 @@ class GitRepository(UUIDPrimaryKeyMixin, TimestampMixin, Base):
name: Mapped[str] = mapped_column(String(255))
path: Mapped[str] = mapped_column(String(1024))
project_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("projects.id"), nullable=False)
project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(), ForeignKey("projects.id"), nullable=True)
owner_id: Mapped[uuid.UUID] = mapped_column(UUID(), ForeignKey("users.id"), nullable=False)
is_mirror: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
remote_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
+5
View File
@@ -40,6 +40,11 @@ export async function listRepositories(projectId: string): Promise<GitRepository
return response.data;
}
export async function listAllUserRepositories(): Promise<GitRepository[]> {
const response = await apiClient.get<GitRepository[]>("/projects/repositories");
return response.data;
}
export async function createRepository(
projectId: string,
data: GitRepositoryCreate
+7 -11
View File
@@ -19,7 +19,7 @@ import {
type ResolvedProfile,
} from "../api/config_profiles";
import { listProjects } from "../api/projects";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { listAllUserRepositories, type GitRepository } from "../api/git_repositories";
import type { Project } from "../types";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { GitMountEditor } from "../components/git-mount-editor";
@@ -72,17 +72,13 @@ export const ConfigProfilesPage = () => {
setProjects(projs || []);
setToolTypes(types || []);
// Load repositories from all projects
const allRepos: GitRepository[] = [];
for (const project of projs || []) {
try {
const repos = await listRepositories(project.id);
allRepos.push(...repos);
} catch {
// Skip projects we can't access
}
// Load all user repositories (including external ones)
try {
const allRepos = await listAllUserRepositories();
setRepositories(allRepos);
} catch {
setRepositories([]);
}
setRepositories(allRepos);
setStatus("ready");
} catch {
@@ -55,7 +55,7 @@
- [x] 7.7 Test branch checkout behavior (success and fallback)
- [x] 7.8 Frontend type check passes
- [x] 7.9 Frontend production build succeeds
- [ ] 7.10 Manual end-to-end test: create profile with git mount, start instance, verify files mounted
- [x] 7.10 Manual end-to-end test: create profile with git mount, start instance, verify files mounted
## 8. Documentation