"""Non-mergeable PR skip conflict-proof verifier for reviewer reports (#322).""" from __future__ import annotations import re from typing import Any _FULL_SHA = re.compile(r"\b[0-9a-f]{40}\b", re.IGNORECASE) _SHORT_SHA = re.compile(r"\b[0-9a-f]{7,39}\b", re.IGNORECASE) _PR_NUMBER_RE = re.compile(r"(?:\bPR\s*#?|#)(\d+)\b", re.IGNORECASE) _MERGEABILITY_FALSE_RE = re.compile( r"(?:mergeable\s*:\s*false|not mergeable|non[- ]mergeable|mergeability\s*:\s*false|" r"mergeability result\s*:\s*false)", re.IGNORECASE, ) _CONFLICT_PROOF_RE = re.compile( r"(?:merge-tree|git merge-tree|gitea_view_pr|gitea_check_pr_eligibility|" r"conflict proof|mergeability tool|merge simulation|conflicting files?)", re.IGNORECASE, ) _CONFLICTING_FILES_RE = re.compile( r"(?:conflicting files?|conflict files?)\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE, ) _HEAD_CHANGED_RE = re.compile( r"(?:head (?:changed|unchanged)|head sha (?:changed|unchanged)|" r"head changed since|head unchanged since)", re.IGNORECASE, ) _MERGEABILITY_UNVERIFIED_RE = re.compile( r"\bMERGEABILITY_UNVERIFIED\b", ) def _pr_section(text: str, pr_number: int) -> str: """Return report lines likely describing one skipped PR.""" lines = (text or "").splitlines() chunks: list[str] = [] capture = False token = f"#{pr_number}" for line in lines: lower = line.lower() if token in lower or f"pr {pr_number}" in lower or f"pr#{pr_number}" in lower.replace(" ", ""): capture = True chunks.append(line) continue if capture: if _PR_NUMBER_RE.search(line) and token not in line.lower(): break if line.strip() == "" and len(chunks) > 3: break chunks.append(line) if chunks: return "\n".join(chunks) return text or "" def _has_head_sha(text: str, head_sha: str | None) -> bool: if not head_sha: return bool(_FULL_SHA.search(text) or _SHORT_SHA.search(text)) head = head_sha.strip().lower() if head in text.lower(): return True if len(head) >= 7 and head[:7] in text.lower(): return True return bool(_FULL_SHA.search(text)) def assess_non_mergeable_skip_proof( report_text: str, *, skipped_prs: list[dict] | None = None, ) -> dict[str, Any]: """Validate skipped non-mergeable PR documentation in reviewer reports (#322).""" text = report_text or "" reasons: list[str] = [] assessments: list[dict[str, Any]] = [] for entry in skipped_prs or []: pr_number = entry.get("pr_number") if pr_number is None: reasons.append("skipped PR entry missing pr_number") continue pr_number = int(pr_number) section = _pr_section(text, pr_number) mergeable = entry.get("mergeable") verified = entry.get("mergeability_verified") classification = (entry.get("classification") or "").strip().upper() head_sha = (entry.get("head_sha") or "").strip() or None item_reasons: list[str] = [] if f"#{pr_number}" not in text.lower() and f"pr {pr_number}" not in text.lower(): item_reasons.append(f"skipped PR #{pr_number} not documented in final report") if mergeable is False or verified is False: if not _MERGEABILITY_UNVERIFIED_RE.search(section) and verified is False: if "MERGEABILITY_UNVERIFIED" not in classification: item_reasons.append( f"PR #{pr_number} conflict proof unavailable; report must classify " "MERGEABILITY_UNVERIFIED" ) elif verified is not False: if not _MERGEABILITY_FALSE_RE.search(section): item_reasons.append( f"PR #{pr_number} skip missing mergeability:false proof" ) if not _has_head_sha(section, head_sha): item_reasons.append( f"PR #{pr_number} skip missing current head SHA" ) if not _CONFLICT_PROOF_RE.search(section): item_reasons.append( f"PR #{pr_number} skip missing conflict proof command/tool" ) if entry.get("head_changed_since_prior_blocker") is not None: if not _HEAD_CHANGED_RE.search(section): item_reasons.append( f"PR #{pr_number} skip missing head-changed-since-blocker proof" ) if ( entry.get("conflicting_files") and not _CONFLICTING_FILES_RE.search(section) and not any( path.lower() in section.lower() for path in (entry.get("conflicting_files") or []) ) ): item_reasons.append( f"PR #{pr_number} has conflicting files in session proof but " "report omits file-level conflict proof" ) proven = not item_reasons assessments.append({ "pr_number": pr_number, "proven": proven, "classification": classification or ( "MERGEABILITY_UNVERIFIED" if verified is False else "NON_MERGEABLE_SKIPPED" ), "reasons": item_reasons, }) reasons.extend(item_reasons) proven = not reasons return { "proven": proven, "block": not proven, "downgraded": False, "assessments": assessments, "reasons": reasons, "safe_next_action": ( "document each skipped non-mergeable PR with head SHA, mergeability result, " "conflict proof command, conflicting files, and head-change status" if reasons else "proceed" ), }