diff --git a/basic-memory-git-sync/src/basic_memory_git_sync/sync.py b/basic-memory-git-sync/src/basic_memory_git_sync/sync.py index b966a77..0b7dace 100644 --- a/basic-memory-git-sync/src/basic_memory_git_sync/sync.py +++ b/basic-memory-git-sync/src/basic_memory_git_sync/sync.py @@ -83,20 +83,45 @@ def __init__( # -- lifecycle -- def attach(self) -> None: - """Attach to the existing checkout, or clone it on first boot.""" + """Attach to the existing checkout, or initialise + fetch on first boot. + + On a fresh PVC the directory is usually empty, so we clone. But the + shared vault PVC can also be created non-empty-but-not-a-repo — e.g. + when another container (or the vault-agent secrets mount) has already + touched the mount root — in which case ``clone_from`` would fail with + "destination path exists and is not an empty directory". We treat + "exists, not a git repo" as a repo to initialise: ``git init`` can + adopt an existing directory, then we add the origin and fetch the + branch. This makes first boot robust to the shared-PVC layout without + discarding anything already on the volume. + """ if self._vault_dir.exists() and (self._vault_dir / ".git").exists(): self._repo = Repo(self._vault_dir) self._log.info("backstop.attached", dir=str(self._vault_dir)) self._repo.remotes.origin.fetch() - else: - self._vault_dir.parent.mkdir(parents=True, exist_ok=True) - self._repo = Repo.clone_from( - self._clone_url, - self._vault_dir, - branch=self._branch, - env=self._git_env(), - ) - self._log.info("backstop.cloned", dir=str(self._vault_dir)) + return + + self._vault_dir.parent.mkdir(parents=True, exist_ok=True) + if self._vault_dir.exists() and not (self._vault_dir / ".git").exists(): + # Adopt the existing (non-git) directory instead of clone_from, + # which refuses a non-empty target. init + remote + fetch mirrors + # a clone without requiring an empty target. + repo = Repo.init(self._vault_dir) + repo.create_remote("origin", self._clone_url) + with repo.git.custom_environment(**self._git_env()): + repo.git.fetch("origin", self._branch) + repo.git.checkout("-B", self._branch, f"origin/{self._branch}") + self._repo = repo + self._log.info("backstop.init_adopted", dir=str(self._vault_dir)) + return + + self._repo = Repo.clone_from( + self._clone_url, + self._vault_dir, + branch=self._branch, + env=self._git_env(), + ) + self._log.info("backstop.cloned", dir=str(self._vault_dir)) def close(self) -> None: self._repo = None diff --git a/basic-memory-git-sync/tests/unit/test_sync.py b/basic-memory-git-sync/tests/unit/test_sync.py index 0aecb71..9350692 100644 --- a/basic-memory-git-sync/tests/unit/test_sync.py +++ b/basic-memory-git-sync/tests/unit/test_sync.py @@ -68,6 +68,27 @@ def test_attach_reuses_existing_checkout(tmp_path: Path, remote: Path) -> None: b.attach() # no error +def test_attach_adopts_existing_non_git_dir(tmp_path: Path, remote: Path) -> None: + """First boot on a shared PVC root that is non-empty but not a git repo. + + Reproduces the production failure: another container (or the vault-agent + secrets mount) has already written into the vault PVC root, so + ``clone_from`` would refuse per "destination path exists and is not an + empty directory". attach() must init + fetch into the adopted dir instead + of failing, without discarding what is already on the volume. + """ + v = tmp_path / "vault" + v.mkdir(parents=True) + (v / "secrets").mkdir() # e.g. the vault-agent secrets overlay + b = VaultGitBackstop(clone_url=str(remote), vault_dir=v, push=False) + b.attach() + assert (v / ".git").exists() + assert (v / "secrets").exists() # not discarded + # The remote branch was fetched + checked out; the sidecar can now push writes. + assert b._repo.active_branch.name == "main" + assert b._repo.remotes.origin.url == str(remote) + + def test_poll_commits_nothing_when_clean(backstop: VaultGitBackstop) -> None: result = backstop.poll_once() assert result.committed is False