fix(preflight): resolve workspace repo for session-bound mutation preflight
Root-cause follow-up to #733. `_resolve` (the actual API-targeting resolver) already prefers the workspace-aligned git remote over the `REMOTES` default (bare `prgs` -> Scaled-Tech-Consulting/Timesheet). The shared #604 anti-stomp preflight (`_run_anti_stomp_preflight`) did not mirror that: for every mutation task whose call site omitted org/repo it filled the remote-wide `REMOTES` default, so the #530 repo guard false-positived `wrong_repo` against a Gitea-Tools checkout and blocked native issue/PR/review/merge/lock/cleanup mutations at preflight — even though the resolve stage would have targeted the workspace repo correctly. Fix: for omitted coordinates, `_run_anti_stomp_preflight` now prefers the trusted git-remote-derived workspace identity (marking those filled sides explicit, exactly like `_resolve`) for session-bound mutation tasks. This fixes every remaining preflight call site at the shared chokepoint without changing the global `REMOTES` prgs default (still the best-effort last resort when no workspace identity can be derived). `delete_branch` keeps its explicit cross-repository target contract (#733) via `_EXPLICIT_TARGET_MUTATION_TASKS` and still fails closed on omitted coordinates. Tests: tests/test_preflight_workspace_repo_forwarding.py (session-bound resolution across 20 tasks, #530-guard pass, explicit-coords passthrough, no-workspace REMOTES fallback, delete_branch contract preserved). Full suite 3228 passed / 6 skipped / 240 subtests (1 pre-existing env-leak failure in test_issue_702_review_findings_f1_f6, fails identically on baseline master). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+57
-11
@@ -767,6 +767,15 @@ def _anti_stomp_in_test_mode() -> bool:
|
||||
return not bool(os.environ.get("GITEA_TEST_FORCE_ANTI_STOMP"))
|
||||
|
||||
|
||||
# Mutation tasks that carry an explicit cross-repository *target* contract and
|
||||
# must NOT auto-resolve the workspace repository for omitted coordinates. Only
|
||||
# ``delete_branch`` (#733) qualifies: a destructive raw-branch deletion names
|
||||
# its org/repo explicitly and fails closed otherwise. Every other mutation is
|
||||
# session-bound to the single workspace-aligned repository and resolves it from
|
||||
# the trusted git remote (mirroring :func:`_resolve`).
|
||||
_EXPLICIT_TARGET_MUTATION_TASKS = frozenset({"delete_branch"})
|
||||
|
||||
|
||||
def _run_anti_stomp_preflight(
|
||||
task: str | None,
|
||||
*,
|
||||
@@ -832,21 +841,56 @@ def _run_anti_stomp_preflight(
|
||||
local_remote_url = None
|
||||
org_explicit = org is not None
|
||||
repo_explicit = repo is not None
|
||||
filled_org = False
|
||||
filled_repo = False
|
||||
if remote:
|
||||
try:
|
||||
local_remote_url = _local_git_remote_url(remote)
|
||||
except Exception:
|
||||
local_remote_url = None
|
||||
if resolved_org is None or resolved_repo is None:
|
||||
try:
|
||||
rem = _effective_remote(remote)
|
||||
if rem in REMOTES:
|
||||
if resolved_org is None:
|
||||
resolved_org = REMOTES[rem]["org"]
|
||||
if resolved_repo is None:
|
||||
resolved_repo = REMOTES[rem]["repo"]
|
||||
except Exception:
|
||||
pass
|
||||
# Session-bound mutations act on the single workspace-aligned
|
||||
# repository, so prefer the trusted git-remote-derived identity for
|
||||
# omitted coordinates — exactly as :func:`_resolve` does — instead
|
||||
# of the remote-wide ``REMOTES`` default *target*. A bare ``prgs``
|
||||
# default resolves to ``Timesheet`` and false-positived the #530
|
||||
# repo guard against a ``Gitea-Tools`` checkout, blocking native
|
||||
# issue/PR/review/merge/lock/cleanup mutations at preflight while
|
||||
# the actual resolve stage (``_resolve``) would have targeted the
|
||||
# workspace repo correctly. Workspace-filled sides are intentional
|
||||
# alignment and are marked explicit so the guard accepts them.
|
||||
# ``delete_branch`` keeps its explicit cross-repo target contract
|
||||
# (#733) and is excluded, so it still fails closed on omitted
|
||||
# coordinates. ``REMOTES`` stays a best-effort last resort only when
|
||||
# no workspace identity can be derived (preserves the dadeschools
|
||||
# default-target behavior and the existing unit suite).
|
||||
workspace_parts = None
|
||||
if task not in _EXPLICIT_TARGET_MUTATION_TASKS:
|
||||
try:
|
||||
workspace_parts = (
|
||||
remote_repo_guard.parse_org_repo_from_remote_url(
|
||||
local_remote_url
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
workspace_parts = None
|
||||
if workspace_parts:
|
||||
if resolved_org is None:
|
||||
resolved_org = workspace_parts[0]
|
||||
filled_org = True
|
||||
if resolved_repo is None:
|
||||
resolved_repo = workspace_parts[1]
|
||||
filled_repo = True
|
||||
if resolved_org is None or resolved_repo is None:
|
||||
try:
|
||||
rem = _effective_remote(remote)
|
||||
if rem in REMOTES:
|
||||
if resolved_org is None:
|
||||
resolved_org = REMOTES[rem]["org"]
|
||||
if resolved_repo is None:
|
||||
resolved_repo = REMOTES[rem]["repo"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
parity = _current_master_parity()
|
||||
startup_head = parity.get("startup_head")
|
||||
@@ -900,8 +944,10 @@ def _run_anti_stomp_preflight(
|
||||
resolved_org=resolved_org,
|
||||
resolved_repo=resolved_repo,
|
||||
local_remote_url=local_remote_url,
|
||||
org_explicit=org_explicit,
|
||||
repo_explicit=repo_explicit,
|
||||
# Workspace-filled sides are intentional alignment for #530 (same
|
||||
# contract as :func:`_resolve`), so treat them as explicit.
|
||||
org_explicit=org_explicit or filled_org,
|
||||
repo_explicit=repo_explicit or filled_repo,
|
||||
check_repo=check_repo,
|
||||
profile_name=profile_name,
|
||||
profile_role=profile_role,
|
||||
|
||||
Reference in New Issue
Block a user