Recover external repository renames through verified Forgejo redirects
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 24s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a070b5-4994-7271-bd8b-7c3dbcedec4b
This commit is contained in:
tegwick 2026-09-05 18:57:15 +02:00
parent 022cf4b727
commit fe6b8d96c2
4 changed files with 166 additions and 2 deletions

View file

@ -12,7 +12,7 @@ import os
from dataclasses import dataclass
from pathlib import Path
from typing import Protocol
from urllib.parse import quote
from urllib.parse import quote, urljoin
import httpx
@ -32,6 +32,14 @@ class ForgeRepositoryUnreadable(ForgeRepositoryError):
pass
class ForgeRepositoryRedirected(ForgeRepositoryUnreadable):
"""Requested coordinate redirects; no credentials are forwarded."""
def __init__(self, source_url: str, location: str):
super().__init__("Forge repository coordinate redirects")
self.target_url = urljoin(source_url, location) if location else None
class ForgeRepositoryConflict(ForgeRepositoryError):
pass
@ -106,6 +114,10 @@ class ForgejoRepositoryGateway:
try:
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(url, headers=self._headers(token))
if response.status_code in {301, 302, 303, 307, 308}:
raise ForgeRepositoryRedirected(
url, response.headers.get("location", "")
)
if response.status_code == 404:
if token is None:
raise ForgeRepositoryUnreadable(

View file

@ -11,6 +11,7 @@ from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any
from urllib.parse import quote
from sqlalchemy import or_, select
from sqlalchemy.exc import IntegrityError
@ -44,6 +45,7 @@ from api.schemas.repository_rename import (
from api.services.forge_repository import (
ForgeRepositoryConflict,
ForgeRepositoryGateway,
ForgeRepositoryRedirected,
ForgeRepositorySnapshot,
ForgeRepositoryUnreadable,
)
@ -1101,13 +1103,36 @@ async def _forge_at(
async def _apply_forge_rename(
gateway: ForgeRepositoryGateway, operation: RepositoryRenameOperation
) -> dict[str, Any]:
old = await _forge_at(gateway, operation, operation.old_slug)
new = await _forge_at(gateway, operation, operation.new_slug)
if new is not None:
# A redirect is compatibility evidence only. Establish immutable identity
# at the requested new coordinate before accepting the old-name alias.
_assert_snapshot(operation, new, expected_name=operation.new_slug)
try:
old = await gateway.inspect(
instance=operation.expected_forge_instance,
owner=operation.expected_forge_owner,
name=operation.old_slug,
)
except ForgeRepositoryRedirected as exc:
canonical_api = (
operation.expected_forge_instance.rstrip("/")
+ "/api/v1/repos/"
+ quote(operation.expected_forge_owner, safe="")
+ "/"
+ quote(operation.new_slug, safe="")
)
if exc.target_url != canonical_api:
raise RenamePreconditionFailed(
"Old Forge name redirects outside the verified new coordinate"
) from exc
old = None
except ForgeRepositoryUnreadable as exc:
raise RenamePreconditionFailed(str(exc)) from exc
if old is not None:
raise RenamePreconditionFailed("Both old and new Forge names are claimed")
return {"resumed": True, "forge": new.as_dict()}
old = await _forge_at(gateway, operation, operation.old_slug)
if old is None:
raise RenamePreconditionFailed("Forge repository is absent at both expected names")
_assert_snapshot(operation, old, expected_name=operation.old_slug)