"""The PKGBUILD audit screen: what changed, why it matters, and the diff itself. The semantic summary comes first because a line diff answers the wrong question. "pkgver 1.19 -> 1.19, source +1 -1, sums +1 -1" tells you what kind of change it was; the rendered diff underneath tells you whether you agree with it. """ from __future__ import annotations import difflib from rich.text import Text from ..audit import Report, Severity from ..icons import Glyphs from ..theme import Theme from .base import Columns, Screen, truncate SEVERITY_ROLE = {Severity.INFO: "muted", Severity.LOOK: "warn", Severity.BLOCK: "alert"} def summary_parts(report: Report) -> list[tuple[str, str]]: """(text, role) pairs describing the change in PKGBUILD terms.""" d = report.diff parts: list[tuple[str, str]] = [] def field(label: str, value: str, role: str = "text") -> None: parts.append((f"{label} {value}", role)) if d.pkgver: field("pkgver", f"{d.pkgver[0]} → {d.pkgver[1]}", "new") if d.pkgrel: field("pkgrel", f"{d.pkgrel[0]} → {d.pkgrel[1]}", "new") if d.epoch: field("epoch", f"{d.epoch[0]} → {d.epoch[1]}", "warn") if d.source: field("source", d.source.summary(), "warn") if d.sums: field("sums", d.sums.summary(), "warn") if d.depends: field("depends", d.depends.summary(), "warn") if d.makedepends: field("makedepends", d.makedepends.summary(), "warn") if d.provides: field("provides", d.provides.summary(), "warn") if d.validpgpkeys: field("pgpkeys", d.validpgpkeys.summary(), "alert") if d.funcs_added: field("functions added", ", ".join(d.funcs_added), "warn") if d.funcs_changed: field("functions changed", ", ".join(d.funcs_changed), "text") if d.lines_added or d.lines_removed: field("PKGBUILD", f"+{d.lines_added} -{d.lines_removed}", "text") if not parts: parts.append(("no metadata changed", "muted")) return parts def diff_lines(old: str, new: str, *, context: int = 3) -> list[tuple[str, str, str]]: """(marker, line_number, text) for a unified diff of two PKGBUILD texts.""" out: list[tuple[str, str, str]] = [] old_lines = old.splitlines() new_lines = new.splitlines() matcher = difflib.SequenceMatcher(None, old_lines, new_lines) for group in matcher.get_grouped_opcodes(context): if out: out.append((" ", "", "")) for tag, i1, i2, j1, j2 in group: if tag == "equal": for offset, line in enumerate(old_lines[i1:i2]): out.append((" ", str(i1 + offset + 1), line)) else: for offset, line in enumerate(old_lines[i1:i2]): out.append(("-", str(i1 + offset + 1), line)) for offset, line in enumerate(new_lines[j1:j2]): out.append(("+", str(j1 + offset + 1), line)) return out def build( report: Report, *, theme: Theme, glyphs: Glyphs, width: int = 100, title: str = "", old_text: str = "", new_text: str = "", limit: int = 40, baseline: bool = True, into: Screen | None = None, ) -> list[Text]: screen = into or Screen(theme=theme, glyphs=glyphs, columns=Columns.for_width(width)) cols = screen.columns screen.header(title or f"{report.package} · AUR") role_of = SEVERITY_ROLE loud = len(report.loud) screen.section( "Findings", f"{len(report.findings)} item(s)" + (f" · {loud} need a look" if loud else " · nothing unusual"), ) for finding in report.findings: role = role_of[finding.severity] row = screen.row() row.span(f"{finding.severity.glyph:<2}", getattr(theme, role)) row.span(" ") row.span( truncate(finding.title, cols.ver - row.at - 2), getattr(theme, role) if finding.severity is not Severity.INFO else theme.muted, ) if finding.where: row.right(truncate(finding.where, 20), theme.muted) screen.finish(row) if finding.evidence: line = screen.row(indent=cols.indent + 4) line.span(truncate(finding.evidence, cols.right - line.at), theme.ver) screen.finish(line) screen.section("Change", "parsed statically · PKGBUILD never sourced") if not baseline: # an all-additions diff would be technically true and completely useless: # it would look like the whole PKGBUILD was rewritten when in fact we have # simply never recorded a version to compare against screen.add( screen.row().span("no baseline recorded for this package yet", theme.muted).done() ) screen.add( screen.row() .span("it will be recorded after the next successful build", theme.muted) .done() ) parts = summary_parts(report) if baseline else [] row = screen.row() for text, role in parts: chunk = text + " " if row.at + len(chunk) > cols.right: screen.finish(row) row = screen.row() row.span(chunk, getattr(theme, role) if hasattr(theme, role) else theme.text) screen.finish(row) if baseline and (old_text or new_text): lines = diff_lines(old_text, new_text) shown = lines[:limit] screen.section( "Diff", f"PKGBUILD · +{report.diff.lines_added} -{report.diff.lines_removed}" ) for marker, number, body in shown: role = {"+": "ok", "-": "err"}.get(marker, "muted") line = screen.row() line.span(marker, getattr(theme, role)) line.span(" ") line.span(f"{number:>4}", theme.muted) line.span(" ") line.span(truncate(body, cols.right - line.at), getattr(theme, role)) screen.finish(line) if len(lines) > len(shown): screen.add( screen.row() .span(f"+ {len(lines) - len(shown)} more diff lines", theme.muted) .done() ) elif new_text: # show it as a reference listing, with no +/- implying a comparison body_lines = new_text.splitlines() shown = body_lines[:limit] screen.section("PKGBUILD", f"{len(body_lines)} lines · shown as reference") for offset, body in enumerate(shown, start=1): line = screen.row() line.span(" ", theme.rule) line.span(f"{offset:>4}", theme.muted) line.span(" ") line.span(truncate(body, cols.right - line.at), theme.ver) screen.finish(line) if len(body_lines) > len(shown): screen.add( screen.row() .span(f"+ {len(body_lines) - len(shown)} more lines", theme.muted) .done() ) screen.hint("", "[o] open in delta [a] accept [s] skip [q] abort") return screen.render()