feat(terminal): use project name for in-container cwd and clone directory
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import uuid
|
||||
@@ -17,12 +18,12 @@ from sqlalchemy import select
|
||||
from src.models import Workspace
|
||||
from src.services.git.git_service import GitService
|
||||
from src.services.shared.ssh_keys import _get_fernet
|
||||
from src.utils.git_url_parser import extract_base_repo_url
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.models import GitRepository
|
||||
from src.models import Project
|
||||
from src.models import ToolInstance
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -49,30 +50,36 @@ class WorkspaceManager:
|
||||
BASE_PATH = "/data/working-copies"
|
||||
|
||||
@staticmethod
|
||||
def _repo_directory_name(repo: "GitRepository") -> str:
|
||||
"""Return the directory name git would create for a standard clone.
|
||||
def _directory_slug(name: str) -> str:
|
||||
"""Return a filesystem-safe, lowercase directory slug."""
|
||||
slug = name.lower().strip()
|
||||
slug = re.sub(r"[^a-z0-9_-]+", "-", slug)
|
||||
slug = re.sub(r"-+", "-", slug).strip("-")
|
||||
return slug or "project"
|
||||
|
||||
Prefers the name parsed from the remote URL and falls back to the
|
||||
user-provided repository name when no remote URL is available.
|
||||
@staticmethod
|
||||
def _repo_directory_name(project_name: str) -> str:
|
||||
"""Return the project-name directory for a workspace clone.
|
||||
|
||||
New workspaces are cloned under ``/data/working-copies/{workspace_id}/{project_name}/``
|
||||
so that the in-container mount target and terminal cwd can match
|
||||
``/home/user/{project_name}``.
|
||||
"""
|
||||
if repo.remote_url:
|
||||
base_url = extract_base_repo_url(repo.remote_url) or repo.remote_url
|
||||
name = base_url.rstrip("/").split("/")[-1]
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
if name:
|
||||
return name
|
||||
return repo.name
|
||||
return WorkspaceManager._directory_slug(project_name)
|
||||
|
||||
def _workspace_path(self, workspace_id: uuid.UUID, repo: "GitRepository") -> str:
|
||||
def _workspace_path(
|
||||
self,
|
||||
workspace_id: uuid.UUID,
|
||||
project_name: str,
|
||||
) -> str:
|
||||
"""Return the filesystem path for a workspace.
|
||||
|
||||
Layout: /data/working-copies/{workspace_id}/{repo_name}/
|
||||
Layout: /data/working-copies/{workspace_id}/{project_name}/
|
||||
The workspace_id prevents collisions between workspaces, and the
|
||||
repo_name matches the directory git clone naturally creates.
|
||||
project_name becomes the in-container directory name.
|
||||
"""
|
||||
return os.path.join(
|
||||
self.BASE_PATH, str(workspace_id), self._repo_directory_name(repo)
|
||||
self.BASE_PATH, str(workspace_id), self._repo_directory_name(project_name)
|
||||
)
|
||||
|
||||
async def create(
|
||||
@@ -82,6 +89,7 @@ class WorkspaceManager:
|
||||
name: str,
|
||||
branch: str = "main",
|
||||
session: AsyncSession | None = None,
|
||||
project: "Project | None" = None,
|
||||
) -> Workspace:
|
||||
"""Clone repo to workspace path and create DB record.
|
||||
|
||||
@@ -91,6 +99,8 @@ class WorkspaceManager:
|
||||
name: The workspace name (unique per repo).
|
||||
branch: The branch to clone (default: "main").
|
||||
session: Database session for loading SSH keys.
|
||||
project: Optional project for naming the clone directory. Fetched
|
||||
from the repo relationship if not provided.
|
||||
|
||||
Returns:
|
||||
The created Workspace record.
|
||||
@@ -101,7 +111,13 @@ class WorkspaceManager:
|
||||
if not repo.remote_url:
|
||||
raise ValueError("Repository has no remote URL")
|
||||
|
||||
repo_dir_name = self._repo_directory_name(repo)
|
||||
if project is None and repo.project_id is not None and session is not None:
|
||||
from src.models import Project
|
||||
|
||||
project = await session.get(Project, repo.project_id)
|
||||
project_name = project.name if project else repo.name
|
||||
|
||||
repo_dir_name = self._repo_directory_name(project_name)
|
||||
workspace_id = uuid.uuid4()
|
||||
parent_path = os.path.join(self.BASE_PATH, str(workspace_id))
|
||||
expected_path = os.path.join(parent_path, repo_dir_name)
|
||||
@@ -346,11 +362,18 @@ class WorkspaceManager:
|
||||
return workspace
|
||||
|
||||
from src.models import GitRepository
|
||||
from src.models import Project
|
||||
|
||||
repo = await session.get(GitRepository, instance.repository_id)
|
||||
if repo is None:
|
||||
raise RuntimeError(f"Repository {instance.repository_id} not found")
|
||||
|
||||
project = (
|
||||
await session.get(Project, instance.project_id)
|
||||
if instance.project_id
|
||||
else None
|
||||
)
|
||||
|
||||
base_name = (
|
||||
f"{instance.name}-migrated"
|
||||
if instance.clone_mode == "clone"
|
||||
@@ -364,7 +387,7 @@ class WorkspaceManager:
|
||||
|
||||
if instance.clone_mode == "clone":
|
||||
workspace = await self._migrate_clone_into_workspace(
|
||||
instance, repo, session, name
|
||||
instance, repo, session, name, project=project
|
||||
)
|
||||
else:
|
||||
workspace = await self.create(
|
||||
@@ -373,6 +396,7 @@ class WorkspaceManager:
|
||||
name=name,
|
||||
branch=instance.branch or "main",
|
||||
session=session,
|
||||
project=project,
|
||||
)
|
||||
|
||||
instance.workspace_id = workspace.id
|
||||
@@ -409,6 +433,7 @@ class WorkspaceManager:
|
||||
repo: "GitRepository",
|
||||
session: AsyncSession,
|
||||
name: str,
|
||||
project: "Project | None" = None,
|
||||
) -> Workspace:
|
||||
"""Move an existing clone-mode repo into a new workspace path."""
|
||||
import shutil
|
||||
@@ -423,7 +448,8 @@ class WorkspaceManager:
|
||||
|
||||
workspace_id = uuid.uuid4()
|
||||
parent_path = os.path.join(self.BASE_PATH, str(workspace_id))
|
||||
repo_dir_name = self._repo_directory_name(repo)
|
||||
project_name = project.name if project else repo.name
|
||||
repo_dir_name = self._repo_directory_name(project_name)
|
||||
target_path = os.path.join(parent_path, repo_dir_name)
|
||||
|
||||
os.makedirs(parent_path, exist_ok=True)
|
||||
@@ -433,8 +459,8 @@ class WorkspaceManager:
|
||||
if os.path.exists(target_path):
|
||||
shutil.rmtree(target_path, ignore_errors=True)
|
||||
|
||||
# Move the existing clone into the repo-named subdirectory so the
|
||||
# workspace path matches the natural git clone layout.
|
||||
# Move the existing clone into the project-named subdirectory so the
|
||||
# workspace path matches the new in-container layout.
|
||||
shutil.move(clone_path, target_path)
|
||||
self._make_world_writable(target_path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user