feat(6242): a repo renamed on its forge keeps its binding and shape ledger
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 1m13s
CI & Build / integration (push) Successful in 1m15s
CI & Build / Python tests (push) Successful in 2m8s
CI & Build / Build & push image (push) Successful in 28s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 1m13s
CI & Build / integration (push) Successful in 1m15s
CI & Build / Python tests (push) Successful in 2m8s
CI & Build / Build & push image (push) Successful in 28s
A binding and every row of its project's code-shape ledger are keyed by the repo's remote. A rename on the forge changes the remote, so binding the new name started an empty ledger beside the old one and left every judgment behind. bind_repo now takes moved_from, the old remote. Its binding and the project's ledger rows move to the new key in one transaction, and the reply says how many shapes moved. The move is refused when: - the old name isn't bound to that project; - the new name is already bound; - the project already has a ledger under the new name. Each refusal leaves both names exactly as they were. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,10 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from grimoire.models import async_session
|
||||
from grimoire.models.code_shape import CodeShape
|
||||
from grimoire.models.repo_binding import RepoBinding
|
||||
|
||||
|
||||
@@ -100,6 +101,58 @@ async def set_binding(
|
||||
return binding
|
||||
|
||||
|
||||
async def move_binding(
|
||||
user_id: int, old_repo: str, new_repo: str, project_id: int,
|
||||
) -> tuple[RepoBinding, int]:
|
||||
"""Carry a renamed repo's binding, and its project's shape ledger, from the
|
||||
old remote to the new one. Returns the binding and how many shapes moved.
|
||||
|
||||
Every ledger row is keyed by the repo it was found in, so a repo renamed on
|
||||
its forge would otherwise start an empty ledger under the new name and
|
||||
leave every judgment behind under the old one. Both keys change in one
|
||||
transaction. It refuses rather than merge: the old key must be bound to
|
||||
this project by the caller, and the new key must have neither a binding of
|
||||
the caller's nor ledger rows of this project's.
|
||||
"""
|
||||
old_key = normalize_repo_key(old_repo)
|
||||
new_key = normalize_repo_key(new_repo)
|
||||
if not old_key or not new_key:
|
||||
raise ValueError("both remotes must be parseable repo URLs")
|
||||
if old_key == new_key:
|
||||
raise ValueError(f"`{old_key}` is the same repo under both names")
|
||||
async with async_session() as session:
|
||||
mine = select(RepoBinding).where(RepoBinding.user_id == user_id)
|
||||
binding = (
|
||||
await session.execute(mine.where(RepoBinding.repo_key == old_key))
|
||||
).scalar_one_or_none()
|
||||
if binding is None or binding.project_id != project_id:
|
||||
raise ValueError(f"`{old_key}` is not bound to project {project_id}")
|
||||
if (
|
||||
await session.execute(mine.where(RepoBinding.repo_key == new_key))
|
||||
).scalar_one_or_none() is not None:
|
||||
raise ValueError(
|
||||
f"`{new_key}` is already bound; unbind it first, then move"
|
||||
)
|
||||
shapes_here = select(CodeShape.id).where(CodeShape.project_id == project_id)
|
||||
if (
|
||||
await session.execute(
|
||||
shapes_here.where(CodeShape.repo_key == new_key).limit(1)
|
||||
)
|
||||
).first() is not None:
|
||||
raise ValueError(
|
||||
f"project {project_id} already has a ledger under `{new_key}`"
|
||||
)
|
||||
moved = await session.execute(
|
||||
update(CodeShape)
|
||||
.where(CodeShape.project_id == project_id, CodeShape.repo_key == old_key)
|
||||
.values(repo_key=new_key)
|
||||
)
|
||||
binding.repo_key = new_key
|
||||
await session.commit()
|
||||
await session.refresh(binding)
|
||||
return binding, moved.rowcount or 0
|
||||
|
||||
|
||||
async def list_bindings(user_id: int) -> list[RepoBinding]:
|
||||
async with async_session() as session:
|
||||
rows = await session.execute(
|
||||
|
||||
Reference in New Issue
Block a user