fix: reuse cached config profile git mounts
- Reuse valid deterministic git mount clones on repeated starts - Remove incomplete clone destinations before retrying - Add regression coverage for cached and partial clones - Document OpenSpec change fix-config-profile-git-mount-clone-reuse Quality gates: pytest (13 passed), ruff, mypy
This commit is contained in:
@@ -283,16 +283,27 @@ def clone_git_repo(
|
||||
url_hash = hashlib.md5(f"{remote_url}:{branch_segment}".encode()).hexdigest()[:12]
|
||||
repo_name = remote_url.split("/")[-1].replace(".git", "") or "repo"
|
||||
clone_dir = os.path.join(clone_parent, "git-mounts", f"{repo_name}-{url_hash}")
|
||||
repo_path = clone_repository(
|
||||
remote_url,
|
||||
None, # No SSH key for now - can be added later
|
||||
clone_dir,
|
||||
branch or "main",
|
||||
project_name=project_name,
|
||||
)
|
||||
clone_name = _slugify_directory_name(project_name) if project_name else "repo-clone"
|
||||
repo_path = os.path.join(clone_dir, clone_name)
|
||||
|
||||
if not os.path.exists(repo_path):
|
||||
if os.path.isdir(os.path.join(repo_path, ".git")):
|
||||
# Reuse the deterministic per-repository cache on repeated starts.
|
||||
try:
|
||||
pull_repository_updates(repo_path, remote_url)
|
||||
logger.debug("Pulled updates for git mount %s", remote_url)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to pull updates for %s: %s", remote_url, exc)
|
||||
else:
|
||||
try:
|
||||
# A failed clone can leave its destination behind. Remove only the
|
||||
# computed clone path so the next start can retry cleanly.
|
||||
if os.path.lexists(repo_path):
|
||||
logger.warning("Removing incomplete git mount clone at %s", repo_path)
|
||||
if os.path.isdir(repo_path) and not os.path.islink(repo_path):
|
||||
shutil.rmtree(repo_path)
|
||||
else:
|
||||
os.unlink(repo_path)
|
||||
|
||||
os.makedirs(clone_dir, exist_ok=True)
|
||||
repo_path = clone_repository(
|
||||
remote_url,
|
||||
@@ -305,13 +316,6 @@ def clone_git_repo(
|
||||
except Exception as exc:
|
||||
logger.warning("Clone failed for git mount %s: %s", remote_url, exc)
|
||||
raise
|
||||
else:
|
||||
# Repo exists - pull latest updates
|
||||
try:
|
||||
pull_repository_updates(repo_path, remote_url)
|
||||
logger.debug("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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Unit tests for the tool instance service."""
|
||||
|
||||
import hashlib
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
@@ -8,6 +9,7 @@ import pytest
|
||||
from src.services.tool.instance_service import (
|
||||
_get_repository_mount_name,
|
||||
_stack_profile_mounts_with_git_mounts,
|
||||
clone_git_repo,
|
||||
modify_compose_file,
|
||||
prepare_manifest_instance,
|
||||
)
|
||||
@@ -67,6 +69,61 @@ class TestGetRepositoryMountName:
|
||||
assert _get_repository_mount_name(project, repo) == "project-v2-0"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCloneGitRepo:
|
||||
"""Regression tests for reusable config-profile git mount clones."""
|
||||
|
||||
def test_reuses_existing_clone(self, monkeypatch, tmp_path) -> None:
|
||||
from src.services.tool import instance_service
|
||||
|
||||
remote_url = "https://gitlab.com/example/dotfiles"
|
||||
branch = None
|
||||
clone_parent = str(tmp_path)
|
||||
url_hash = hashlib.md5(f"{remote_url}:default".encode()).hexdigest()[:12]
|
||||
repo_path = (
|
||||
tmp_path
|
||||
/ "git-mounts"
|
||||
/ f"dotfiles-{url_hash}"
|
||||
/ "repo-clone"
|
||||
)
|
||||
(repo_path / ".git").mkdir(parents=True)
|
||||
|
||||
clone = MagicMock(side_effect=AssertionError("existing clone must be reused"))
|
||||
pull = MagicMock()
|
||||
monkeypatch.setattr(instance_service, "clone_repository", clone)
|
||||
monkeypatch.setattr(instance_service, "pull_repository_updates", pull)
|
||||
|
||||
result = clone_git_repo(remote_url, branch, clone_parent)
|
||||
|
||||
assert result == str(repo_path)
|
||||
clone.assert_not_called()
|
||||
pull.assert_called_once_with(str(repo_path), remote_url)
|
||||
|
||||
def test_replaces_incomplete_clone_before_retry(self, monkeypatch, tmp_path) -> None:
|
||||
from src.services.tool import instance_service
|
||||
|
||||
remote_url = "https://gitlab.com/example/dotfiles"
|
||||
url_hash = hashlib.md5(f"{remote_url}:main".encode()).hexdigest()[:12]
|
||||
clone_dir = tmp_path / "git-mounts" / f"dotfiles-{url_hash}"
|
||||
repo_path = clone_dir / "repo-clone"
|
||||
repo_path.mkdir(parents=True)
|
||||
(repo_path / "partial-file").write_text("incomplete")
|
||||
|
||||
def clone(_url, _key, destination, _branch, project_name=None):
|
||||
assert destination == str(clone_dir)
|
||||
assert project_name is None
|
||||
assert not repo_path.exists()
|
||||
(repo_path / ".git").mkdir(parents=True)
|
||||
return str(repo_path)
|
||||
|
||||
monkeypatch.setattr(instance_service, "clone_repository", clone)
|
||||
|
||||
result = clone_git_repo(remote_url, "main", str(tmp_path))
|
||||
|
||||
assert result == str(repo_path)
|
||||
assert (repo_path / ".git").is_dir()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestStackProfileMountsWithGitMounts:
|
||||
"""Tests for _stack_profile_mounts_with_git_mounts."""
|
||||
|
||||
Reference in New Issue
Block a user