Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 22 additions & 29 deletions basic-memory-git-sync/src/basic_memory_git_sync/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from typing import Protocol

import structlog
from git import Actor, GitCommandError, Remote, Repo
from git import Actor, GitCommandError, Repo

from basic_memory_git_sync.settings import Settings

Expand Down Expand Up @@ -98,20 +98,28 @@ def attach(self) -> None:
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))
origin = self._origin()
if origin is None:
# A previous crashed boot left a .git with no origin remote
# (e.g. Repo.init persisted but the create_remote/fetch did
# not). Re-register it before fetching so attach converges
# instead of crashing on the missing `.origin`.
self._log.info("backstop.registering_origin", dir=str(self._vault_dir))
origin = self._repo.create_remote("origin", self._clone_url)
# GitPython's remote API is unreliable on a partially-initialised
# repo (remotes["origin"] can raise even when origin is in config),
# so drive the recovery with `git remote` / `git fetch` commands
# directly. If origin is absent, (re)add it; then ensure the
# tracked branch exists and fetch.
with self._repo.git.custom_environment(**self._git_env()):
origin.fetch()
# A bare Repo.init (no remote, no branch) leaves the repo on
# the git default branch (often `master`); adopt the branch we
# track so polls commit to the right ref.
if self._repo.active_branch.name != self._branch:
try:
self._repo.git.remote("get-url", "origin")
except GitCommandError:
self._log.info("backstop.registering_origin", dir=str(self._vault_dir))
self._repo.git.remote("add", "origin", self._clone_url)
self._repo.git.fetch("origin")
self._repo.git.fetch("origin", self._branch)
# A bare Repo.init (or crash) can leave the repo on the git
# default branch (often `master`) or on no branch (unborn
# HEAD); adopt the branch we track so polls commit to the
# right ref.
try:
head = self._repo.git.rev_parse("--abbrev-ref", "HEAD") or ""
except GitCommandError:
head = ""
if head != self._branch:
self._repo.git.checkout("-B", self._branch, f"origin/{self._branch}")
self._log.info("backstop.branch_adopted", branch=self._branch)
return
Expand Down Expand Up @@ -215,21 +223,6 @@ def _require_repo(self) -> Repo:
raise RuntimeError("VaultGitBackstop.attach() must be called before poll_once()")
return self._repo

def _origin(self) -> Remote | None:
"""Look up the `origin` remote by name, or None if absent.

GitPython's ``repo.remotes.origin`` attribute raises when no remote
named ``origin`` is registered (e.g. after a crashed ``Repo.init``
that persisted the repo without the remote). Look it up defensively
by name so attach() can re-register it instead of crashing.
"""
if self._repo is None:
return None
try:
return self._repo.remotes["origin"]
except IndexError:
return None


def run_forever(
settings: Settings,
Expand Down
8 changes: 4 additions & 4 deletions basic-memory-git-sync/tests/unit/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,16 @@ def test_attach_readds_origin_when_missing(tmp_path: Path, remote: Path) -> None
"""A previous crashed boot left a .git with no origin remote.

Reproduces the production crash: a prior boot did ``Repo.init`` but died
before ``create_remote``/fetch persisted, so ``attach()`` sees a real
.git but ``repo.remotes.origin`` raises. The new ``_origin()`` look-up
must re-register the remote and fetch instead of crashing.
before the remote/fetch persisted, so ``attach()`` sees a real .git but
no usable origin. attach() must re-add origin (git remote + fetch) and
adopt the tracked branch instead of crashing.
"""
v = tmp_path / "vault"
v.mkdir(parents=True)
Repo.init(v)
b = VaultGitBackstop(clone_url=str(remote), vault_dir=v, push=False)
b.attach() # must not raise
assert b._repo.remotes["origin"].url == str(remote)
assert b._repo.git.remote("get-url", "origin") == str(remote)
assert b._repo.active_branch.name == "main"


Expand Down
Loading