#!/usr/bin/env python3 """Shared logic for project source headers (see HEADER.txt).""" from __future__ import annotations import hashlib import re import subprocess from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Iterable REPO_ROOT = Path(__file__).resolve().parents[1] HEADER_TEMPLATE = REPO_ROOT / "HEADER.txt" BANNER_START = "/*********************************************************************" BANNER_END = "**********************************************************************/" MARKER_CREATED = "Created at:" MARKER_UPDATED = "Updated at:" MARKER_DIGEST = "Digest:" MARKER_CONTRIBUTORS = "Contributors:" MARKER_CLASS = re.compile(r"^\s*\*?\s*(class|interface|enum)\s+(\w+)", re.I) ASSISTANT_NAME = "Cursor Agent" ASSISTANT_CONTRIBUTOR = ASSISTANT_NAME IDENTITY_IN_ANGLE = re.compile(r"^(.+?)\s*<([^>]+)>\s*$") IGNORED_GIT_AUTHORS = frozenset({"Not Committed Yet", "Unknown"}) SOURCE_SUFFIXES = {".java", ".aidl", ".c", ".cpp", ".h", ".php"} SKIP_DIR_NAMES = { ".git", ".gradle", "build", ".cxx", "third-party", "node_modules", ".idea", } EXCLUDED_PATH_MARKERS = frozenset({"third-party"}) def is_excluded_path(path: Path) -> bool: """Never apply headers under third-party/ or other vendor trees.""" resolved = path.resolve() root = REPO_ROOT.resolve() try: rel_parts = resolved.relative_to(root).parts except ValueError: return True if any(part in EXCLUDED_PATH_MARKERS for part in rel_parts): return True return any(part in SKIP_DIR_NAMES for part in rel_parts) @dataclass class FileMeta: rel_path: str filename: str package_line: str class_line: str created_at: str updated_at: str last_contributor: str contributors_block: str language: str def iter_source_files(root: Path) -> Iterable[Path]: for path in root.rglob("*"): if path.suffix.lower() not in SOURCE_SUFFIXES: continue if is_excluded_path(path): continue if path.is_file(): yield path _GLOBAL_IDENTITY_MAP: dict[str, str] | None = None def global_git_identity_map() -> dict[str, str]: """Repo-wide author -> email from git shortlog.""" global _GLOBAL_IDENTITY_MAP if _GLOBAL_IDENTITY_MAP is not None: return _GLOBAL_IDENTITY_MAP emails: dict[str, str] = {} out = run_git(["shortlog", "-sne", "--all"], REPO_ROOT) for line in out.splitlines(): line = line.strip() if not line: continue # "29\tAnton Afanasyeu " or "29 Anton ..." if "\t" in line: _, rest = line.split("\t", 1) else: parts = line.split(None, 1) rest = parts[1] if len(parts) > 1 else line name, mail = parse_identity(rest.strip()) if is_tracked_author(name) and mail: emails.setdefault(name, mail) _GLOBAL_IDENTITY_MAP = emails return emails def run_git(args: list[str], cwd: Path) -> str: try: out = subprocess.check_output( ["git", *args], cwd=cwd, stderr=subprocess.DEVNULL, text=True, ) return out.strip() except (subprocess.CalledProcessError, FileNotFoundError): return "" def normalize_email(raw: str) -> str: e = raw.strip() if e.startswith("<") and e.endswith(">"): e = e[1:-1].strip() if not e or e.lower() in {"unknown", "none", "n/a"}: return "" if "@" not in e: return "" return e def parse_identity(text: str) -> tuple[str, str]: """Split 'Name ' or plain name.""" t = text.strip() if not t: return "", "" m = IDENTITY_IN_ANGLE.match(t) if m: return m.group(1).strip(), normalize_email(m.group(2)) return t, "" def is_tracked_author(name: str) -> bool: n = name.strip() return bool(n) and n not in IGNORED_GIT_AUTHORS def format_identity(name: str, email: str = "") -> str: name = name.strip() email = normalize_email(email) if not name: return ASSISTANT_NAME if email: return f"{name} <{email}>" return name def format_git_date(raw: str) -> str: if not raw: return datetime.now().astimezone().strftime("%a %d %b %Y %H:%M:%S %z") try: # git %ai: 2026-05-20 15:04:12 +0300 dt = datetime.strptime(raw, "%Y-%m-%d %H:%M:%S %z") return dt.strftime("%a %d %b %Y %H:%M:%S %z") except ValueError: return raw def git_identity_map(path: Path, body: str = "") -> dict[str, str]: """Map git author name -> email (best effort per file).""" rel = repo_relative(path) emails: dict[str, str] = {} log = run_git(["log", "--follow", "--format=%an%x09%ae", "--", rel], REPO_ROOT) for line in log.splitlines(): if "\t" not in line: continue an, ae = line.split("\t", 1) an, ae = an.strip(), normalize_email(ae) if is_tracked_author(an) and ae: emails.setdefault(an, ae) blame = run_git(["blame", "--line-porcelain", "--", rel], REPO_ROOT) author_name: str | None = None for line in blame.splitlines(): if line.startswith("author "): author_name = line[7:].strip() elif line.startswith("author-mail "): mail = normalize_email(line[12:]) if is_tracked_author(author_name or "") and mail: emails.setdefault(author_name, mail) author_name = None elif line.startswith("\t"): author_name = None for tag in re.finditer( r"@author\s+(.+)$|/\*\s*@author\s+(.+?)\s*\*/", body, re.MULTILINE | re.IGNORECASE, ): fragment = (tag.group(1) or tag.group(2) or "").strip() name, mail = parse_identity(fragment) if name and mail: emails.setdefault(name, mail) for name, mail in global_git_identity_map().items(): emails.setdefault(name, mail) return emails def git_created_updated(path: Path, body: str = "") -> tuple[str, str, str]: rel = repo_relative(path) created_raw = run_git( ["log", "--follow", "--diff-filter=A", "--format=%ai", "-1", "--", rel], REPO_ROOT, ) updated_raw = run_git(["log", "-1", "--format=%ai", "--", rel], REPO_ROOT) last = run_git(["log", "-1", "--format=%an%x09%ae", "--", rel], REPO_ROOT) if "\t" in last: an, ae = last.split("\t", 1) author = format_identity(an, ae) elif last: id_map = git_identity_map(path, body) author = format_identity(last, id_map.get(last, "")) else: author = ASSISTANT_NAME return ( format_git_date(created_raw), format_git_date(updated_raw or created_raw), author, ) def git_config_identity() -> tuple[str, str]: name = run_git(["config", "user.name"], REPO_ROOT) email = run_git(["config", "user.email"], REPO_ROOT) return name, email def git_config_identity_formatted() -> str: name, email = git_config_identity() if name: return format_identity(name, email) return ASSISTANT_NAME def resolve_header_contributor() -> str: """ Pre-commit / env contributor: HEADER_CONTRIBUTOR or HEADER_CONTRIBUTOR_EMAIL enriched with git user.email when the name has no address. """ import os raw = os.environ.get("HEADER_CONTRIBUTOR", "").strip() email_extra = normalize_email(os.environ.get("HEADER_CONTRIBUTOR_EMAIL", "")) cfg_name, cfg_email = git_config_identity() if raw: name, email = parse_identity(raw) if not name: name = raw if not email: email = email_extra or cfg_email or global_git_identity_map().get(name, "") return format_identity(name, email) if email_extra and cfg_name: return format_identity(cfg_name, email_extra) return git_config_identity_formatted() def git_contributors(path: Path, body_line_count: int, body: str = "") -> list[tuple[str, int, int]]: rel = repo_relative(path) id_map = git_identity_map(path, body) commit_counts: dict[str, int] = {} log = run_git(["log", "--follow", "--format=%an", "--", rel], REPO_ROOT) for line in log.splitlines(): name = line.strip() if is_tracked_author(name): commit_counts[name] = commit_counts.get(name, 0) + 1 line_counts: dict[str, int] = {} blame = run_git(["blame", "--line-porcelain", "--", rel], REPO_ROOT) author: str | None = None for line in blame.splitlines(): if line.startswith("author "): author = line[7:].strip() elif line.startswith("\t") and author and is_tracked_author(author): line_counts[author] = line_counts.get(author, 0) + 1 names = {n for n in (set(commit_counts) | set(line_counts)) if is_tracked_author(n)} rows: list[tuple[str, int, int]] = [] for name in names: label = format_identity(name, id_map.get(name, "")) rows.append((label, commit_counts.get(name, 0), line_counts.get(name, 0))) rows.sort(key=lambda r: (-r[1], -r[2], r[0].lower())) if not rows: rows = [(ASSISTANT_NAME, 0, body_line_count)] return rows def contributors_block(rows: list[tuple[str, int, int]], include_assistant: bool = True) -> str: lines = [] for label, commits, nlines in rows: name, _ = parse_identity(label) if name == ASSISTANT_NAME: continue lines.append(f" * - {label} ({commits} commits, {nlines} lines)") if include_assistant: lines.append(f" * - {ASSISTANT_NAME} (project assistant)") return "\n".join(lines) def detect_class_line(body: str, suffix: str) -> str: if suffix not in {".java", ".aidl", ".cpp", ".php"}: return "" patterns = [ ("class", r"^\s*public\s+(?:final\s+)?class\s+(\w+)"), ("class", r"^\s*class\s+(\w+)"), ("interface", r"^\s*public\s+interface\s+(\w+)"), ("interface", r"^\s*interface\s+(\w+)"), ("enum", r"^\s*public\s+enum\s+(\w+)"), ("enum", r"^\s*enum\s+(\w+)"), ] for line in body.splitlines(): for kind, pat in patterns: m = re.match(pat, line) if m: return f" * {kind} {m.group(1)}" return "" def package_line_for_file(source_text: str, rel_path: str, suffix: str) -> str: for line in source_text.splitlines(): if suffix in {".java", ".aidl"}: m = re.match(r"^\s*package\s+([\w.]+)\s*;", line) if m: return f"package {m.group(1)};" return f"/* package {rel_path} */" def strip_existing_header(text: str, suffix: str) -> tuple[str, str]: """Returns (body without header/package, preserved leading e.g. = 0: leading = work[: first_nl + 1] work = work[first_nl + 1 :] else: leading = work + "\n" work = "" lines = work.splitlines(keepends=True) i = 0 if i < len(lines) and ( lines[i].startswith("package ") or lines[i].startswith("/* package ") ): i += 1 if i < len(lines) and lines[i].strip() == "": i += 1 if i < len(lines) and BANNER_START in lines[i]: while i < len(lines) and BANNER_END not in lines[i]: i += 1 if i < len(lines): i += 1 if i < len(lines) and lines[i].strip() == "": i += 1 body = "".join(lines[i:]).lstrip("\n") if body.startswith("package "): first_nl = body.find("\n") if first_nl >= 0: body = body[first_nl + 1 :].lstrip("\n") return body, leading def repo_relative(path: Path) -> str: root = REPO_ROOT.resolve() try: return path.resolve().relative_to(root).as_posix() except ValueError: return path.name def build_meta(path: Path, body: str, source_text: str = "") -> FileMeta: rel = repo_relative(path) suffix = path.suffix.lower() full_source = source_text if source_text else body created, updated, author = git_created_updated(path, body) rows = git_contributors(path, max(1, len(body.splitlines())), body) if not any(parse_identity(r[0])[0] == ASSISTANT_NAME for r in rows): rows.append((ASSISTANT_NAME, 0, 0)) pkg = package_line_for_file(full_source, rel, suffix) lang = "java" if suffix in {".java", ".aidl"} else ("php" if suffix == ".php" else "c") class_line = detect_class_line(body, suffix) return FileMeta( rel_path=rel, filename=path.name, package_line=pkg, class_line=class_line, created_at=created, updated_at=updated, last_contributor=author, contributors_block=contributors_block(rows), language=lang, ) def render_header(meta: FileMeta, digest_hex: str | None) -> str: lines = [ meta.package_line, "", BANNER_START, f" * {meta.filename}", f" * {MARKER_CREATED} {meta.created_at}", f" * {MARKER_UPDATED} {meta.updated_at} by {meta.last_contributor}", ] if meta.class_line: lines.append(meta.class_line) lines.append(f" * {MARKER_CONTRIBUTORS}") lines.append(meta.contributors_block) if digest_hex: lines.append(f" * {MARKER_DIGEST} SHA256\t{digest_hex}") lines.append(BANNER_END) lines.append("") return "\n".join(lines) def compute_digest(prefix: str, body: str) -> str: payload = prefix + body return hashlib.sha256(payload.encode("utf-8")).hexdigest() def apply_header_to_content(path: Path, text: str, update_only: bool = False) -> str: suffix = path.suffix.lower() body, leading = strip_existing_header(text, suffix) meta = build_meta(path, body, text) if update_only and MARKER_CREATED in text: m = re.search(r"\* Created at: (.+)", text) if m: meta.created_at = m.group(1).strip() prefix_without_digest = render_header(meta, None) digest = compute_digest(prefix_without_digest, body) prefix = render_header(meta, digest) return leading + prefix + body def has_valid_header(text: str) -> bool: return MARKER_CREATED in text and BANNER_START in text and BANNER_END in text def header_needs_identity_refresh(text: str, path: Path) -> bool: """True when a known author appears without email but git has one.""" if not has_valid_header(text): return False suffix = path.suffix.lower() body, _ = strip_existing_header(text, suffix) id_map = git_identity_map(path, body) updated = re.search(r"\* Updated at: .+ by (.+)$", text, re.MULTILINE) if updated: name, email = parse_identity(updated.group(1).strip()) if name != ASSISTANT_NAME and not email and id_map.get(name): return True for line in text.splitlines(): stripped = line.strip() if not stripped.startswith("* - "): continue label = stripped[7:].split(" (", 1)[0].strip() name, email = parse_identity(label) if name != ASSISTANT_NAME and not email and id_map.get(name): return True return False def update_header_timestamps(text: str, path: Path, contributor: str | None = None) -> str: suffix = path.suffix.lower() body, leading = strip_existing_header(text, suffix) if contributor: who = contributor else: who = resolve_header_contributor() if who == ASSISTANT_NAME: who = git_created_updated(path, body)[2] updated_line = datetime.now().astimezone().strftime("%a %d %b %Y %H:%M:%S %z") m_created = re.search(r"\* Created at: (.+)", text) created = m_created.group(1).strip() if m_created else git_created_updated(path)[0] meta = build_meta(path, body, text) meta.created_at = created meta.updated_at = updated_line meta.last_contributor = who prefix_without_digest = render_header(meta, None) digest = compute_digest(prefix_without_digest, body) prefix = render_header(meta, digest) return leading + prefix + body