#!/usr/bin/env python3 """Preview or import inline TODO-style markers as Gitea issues.""" from __future__ import annotations import argparse import hashlib import json import os import pathlib import re import subprocess import sys from typing import Any from gitea_common import GiteaClient, GiteaError, infer_target, label_ids_by_name, load_dotenv, repo_path, repo_root, require_token MARKER_RE = re.compile( r"(?P#|//|/\*|\*|--|", "", "Imported from an inline marker.", "", f"- Repository: `{owner}/{repo}`", f"- Location: `{marker.location}`", f"- Marker: `{marker.marker}`", f"- Context: `{marker.context or 'none'}`", "", "Current line:", "", "```text", marker.raw, "```", "", "Migration rule:", "", "- Keep this Gitea issue as the canonical backlog item.", "- When touching this code, remove the inline marker or replace it with a short issue reference.", ] ) return IssuePreview(title=title, body=body, labels=labels, fingerprint=fingerprint, location=marker.location) def clean_marker_text(text: str) -> str: cleaned = text.strip() cleaned = re.sub(r"\s*(?:#|//|/\*|\*|--)\s*$", "", cleaned).strip() cleaned = re.sub(r"\s*\*/\s*$", "", cleaned).strip() return cleaned def default_labels(marker: Marker, *, module_label: str, infer_areas: bool) -> list[str]: labels = ["source/todo-scan", "status/triage", module_label, marker_type_label(marker.marker)] area = infer_area_label(marker.path) if infer_areas else "" if area: labels.append(area) return labels def marker_type_label(marker: str) -> str: if marker == "FIXME": return "type/bug" if marker in {"HACK", "XXX"}: return "type/debt" return "type/task" def infer_area_label(path: pathlib.Path) -> str: text = path.as_posix() if text.startswith("webui/"): return "area/webui" if text.startswith("alembic/"): return "area/migrations" if text.startswith("docs/"): return "area/docs" if text.startswith("scripts/"): return "area/devex" if "/rbac" in text or "permission" in text: return "area/rbac" if "/access" in text or "auth" in text or "session" in text: return "area/auth" if "tenant" in text: return "area/tenancy" if "governance" in text or "privacy" in text or "audit" in text or "retention" in text: return "area/governance" if "module" in text: return "area/module-system" if "migration" in text: return "area/migrations" if "router" in text or "api" in text: return "area/api" if "db" in text or "model" in text or "persistence" in text: return "area/db" return "" def infer_module_label(root: pathlib.Path) -> str: known = { "govoplan-core": "module/core", "govoplan-access": "module/access", "govoplan-admin": "module/admin", "govoplan-tenancy": "module/tenancy", "govoplan-policy": "module/policy", "govoplan-audit": "module/audit", "govoplan-mail": "module/mail", "govoplan-files": "module/files", "govoplan-campaign": "module/campaign", "govoplan-web": "module/web", } if root.name in known: return known[root.name] if root.name.startswith("govoplan-"): suffix = root.name.removeprefix("govoplan-") if suffix: return f"module/{suffix}" return "module/core" def marker_fingerprint(owner: str, repo: str, marker: Marker) -> str: stable = "\n".join([owner, repo, marker.path.as_posix(), marker.marker, marker.context, marker.text]) return hashlib.sha256(stable.encode("utf-8")).hexdigest()[:24] def truncate_title(title: str, limit: int = 180) -> str: if len(title) <= limit: return title return f"{title[: limit - 1].rstrip()}..." def load_label_ids(client: GiteaClient, owner: str, repo: str) -> dict[str, int]: return label_ids_by_name(client, owner, repo) def load_existing_fingerprints(client: GiteaClient, owner: str, repo: str) -> set[str]: issues = client.paginate( repo_path(owner, repo, "/issues"), query={"state": "all", "labels": "source/todo-scan"}, ) fingerprints: set[str] = set() for issue in issues: body = str(issue.get("body") or "") for match in re.finditer(r"(?:codex|govoplan)-todo-fingerprint:([0-9a-f]{24})", body): fingerprints.add(match.group(1)) return fingerprints def _already_linked(context: str, raw_line: str) -> bool: linked_text = f"{context} {raw_line}".lower() return "gitea#" in linked_text or "issue#" in linked_text or re.search(r"#\d+", linked_text) is not None def _looks_like_marker(match: re.Match[str]) -> bool: text = (match.group("text") or "").strip() return bool(match.group("context") or match.group("colon") or (match.group("prefix") and text)) if __name__ == "__main__": raise SystemExit(main())