diff --git a/apps/api/src/api/config_profiles.py b/apps/api/src/api/config_profiles.py index 47ee83a..52279f3 100644 --- a/apps/api/src/api/config_profiles.py +++ b/apps/api/src/api/config_profiles.py @@ -58,18 +58,16 @@ def _calculate_profile_size(data: dict) -> int: class GitMountItem(BaseModel): - repo_id: str = Field(description="UUID of the git repository") + remote_url: str = Field(description="Git remote URL (HTTPS or SSH)") source_path: str = Field(default=".", description="Path within repository (supports glob patterns)") target_path: str = Field(description="Absolute path inside container") branch: str | None = Field(default=None, description="Optional branch or tag name") - @field_validator("repo_id") + @field_validator("remote_url") @classmethod - def validate_repo_id(cls, v: str) -> str: - try: - uuid.UUID(v) - except ValueError: - raise ValueError(f"Invalid repo_id UUID: {v}") + def validate_remote_url(cls, v: str) -> str: + if not v.startswith(("http://", "https://", "git@", "ssh://")): + raise ValueError("remote_url must be a valid git URL (https://, git@, or ssh://)") return v @field_validator("source_path") @@ -271,53 +269,23 @@ async def _validate_git_mounts( git_mounts: list[dict], project_id: uuid.UUID | None = None, ) -> None: - """Validate that all referenced git repositories exist and are accessible. + """Validate git mount URLs. - Repositories must: - 1. Exist - 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 + Simply checks that remote_url looks like a valid git URL. + Actual clone validation happens at instance startup time. """ for mount in git_mounts: - repo_id = mount.get("repo_id") - if not repo_id: + remote_url = mount.get("remote_url") + if not remote_url: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Git mount missing repo_id", + detail="Git mount missing remote_url", ) - try: - repo_uuid = uuid.UUID(repo_id) - except ValueError: + if not remote_url.startswith(("http://", "https://", "git@", "ssh://")): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid repo_id UUID: {repo_id}", - ) - - repo = await session.get(GitRepository, repo_uuid) - if repo is None: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Git repository not found: {repo_id}", - ) - - if repo.owner_id != user_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - 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, - detail=f"Repository {repo_id} does not belong to project {project_id}", + detail=f"Invalid git URL: {remote_url}", ) diff --git a/apps/api/src/api/git_repositories.py b/apps/api/src/api/git_repositories.py index 535e4ca..e4dc6b6 100644 --- a/apps/api/src/api/git_repositories.py +++ b/apps/api/src/api/git_repositories.py @@ -232,36 +232,6 @@ class GitRepositoryResponse(BaseModel): updated_at: datetime -@router.get( - "/{project_id}/repositories", - response_model=list[GitRepositoryResponse], - summary="List repositories", - description="List all git repositories in a project.", -) -async def list_repositories( - project_id: uuid.UUID, - user_id: uuid.UUID = Depends(get_current_user_id), - session: AsyncSession = Depends(get_db_session), -) -> list[GitRepository]: - """List all repositories in a project. - - Args: - project_id: UUID of the project. - user_id: ID of the authenticated user. - session: Database session. - - Returns: - List of repositories in the project. - """ - _user = await _get_user(session, user_id) - _project = await _get_owned_project(project_id, user_id, session) - - result = await session.execute( - select(GitRepository).where(GitRepository.project_id == project_id) - ) - return list(result.scalars().all()) - - @router.get( "/repositories", response_model=list[GitRepositoryResponse], @@ -287,45 +257,6 @@ async def list_user_repositories( return list(result.scalars().all()) -@router.delete( - "/{project_id}/repositories/{repo_id}", - status_code=status.HTTP_204_NO_CONTENT, - summary="Delete a repository", - description="Delete a git repository from the project and remove it from disk.", -) -async def delete_repository( - project_id: uuid.UUID, - repo_id: uuid.UUID, - user_id: uuid.UUID = Depends(get_current_user_id), - session: AsyncSession = Depends(get_db_session), -) -> Response: - """Delete a repository. - - Args: - project_id: UUID of the project. - repo_id: UUID of the repository to delete. - user_id: ID of the authenticated user. - session: Database session. - - Returns: - Empty response with 204 status code. - """ - _user = await _get_user(session, user_id) - _project = await _get_owned_project(project_id, user_id, session) - - repo = await session.get(GitRepository, repo_id) - if repo is None or repo.project_id != project_id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") - - # Remove from disk - if os.path.exists(repo.path): - shutil.rmtree(repo.path) - - await session.delete(repo) - await session.commit() - return Response(status_code=status.HTTP_204_NO_CONTENT) - - @router.post( "/repositories/parse-url", response_model=URLParseResponse, @@ -451,6 +382,75 @@ async def create_external_repository( return repo +@router.get( + "/{project_id}/repositories", + response_model=list[GitRepositoryResponse], + summary="List repositories", + description="List all git repositories in a project.", +) +async def list_repositories( + project_id: uuid.UUID, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> list[GitRepository]: + """List all repositories in a project. + + Args: + project_id: UUID of the project. + user_id: ID of the authenticated user. + session: Database session. + + Returns: + List of repositories in the project. + """ + _user = await _get_user(session, user_id) + _project = await _get_owned_project(project_id, user_id, session) + + result = await session.execute( + select(GitRepository).where(GitRepository.project_id == project_id) + ) + return list(result.scalars().all()) + + +@router.delete( + "/{project_id}/repositories/{repo_id}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Delete a repository", + description="Delete a git repository from the project and remove it from disk.", +) +async def delete_repository( + project_id: uuid.UUID, + repo_id: uuid.UUID, + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> Response: + """Delete a repository. + + Args: + project_id: UUID of the project. + repo_id: UUID of the repository to delete. + user_id: ID of the authenticated user. + session: Database session. + + Returns: + Empty response with 204 status code. + """ + _user = await _get_user(session, user_id) + _project = await _get_owned_project(project_id, user_id, session) + + repo = await session.get(GitRepository, repo_id) + if repo is None or repo.project_id != project_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="repository not found") + + # Remove from disk + if os.path.exists(repo.path): + shutil.rmtree(repo.path) + + await session.delete(repo) + await session.commit() + return Response(status_code=status.HTTP_204_NO_CONTENT) + + @router.post( "/{project_id}/repositories", response_model=GitRepositoryResponse, diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index c24fbc1..02a3c07 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -96,78 +96,58 @@ async def _resolve_single_git_mount( ) -> list[dict]: """Resolve a single git mount to volume mount entries. + Clones directly from remote_url, no database lookup needed. Returns a list of volume mounts (one for each matched file/directory). """ - repo_id = git_mount.get("repo_id") + remote_url = git_mount.get("remote_url") source_path = git_mount.get("source_path", ".") target_path = git_mount.get("target_path") branch = git_mount.get("branch") - if not repo_id or not target_path: - logger.warning("Invalid git mount skipped: missing repo_id or target_path") - return [] - - try: - repo_uuid = uuid.UUID(repo_id) - except ValueError: - logger.warning("Invalid git mount skipped: repo_id is not a valid UUID: %s", repo_id) - return [] - - repo = await session.get(GitRepository, repo_uuid) - if repo is None: - logger.warning("Git mount skipped: repository %s not found", repo_id) + if not remote_url or not target_path: + logger.warning("Invalid git mount skipped: missing remote_url or target_path") return [] - # Determine repo path - use existing or auto-clone - repo_path = repo.path + if not instance_dir: + logger.warning("Git mount skipped: no instance_dir provided for cloning") + return [] - if not repo_path or not os.path.exists(repo_path): - # Auto-clone if remote URL is available and instance_dir is provided - if repo.remote_url and instance_dir: - try: - ssh_key_path = None - if repo.ssh_key_id: - from src.models.ssh_key import SSHKey - ssh_key = await session.get(SSHKey, repo.ssh_key_id) - if ssh_key: - ssh_dir = prepare_ssh_key_files(instance_dir, ssh_key) - ssh_key_path = str(Path(ssh_dir) / "id_ed25519") - - repo_path = await asyncio.to_thread( - clone_repository, - repo.remote_url, - ssh_key_path, - instance_dir, - branch or "main", - ) - logger.info("Auto-cloned repository %s to %s", repo.name, repo_path) - except Exception as exc: - logger.warning("Auto-clone failed for repository %s: %s", repo.name, exc) - return [] - else: - logger.warning( - "Git mount skipped: repository %s path not found at %s and no remote_url available", - repo_id, repo_path + # Generate a unique directory name from the URL + import hashlib + url_hash = hashlib.md5(remote_url.encode()).hexdigest()[:12] + repo_name = remote_url.split("/")[-1].replace(".git", "") or "repo" + clone_dir = os.path.join(instance_dir, "git-mounts", f"{repo_name}-{url_hash}") + + # Clone or pull the repository + repo_path = clone_dir + if not os.path.exists(clone_dir): + try: + repo_path = await asyncio.to_thread( + clone_repository, + remote_url, + None, # No SSH key for now - can be added later + os.path.dirname(clone_dir), + branch or "main", ) + logger.info("Cloned git mount repository %s to %s", remote_url, repo_path) + except Exception as exc: + logger.warning("Clone failed for git mount %s: %s", remote_url, exc) return [] else: # Repo exists - pull latest updates - if repo.remote_url: - try: - await asyncio.to_thread(_pull_repository_updates, repo_path, repo.remote_url) - logger.info("Pulled updates for repository %s", repo.name) - except Exception as exc: - logger.warning("Failed to pull updates for repository %s: %s", repo.name, exc) - # Continue with existing code as fallback + try: + await asyncio.to_thread(_pull_repository_updates, repo_path, remote_url) + logger.info("Pulled updates for git mount %s", remote_url) + except Exception as exc: + logger.warning("Failed to pull updates for %s: %s", remote_url, exc) # Handle branch checkout if specified if branch and repo_path: try: await asyncio.to_thread(_checkout_branch, repo_path, branch) - logger.info("Checked out branch %s for repository %s", branch, repo.name) + logger.info("Checked out branch %s for %s", branch, remote_url) except Exception as exc: - logger.warning("Branch checkout failed for %s@%s: %s", repo.name, branch, exc) - # Continue with current branch as fallback + logger.warning("Branch checkout failed for %s@%s: %s", remote_url, branch, exc) # Build source path and expand globs if source_path and source_path != ".": @@ -179,7 +159,7 @@ async def _resolve_single_git_mount( matched_paths = _expand_glob_source(source_full, repo_path) if not matched_paths: - logger.warning("Git mount skipped: no files matched source path %s in repository %s", source_path, repo_id) + logger.warning("Git mount skipped: no files matched source path %s in %s", source_path, remote_url) return [] volume_mounts = [] @@ -201,7 +181,7 @@ async def _resolve_single_git_mount( "target": final_target, "type": "bind", }) - logger.info("Added git mount: %s -> %s (repo: %s)", matched_path, final_target, repo.name) + logger.info("Added git mount: %s -> %s (url: %s)", matched_path, final_target, remote_url) return volume_mounts diff --git a/apps/api/src/services/config_profile_resolver.py b/apps/api/src/services/config_profile_resolver.py index afcc525..0c77224 100644 --- a/apps/api/src/services/config_profile_resolver.py +++ b/apps/api/src/services/config_profile_resolver.py @@ -176,13 +176,13 @@ def _merge_git_mounts( ) -> list[dict[str, Any]]: """Merge git mounts from included profiles. - Later mounts override earlier ones with the same repo_id + target_path combo. + Later mounts override earlier ones with the same remote_url + target_path combo. """ result = list(base) - # Build lookup by (repo_id, target_path) - seen = {(m["repo_id"], m["target_path"]): i for i, m in enumerate(result)} + # Build lookup by (remote_url, target_path) + seen = {(m["remote_url"], m["target_path"]): i for i, m in enumerate(result)} for mount in overlay: - key = (mount["repo_id"], mount["target_path"]) + key = (mount["remote_url"], mount["target_path"]) if key in seen: result[seen[key]] = dict(mount) else: diff --git a/apps/api/tests/integration/test_config_profiles_api.py b/apps/api/tests/integration/test_config_profiles_api.py index 200958f..1ecdb37 100644 --- a/apps/api/tests/integration/test_config_profiles_api.py +++ b/apps/api/tests/integration/test_config_profiles_api.py @@ -333,7 +333,7 @@ class TestConfigProfilesAPI: "files": {}, "git_mounts": [ { - "repo_id": repo_id, + "remote_url": "https://github.com/user/repo.git", "source_path": ".", "target_path": "/app", "branch": "main", @@ -369,7 +369,7 @@ class TestConfigProfilesAPI: json={ "git_mounts": [ { - "repo_id": repo_id, + "remote_url": "https://github.com/user/repo.git", "source_path": "config", "target_path": "/config", } @@ -393,7 +393,7 @@ class TestConfigProfilesAPI: "files": {}, "git_mounts": [ { - "repo_id": repo_id, + "remote_url": "https://github.com/user/repo.git", "source_path": "/absolute/path", "target_path": "/app", } @@ -414,7 +414,7 @@ class TestConfigProfilesAPI: "files": {}, "git_mounts": [ { - "repo_id": repo_id, + "remote_url": "https://github.com/user/repo.git", "source_path": ".", "target_path": "relative/path", } @@ -436,7 +436,7 @@ class TestConfigProfilesAPI: "files": {}, "git_mounts": [ { - "repo_id": repo_id, + "remote_url": "https://github.com/user/repo.git", "source_path": ".", "target_path": "/app", } @@ -450,4 +450,4 @@ class TestConfigProfilesAPI: assert response.status_code == 200 data = response.json() assert len(data["git_mounts"]) == 1 - assert data["git_mounts"][0]["repo_id"] == repo_id + assert data["git_mounts"][0]["remote_url"] == "https://github.com/user/repo.git" diff --git a/apps/api/tests/unit/test_config_profile_resolver.py b/apps/api/tests/unit/test_config_profile_resolver.py index 7177c02..2ffa175 100644 --- a/apps/api/tests/unit/test_config_profile_resolver.py +++ b/apps/api/tests/unit/test_config_profile_resolver.py @@ -102,18 +102,18 @@ class TestMergeFunctions: """Test basic git mount merging.""" result = _merge_git_mounts( [], - [{"repo_id": "repo1", "source_path": ".", "target_path": "/app"}], + [{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app"}], "source", ) assert len(result) == 1 - assert result[0]["repo_id"] == "repo1" + assert result[0]["remote_url"] == "https://github.com/user/repo1.git" assert result[0]["target_path"] == "/app" def test_merge_git_mounts_override_same_repo_target(self) -> None: """Test that git mounts with same repo+target override.""" result = _merge_git_mounts( - [{"repo_id": "repo1", "source_path": ".", "target_path": "/app", "branch": "main"}], - [{"repo_id": "repo1", "source_path": "src", "target_path": "/app", "branch": "dev"}], + [{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app", "branch": "main"}], + [{"remote_url": "https://github.com/user/repo1.git", "source_path": "src", "target_path": "/app", "branch": "dev"}], "source", ) assert len(result) == 1 @@ -123,8 +123,8 @@ class TestMergeFunctions: def test_merge_git_mounts_different_targets(self) -> None: """Test that git mounts with different targets are preserved.""" result = _merge_git_mounts( - [{"repo_id": "repo1", "source_path": ".", "target_path": "/app"}], - [{"repo_id": "repo2", "source_path": ".", "target_path": "/config"}], + [{"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app"}], + [{"remote_url": "https://github.com/user/repo2.git", "source_path": ".", "target_path": "/config"}], "source", ) assert len(result) == 2 @@ -296,7 +296,7 @@ class TestResolveProfile: env_vars={}, files={}, git_mounts=[ - {"repo_id": "repo1", "source_path": ".", "target_path": "/app"}, + {"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app"}, ], ) db_session.add(profile) @@ -304,7 +304,7 @@ class TestResolveProfile: result = await resolve_profile(db_session, profile.id) assert len(result.git_mounts) == 1 - assert result.git_mounts[0]["repo_id"] == "repo1" + assert result.git_mounts[0]["remote_url"] == "https://github.com/user/repo1.git" assert result.git_mounts[0]["target_path"] == "/app" @pytest.mark.asyncio @@ -320,7 +320,7 @@ class TestResolveProfile: env_vars={}, files={}, git_mounts=[ - {"repo_id": "repo1", "source_path": ".", "target_path": "/app"}, + {"remote_url": "https://github.com/user/repo1.git", "source_path": ".", "target_path": "/app"}, ], ) db_session.add(base) @@ -333,7 +333,7 @@ class TestResolveProfile: env_vars={}, files={}, git_mounts=[ - {"repo_id": "repo2", "source_path": "config", "target_path": "/config"}, + {"remote_url": "https://github.com/user/repo2.git", "source_path": "config", "target_path": "/config"}, ], ) db_session.add(child) diff --git a/apps/web/src/api/config_profiles.ts b/apps/web/src/api/config_profiles.ts index 219c258..ec45350 100644 --- a/apps/web/src/api/config_profiles.ts +++ b/apps/web/src/api/config_profiles.ts @@ -25,7 +25,7 @@ export interface ConfigProfileMount { } export interface GitMount { - repo_id: string; + remote_url: string; source_path: string; target_path: string; branch?: string; diff --git a/apps/web/src/components/git-mount-editor.tsx b/apps/web/src/components/git-mount-editor.tsx index d5dc8c8..e9c52ec 100644 --- a/apps/web/src/components/git-mount-editor.tsx +++ b/apps/web/src/components/git-mount-editor.tsx @@ -1,28 +1,25 @@ import { useState } from "react"; import { Icon } from "./icon"; import type { GitMount } from "../api/config_profiles"; -import type { GitRepository } from "../api/git_repositories"; interface GitMountEditorProps { mounts: GitMount[]; - repositories: GitRepository[]; onChange: (mounts: GitMount[]) => void; - onCreateRepository?: (name: string, remoteUrl: string) => Promise; } -export const GitMountEditor = ({ mounts, repositories, onChange, onCreateRepository }: GitMountEditorProps) => { +export const GitMountEditor = ({ mounts, onChange }: GitMountEditorProps) => { const [editingIndex, setEditingIndex] = useState(null); const [newMount, setNewMount] = useState({ - repo_id: "", + remote_url: "", source_path: ".", target_path: "", branch: "", }); const handleAdd = () => { - if (!newMount.repo_id || !newMount.target_path) return; + if (!newMount.remote_url || !newMount.target_path) return; onChange([...mounts, { ...newMount }]); - setNewMount({ repo_id: "", source_path: ".", target_path: "", branch: "" }); + setNewMount({ remote_url: "", source_path: ".", target_path: "", branch: "" }); }; const handleUpdate = (index: number, updated: GitMount) => { @@ -44,6 +41,14 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit return null; }; + const validateUrl = (url: string): string | null => { + if (!url) return "Git URL is required"; + if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("git@") && !url.startsWith("ssh://")) { + return "Must be a valid git URL (https://, git@, or ssh://)"; + } + return null; + }; + return (

Git Mounts

@@ -55,18 +60,15 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit {editingIndex === index ? ( handleUpdate(index, updated)} onCancel={() => setEditingIndex(null)} validatePath={validatePath} - onCreateRepository={onCreateRepository} + validateUrl={validateUrl} /> ) : (
- - {repositories.find((r) => r.id === mount.repo_id)?.name || mount.repo_id} - + {mount.remote_url} {mount.source_path || "."} → {mount.target_path} @@ -103,11 +105,10 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit
Add Git Mount
setNewMount({ repo_id: "", source_path: ".", target_path: "", branch: "" })} + onCancel={() => setNewMount({ remote_url: "", source_path: ".", target_path: "", branch: "" })} validatePath={validatePath} - onCreateRepository={onCreateRepository} + validateUrl={validateUrl} isNew />
@@ -117,21 +118,16 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit interface GitMountFormProps { mount: GitMount; - repositories: GitRepository[]; onSave: (mount: GitMount) => void; onCancel: () => void; validatePath: (path: string, isTarget: boolean) => string | null; - onCreateRepository?: (name: string, remoteUrl: string) => Promise; + validateUrl: (url: string) => string | null; isNew?: boolean; } -const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onCreateRepository, isNew }: GitMountFormProps) => { +const GitMountForm = ({ mount, onSave, onCancel, validatePath, validateUrl, isNew }: GitMountFormProps) => { const [form, setForm] = useState({ ...mount }); const [errors, setErrors] = useState>({}); - const [isCreatingRepo, setIsCreatingRepo] = useState(false); - const [newRepoName, setNewRepoName] = useState(""); - const [newRepoUrl, setNewRepoUrl] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); const handleChange = (field: keyof GitMount, value: string) => { setForm((prev) => ({ ...prev, [field]: value })); @@ -144,32 +140,11 @@ const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onC } }; - const handleCreateRepo = async () => { - if (!onCreateRepository || !newRepoName.trim() || !newRepoUrl.trim()) return; - - setIsSubmitting(true); - try { - const repo = await onCreateRepository(newRepoName.trim(), newRepoUrl.trim()); - handleChange("repo_id", repo.id); - setIsCreatingRepo(false); - setNewRepoName(""); - setNewRepoUrl(""); - } catch (err) { - setErrors((prev) => ({ - ...prev, - repo_id: err instanceof Error ? err.message : "Failed to create repository", - })); - } finally { - setIsSubmitting(false); - } - }; - const handleSubmit = () => { const newErrors: Record = {}; - if (!form.repo_id) { - newErrors.repo_id = "Repository is required"; - } + const urlError = validateUrl(form.remote_url); + if (urlError) newErrors.remote_url = urlError; const sourceError = validatePath(form.source_path || ".", false); if (sourceError) newErrors.source_path = sourceError; @@ -184,79 +159,23 @@ const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onC onSave(form); if (isNew) { - setForm({ repo_id: "", source_path: ".", target_path: "", branch: "" }); + setForm({ remote_url: "", source_path: ".", target_path: "", branch: "" }); } }; return (
- - {!isCreatingRepo ? ( - <> - - {errors.repo_id && {errors.repo_id}} - - ) : ( -
- setNewRepoName(e.target.value)} - placeholder="Repository name" - disabled={isSubmitting} - /> - setNewRepoUrl(e.target.value)} - placeholder="https://github.com/user/repo.git" - disabled={isSubmitting} - /> -
- - -
-
- )} + + handleChange("remote_url", e.target.value)} + placeholder="https://github.com/user/repo.git" + className={errors.remote_url ? "error" : ""} + /> + Repository URL (HTTPS or SSH) + {errors.remote_url && {errors.remote_url}}
diff --git a/apps/web/src/pages/config-profiles.tsx b/apps/web/src/pages/config-profiles.tsx index a2a6407..e49263d 100644 --- a/apps/web/src/pages/config-profiles.tsx +++ b/apps/web/src/pages/config-profiles.tsx @@ -19,7 +19,6 @@ import { type ResolvedProfile, } from "../api/config_profiles"; import { listProjects } from "../api/projects"; -import { listAllUserRepositories, createExternalRepository, 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"; @@ -34,7 +33,6 @@ export const ConfigProfilesPage = () => { const [profiles, setProfiles] = useState([]); const [projects, setProjects] = useState([]); const [toolTypes, setToolTypes] = useState([]); - const [repositories, setRepositories] = useState([]); const [selectedProfileId, setSelectedProfileId] = useState(null); const [isCreating, setIsCreating] = useState(false); @@ -72,14 +70,6 @@ export const ConfigProfilesPage = () => { setProjects(projs || []); setToolTypes(types || []); - // Load all user repositories (including external ones) - try { - const allRepos = await listAllUserRepositories(); - setRepositories(allRepos); - } catch { - setRepositories([]); - } - setStatus("ready"); } catch { setStatus("error"); @@ -1257,16 +1247,7 @@ export const ConfigProfilesPage = () => {
updateFormField("git_mounts", git_mounts)} - onCreateRepository={async (name, remoteUrl) => { - const repo = await createExternalRepository({ - name, - remote_url: remoteUrl, - }); - setRepositories((prev) => [...prev, repo]); - return repo; - }} />
diff --git a/openspec/changes/config-profile-git-mounts/tasks.md b/openspec/changes/config-profile-git-mounts/tasks.md index 4021297..bbe9f18 100644 --- a/openspec/changes/config-profile-git-mounts/tasks.md +++ b/openspec/changes/config-profile-git-mounts/tasks.md @@ -73,3 +73,10 @@ - [x] 9.6 Update spec: auto-clone to persistent location on every container creation - [x] 9.7 Update spec: pull updates when creating new containers - [x] 9.8 Update spec: per-instance isolation (no shared clones) + +## 10. UI Improvements + +- [x] 10.1 Add ability to create external repositories from git mount editor +- [x] 10.2 Show "+ Add new repository..." option in repo dropdown +- [x] 10.3 Add form fields for repo name and remote URL +- [x] 10.4 Auto-refresh repo list after creating new repository