fix(6245): a re-run of bind_repo(moved_from=…) finishes an earlier move
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 59s
CI & Build / integration (push) Successful in 1m25s
CI & Build / Python tests (push) Successful in 2m20s
CI & Build / Build & push image (push) Successful in 32s

Once the binding has moved, the old remote is no longer bound, and
move_binding refused. An install that renamed its repo before snippet
locations moved with it had no way to move them. When the old remote is
unbound and the new one is bound to this project, bind_repo now skips the
binding move and still moves the snippet locations that name the old
repo.

The snippet-move test gives each location its own path: rewritten to one
repo, identical locations merge into one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 16:21:19 -04:00
co-authored by Claude Opus 5.5
parent e89e6a24de
commit f77d18cb63
2 changed files with 45 additions and 5 deletions
+13 -4
View File
@@ -34,8 +34,10 @@ async def bind_repo(
under the old one. Pass the old remote as `moved_from` and the binding,
every shape judgment, and every snippet location recorded in the old repo
move to the new name together. Binding the new name without it starts an
empty ledger beside the old one. A snippet shared with you read-only keeps
its location and is listed in `snippets_read_only`; its owner moves it.
empty ledger beside the old one. Running it again once the binding has
moved moves only what still names the old repo. A snippet shared with you
read-only keeps its location and is listed in `snippets_read_only`; its
owner moves it.
Args:
repo_url: the repo's git remote (e.g. the output of
@@ -54,9 +56,16 @@ async def bind_repo(
ref_arg = None if not ref else ("" if ref.strip() == "-" else ref)
moved = snippets = None
if moved_from:
_, moved = await repo_bindings_svc.move_binding(
uid, moved_from, repo_url, project_id,
already = (
await repo_bindings_svc.resolve_project(uid, moved_from) is None
and await repo_bindings_svc.resolve_project(uid, repo_url) == project_id
)
if already:
moved = 0
else:
_, moved = await repo_bindings_svc.move_binding(
uid, moved_from, repo_url, project_id,
)
snippets = await snippets_svc.move_location_repo(
uid, project_id, moved_from, repo_url,
)
+32 -1
View File
@@ -112,7 +112,12 @@ async def test_the_snippets_recorded_in_the_old_repo_move_with_it(bound):
note = await snippets_svc.create_snippet(
owner, name=name, code=f"def {name}():\n pass\n", language="python",
project_id=pid,
locations=[{"repo": r, "path": "src/app.py", "symbol": name} for r in repos],
# A path per location: rewritten to one repo, identical locations
# would merge into one.
locations=[
{"repo": r, "path": f"src/app{i}.py", "symbol": name}
for i, r in enumerate(repos)
],
)
return int(note.id)
@@ -132,3 +137,29 @@ async def test_the_snippets_recorded_in_the_old_repo_move_with_it(bound):
assert await repos(moved_one) == [NEW_KEY, NEW_KEY, NEW_KEY]
assert await repos(mixed) == [NEW_KEY, "elsewhere/gadget"]
assert await repos(untouched) == ["Gadget", "git.example.com/alice/oldnamesake"]
@pytest.mark.integration
async def test_running_the_move_again_finishes_what_an_earlier_one_left(bound):
from grimoire.mcp._context import _user_id_ctx
from grimoire.mcp.tools.repos import bind_repo
from grimoire.services import snippets as snippets_svc
owner, pid = bound["owner"], bound["pid"]
await svc.move_binding(owner, OLD, NEW, pid)
note = await snippets_svc.create_snippet(
owner, name="left_behind", code="def left_behind():\n pass\n",
language="python", project_id=pid,
locations=[{"repo": "OldName", "path": "src/app.py", "symbol": "left_behind"}],
)
token = _user_id_ctx.set(owner)
try:
out = await bind_repo(NEW, pid, moved_from=OLD)
finally:
_user_id_ctx.reset(token)
assert out["shapes_moved"] == 0
assert out["snippets_moved"] == [int(note.id)]
moved = await snippets_svc.get_snippet(owner, int(note.id))
assert snippets_svc.snippet_fields(moved)["locations"][0]["repo"] == NEW_KEY