fix(6245): bind_repo(moved_from=…) moves the snippet locations recorded in the old repo
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 58s
CI & Build / integration (push) Failing after 1m16s
CI & Build / Python tests (push) Successful in 2m33s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 16s
CI & Build / TypeScript typecheck (push) Successful in 58s
CI & Build / integration (push) Failing after 1m16s
CI & Build / Python tests (push) Successful in 2m33s
CI & Build / Build & push image (push) Skipped
A repo renamed on its forge kept every snippet location naming the old repo: the binding and the shape ledger moved, the snippets did not. - snippets.move_location_repo rewrites, on every live snippet of the project, each location whose repo is the old remote in any form (URL, normalized key, "owner/Repo", bare "Repo", compared without case) to the new normalized key. A snippet the caller can only read is listed in read_only and left alone. - Its loop is shared with move_locations through _rewrite_locations, which saves each changed snippet through update_snippet. - bind_repo calls it after the binding moves and reports snippets_moved and snippets_read_only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1280,22 +1280,15 @@ def _path_touches(recorded: str, changed: str) -> bool:
|
||||
return changed == recorded or changed.startswith(recorded + "/")
|
||||
|
||||
|
||||
async def move_locations(
|
||||
user_id: int, project_id: int, from_path: str, to_path: str,
|
||||
) -> dict:
|
||||
"""Re-path the recorded locations under ``from_path`` (a file or a
|
||||
directory) to the same place under ``to_path``, on every live snippet of
|
||||
the project the caller can edit.
|
||||
async def _rewrite_locations(user_id: int, project_id: int, rewrite) -> dict:
|
||||
"""Apply ``rewrite`` to every recorded location of the project's live
|
||||
snippets, and save each snippet a rewrite changed.
|
||||
|
||||
The twin of the shape ledger's move: a snippet recorded at a path that
|
||||
moved reads `missing` at its next check, and its canonical stamp no longer
|
||||
covers the shape that moved. Each snippet is rewritten through
|
||||
update_snippet, so its body and indexed fields stay one record. A
|
||||
snippet the caller can only read is left as it is and listed in
|
||||
``read_only``.
|
||||
``rewrite(location)`` answers the new location, or None to leave it as it
|
||||
is. Each snippet is saved through update_snippet, so its body and indexed
|
||||
fields stay one record. A snippet the caller can only read is left as it
|
||||
is and listed in ``read_only``.
|
||||
"""
|
||||
old = (from_path or "").strip().strip("/")
|
||||
new = (to_path or "").strip().strip("/")
|
||||
async with async_session() as session:
|
||||
notes = (
|
||||
await session.execute(
|
||||
@@ -1310,15 +1303,14 @@ async def move_locations(
|
||||
read_only: list[int] = []
|
||||
for note in notes:
|
||||
locations = snippet_fields(note).get("locations") or []
|
||||
if not any(_path_touches(old, loc.get("path") or "") for loc in locations):
|
||||
rewritten = [rewrite(loc) for loc in locations]
|
||||
if all(new is None for new in rewritten):
|
||||
continue
|
||||
repathed = [
|
||||
{**loc, "path": new + loc["path"].strip("/")[len(old):]}
|
||||
if _path_touches(old, loc.get("path") or "") else loc
|
||||
for loc in locations
|
||||
]
|
||||
try:
|
||||
await update_snippet(user_id, int(note.id), locations=repathed)
|
||||
await update_snippet(
|
||||
user_id, int(note.id),
|
||||
locations=[new or loc for new, loc in zip(rewritten, locations)],
|
||||
)
|
||||
except PermissionError:
|
||||
read_only.append(int(note.id))
|
||||
continue
|
||||
@@ -1326,6 +1318,57 @@ async def move_locations(
|
||||
return {"moved": moved, "read_only": read_only}
|
||||
|
||||
|
||||
async def move_locations(
|
||||
user_id: int, project_id: int, from_path: str, to_path: str,
|
||||
) -> dict:
|
||||
"""Re-path the recorded locations under ``from_path`` (a file or a
|
||||
directory) to the same place under ``to_path``, on every live snippet of
|
||||
the project the caller can edit.
|
||||
|
||||
The twin of the shape ledger's move: a snippet recorded at a path that
|
||||
moved reads `missing` at its next check, and its canonical stamp no longer
|
||||
covers the shape that moved. Returns {"moved": [ids], "read_only": [ids]}.
|
||||
"""
|
||||
old = (from_path or "").strip().strip("/")
|
||||
new = (to_path or "").strip().strip("/")
|
||||
|
||||
def repath(loc: dict) -> dict | None:
|
||||
if not _path_touches(old, loc.get("path") or ""):
|
||||
return None
|
||||
return {**loc, "path": new + loc["path"].strip("/")[len(old):]}
|
||||
|
||||
return await _rewrite_locations(user_id, project_id, repath)
|
||||
|
||||
|
||||
async def move_location_repo(
|
||||
user_id: int, project_id: int, old_repo: str, new_repo: str,
|
||||
) -> dict:
|
||||
"""Point the project's snippet locations recorded in ``old_repo`` at
|
||||
``new_repo``, for a repo renamed on its forge.
|
||||
|
||||
The twin of the binding move: a location still naming the old repo
|
||||
resolves to nothing on the forge. Repo names are recorded free-form, so a
|
||||
location matches when it is the old remote in any form, or a trailing
|
||||
part of it ("owner/Repo", "Repo"), compared without case. A match is
|
||||
rewritten to the new normalized key. Returns {"moved": [ids],
|
||||
"read_only": [ids]}.
|
||||
"""
|
||||
from grimoire.services.repo_bindings import normalize_repo_key
|
||||
|
||||
old_key = normalize_repo_key(old_repo)
|
||||
new_key = normalize_repo_key(new_repo)
|
||||
|
||||
def rename(loc: dict) -> dict | None:
|
||||
recorded = normalize_repo_key(loc.get("repo") or "")
|
||||
if not recorded or recorded == new_key:
|
||||
return None
|
||||
if recorded != old_key and not old_key.endswith("/" + recorded):
|
||||
return None
|
||||
return {**loc, "repo": new_key}
|
||||
|
||||
return await _rewrite_locations(user_id, project_id, rename)
|
||||
|
||||
|
||||
async def invalidate_for_push(
|
||||
repo_key: str,
|
||||
changed: list[str],
|
||||
|
||||
Reference in New Issue
Block a user