refactor: store workspaces as {workspace_id}/{repo_name} for natural git clone layout
Working copies were stored as /data/working-copies/{repo_id}/{workspace_name}/,
so git clone was forced into a user-named directory. That meant the container
mount basename was the workspace name (e.g. main) instead of the repo name.
- Generate the workspace UUID before cloning and clone into
/data/working-copies/{workspace_id}/ so git creates {repo_name}/ naturally
- Set workspace.path to /data/working-copies/{workspace_id}/{repo_name}/
- Update _migrate_clone_into_workspace() to use the same layout
- _get_repository_mount_name() now prefers workspace.path basename and only
falls back to remote URL / repo.name for legacy repo-only instances
- Update unit tests to assert workspace path basename is used for mounts
Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
This commit is contained in:
@@ -20,6 +20,7 @@ from src.models import (
|
||||
SSHKey,
|
||||
ToolInstance,
|
||||
ToolType,
|
||||
Workspace,
|
||||
)
|
||||
from src.schemas.tool import CreateInstanceRequest, StartInstanceRequest
|
||||
from src.services.git.clone import check_dirty_state, clone_repository
|
||||
@@ -79,13 +80,21 @@ logger = logging.getLogger(__name__)
|
||||
_event_bus = InstanceEventBus()
|
||||
|
||||
|
||||
def _get_repository_mount_name(repo: GitRepository) -> str:
|
||||
"""Return the directory name a standard git clone would create.
|
||||
def _get_repository_mount_name(
|
||||
repo: GitRepository,
|
||||
workspace: "Workspace | None" = None,
|
||||
) -> str:
|
||||
"""Return the directory name the repository should appear under in the container.
|
||||
|
||||
Prefers the repository name parsed from the remote URL so the container
|
||||
mount matches what users expect from ``git clone``. Falls back to the
|
||||
user-provided repository name when no remote URL is available.
|
||||
When a workspace exists, the on-disk layout is
|
||||
``/data/working-copies/{workspace_id}/{repo_name}/``, so the repo-named
|
||||
directory is already available as the basename of ``workspace.path``. For
|
||||
legacy repo-only instances we fall back to parsing the remote URL like a
|
||||
standard ``git clone`` would, then to the user-provided repository name.
|
||||
"""
|
||||
if workspace is not None and workspace.path:
|
||||
return os.path.basename(os.path.normpath(workspace.path))
|
||||
|
||||
if repo.remote_url:
|
||||
base_url = extract_base_repo_url(repo.remote_url) or repo.remote_url
|
||||
name = base_url.rstrip("/").split("/")[-1]
|
||||
@@ -907,10 +916,14 @@ async def prepare_manifest_instance(
|
||||
|
||||
# Use the repository name for the workspace/repo mount target, not the
|
||||
# directory name of a workspace/clone path (which may be "main" or similar).
|
||||
# Prefer the name parsed from the remote URL so it matches a standard clone.
|
||||
# Prefer the actual on-disk workspace directory name when a workspace is
|
||||
# mounted, otherwise fall back to parsing the remote URL like git clone.
|
||||
repo = await session.get(GitRepository, instance.repository_id)
|
||||
workspace: Workspace | None = None
|
||||
if instance.workspace_id:
|
||||
workspace = await session.get(Workspace, instance.workspace_id)
|
||||
repo_name = (
|
||||
_get_repository_mount_name(repo)
|
||||
_get_repository_mount_name(repo, workspace)
|
||||
if repo
|
||||
else os.path.basename(os.path.normpath(repo_path))
|
||||
)
|
||||
@@ -1075,7 +1088,7 @@ async def create_tool_instance(
|
||||
)
|
||||
|
||||
home_dir = tool_type.home_directory or "/home/user"
|
||||
mount_name = _get_repository_mount_name(repo)
|
||||
mount_name = _get_repository_mount_name(repo, workspace)
|
||||
workspace_target = f"{home_dir}/{mount_name}"
|
||||
|
||||
compose_content = f"""version: "3.8"\nservices:
|
||||
@@ -1112,14 +1125,15 @@ async def create_tool_instance(
|
||||
|
||||
# Manifest templates use WORKSPACE_PATH; REPO_PATH is retained as a
|
||||
# deprecated alias for backward compatibility with older templates.
|
||||
mount_name = _get_repository_mount_name(repo, workspace)
|
||||
variables = {
|
||||
"IMAGE_TAG": image_tag,
|
||||
"INSTANCE_NAME": instance_name.lower(),
|
||||
"INSTANCE_DIR": instance_dir,
|
||||
"WORKSPACE_PATH": repo_path,
|
||||
"REPO_PATH": repo_path,
|
||||
"REPO_NAME": _get_repository_mount_name(repo),
|
||||
"WORKSPACE_NAME": _get_repository_mount_name(repo),
|
||||
"REPO_NAME": mount_name,
|
||||
"WORKSPACE_NAME": mount_name,
|
||||
"SSH_PATH": "",
|
||||
"TOOL_PORT": tool_port,
|
||||
"EXTRA_ENV": {},
|
||||
@@ -1140,7 +1154,7 @@ async def create_tool_instance(
|
||||
"TOOL_PORT": tool_port,
|
||||
"USER_ID": str(user_id),
|
||||
"PROJECT_ID": str(project_id),
|
||||
"WORKSPACE_NAME": _get_repository_mount_name(repo),
|
||||
"WORKSPACE_NAME": _get_repository_mount_name(repo, workspace),
|
||||
"HOME_DIRECTORY": tool_type.home_directory or "/home/user",
|
||||
}
|
||||
compose_content = render_compose_template(tool_type.compose_template, variables)
|
||||
|
||||
Reference in New Issue
Block a user