Merge branch 'master' into fix/issue-855-pr-scoped-merged-cleanup
This commit is contained in:
+91
-9
@@ -2445,6 +2445,20 @@ def _evaluate_issue_lock_recovery(
|
||||
descendant_sha=local_head,
|
||||
)
|
||||
|
||||
# #871: the inverse of the #768 descendant relation — the *remote* head may
|
||||
# have advanced past the local/recorded head via a sanctioned merge-based
|
||||
# branch sync (``gitea_update_pr_branch_by_merge``) while the local worktree
|
||||
# stayed put. Observe that provenance server-side so the assessor can prove
|
||||
# it and nothing else. Probed only when the heads differ; never from any
|
||||
# caller-supplied value.
|
||||
sync_provenance: dict | None = None
|
||||
if remote_head and local_head and remote_head != local_head:
|
||||
sync_provenance = issue_lock_worktree.read_merge_sync_provenance(
|
||||
worktree_path,
|
||||
prior_head_sha=local_head,
|
||||
synced_head_sha=remote_head,
|
||||
)
|
||||
|
||||
# #772: with no remote branch there is no head to measure against, so the
|
||||
# base the branch was cut from is observed instead. Probed only in that
|
||||
# case, so the published path's evidence is untouched (#772 AC8).
|
||||
@@ -2487,6 +2501,7 @@ def _evaluate_issue_lock_recovery(
|
||||
remote_branch_exists=remote_branch_exists,
|
||||
recorded_base_sha=recorded_base,
|
||||
base_ancestry=base_ancestry,
|
||||
sync_provenance=sync_provenance,
|
||||
)
|
||||
|
||||
|
||||
@@ -19091,10 +19106,59 @@ def gitea_update_pr_branch_by_merge(
|
||||
prepared_verdict_head_sha=live_pr_head,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
# #871: the remote head is now advanced; the durable linked-issue lock must
|
||||
# be advanced with it, or a later dead-session recovery can never prove
|
||||
# ownership at the new head. This runs AFTER the successful remote update, so
|
||||
# a failure here is a *partial* lifecycle failure — the remote moved but the
|
||||
# durable state did not — and must never be reported as a full success.
|
||||
claimant = _work_lease_claimant(h)
|
||||
matched_issue = ownership.get("matched_issue")
|
||||
synced_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
lock_refresh: dict = {
|
||||
"refreshed": False,
|
||||
"reasons": ["durable lock head refresh was not attempted"],
|
||||
}
|
||||
if new_head and matched_issue and source_branch and (wt or None):
|
||||
try:
|
||||
lock_refresh = issue_lock_store.apply_durable_lock_head_refresh(
|
||||
remote=remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
issue_number=int(matched_issue),
|
||||
branch_name=source_branch,
|
||||
worktree_path=wt,
|
||||
pr_number=pr_number,
|
||||
identity=claimant.get("username"),
|
||||
profile=claimant.get("profile"),
|
||||
current_pid=os.getpid(),
|
||||
expected_old_head=live_pr_head,
|
||||
new_head=new_head,
|
||||
synced_at=synced_at,
|
||||
base_head=live_base_head,
|
||||
)
|
||||
except Exception as exc:
|
||||
lock_refresh = {
|
||||
"refreshed": False,
|
||||
"reasons": [
|
||||
f"durable lock head refresh raised (fail closed): {_redact(str(exc))}"
|
||||
],
|
||||
}
|
||||
else:
|
||||
lock_refresh = {
|
||||
"refreshed": False,
|
||||
"reasons": [
|
||||
"durable lock head refresh could not run: missing new head, "
|
||||
"linked issue, source branch, or worktree binding"
|
||||
],
|
||||
}
|
||||
|
||||
durable_refreshed = bool(lock_refresh.get("refreshed"))
|
||||
base_result = {
|
||||
"performed": True,
|
||||
"mutation_allowed": True,
|
||||
"durable_lock_refreshed": durable_refreshed,
|
||||
"durable_lock_refresh": lock_refresh,
|
||||
"fully_synchronized": durable_refreshed,
|
||||
"style": "merge",
|
||||
"force_push": False,
|
||||
"rebase": False,
|
||||
@@ -19116,19 +19180,37 @@ def gitea_update_pr_branch_by_merge(
|
||||
"prepared_verdict_invalidated": transition.get(
|
||||
"prepared_verdict_invalidated"
|
||||
),
|
||||
"recommended_next_action": transition.get(
|
||||
"recommended_next_action",
|
||||
pr_sync_status.ACTION_FRESH_REVIEW_REQUIRED,
|
||||
),
|
||||
"transition": transition,
|
||||
"role_kind": role,
|
||||
"profile_name": profile.get("profile_name"),
|
||||
"worktree_path": wt or None,
|
||||
"reasons": list(transition.get("reasons") or []) + [
|
||||
"update-by-merge completed via native Gitea API (style=merge only)"
|
||||
],
|
||||
}
|
||||
|
||||
if durable_refreshed:
|
||||
base_result["success"] = True
|
||||
base_result["recommended_next_action"] = transition.get(
|
||||
"recommended_next_action",
|
||||
pr_sync_status.ACTION_FRESH_REVIEW_REQUIRED,
|
||||
)
|
||||
base_result["reasons"] = list(transition.get("reasons") or []) + [
|
||||
"update-by-merge completed via native Gitea API (style=merge only)",
|
||||
f"durable linked-issue lock #{matched_issue} head refreshed to "
|
||||
f"{new_head} (verified by read-after-write)",
|
||||
]
|
||||
return base_result
|
||||
|
||||
# Partial lifecycle failure: the remote advanced but the durable lock did
|
||||
# not. Do NOT report a fully successful synchronization (#871).
|
||||
base_result["success"] = False
|
||||
base_result["partial_lifecycle_failure"] = True
|
||||
base_result["recommended_next_action"] = pr_sync_status.ACTION_BLOCKED
|
||||
base_result["reasons"] = list(lock_refresh.get("reasons") or []) + [
|
||||
f"PARTIAL LIFECYCLE FAILURE: PR #{pr_number} remote head advanced to "
|
||||
f"{new_head} but the durable linked-issue lock head was not refreshed; "
|
||||
"the synchronization is NOT complete",
|
||||
]
|
||||
return base_result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_assess_conflict_fix_push(
|
||||
|
||||
Reference in New Issue
Block a user