mehen diff compares metrics between two git revisions and emits a per-file delta report. It is the
engine behind the GitHub Action and the recommended way to wire mehen into a
PR workflow.
Examples
- PR head vs main (Markdown)
- JSON for a CI script
- Custom metrics + polarity
- Strict gate on docs
Output formats
- Markdown
- JSON
GitHub-flavored Markdown table optimized for sticky PR comments. This is what the
GitHub Action posts on each PR.
What gets compared
mehen asks Git for the changed-file set, runs the equivalent ofmehen metrics against each side,
joins by repository-relative path, and computes per-metric deltas. It does not recursively walk
untracked working-tree content, so ignored build and dependency directories are never candidates.
Files that exist on only one side are reported as new or deleted. Tracked files matched by
linguist-generated, linguist-vendored, or binary Git attributes are skipped by default.
Added and modified files use attributes from the requested head revision; deleted files use the
base revision where they still exist. Revision filtering reads committed .gitattributes files
only, so checkout-local .git/info/attributes and configured global attributes cannot change a
historical report. Use --ignore-git-attributes=false to include attributed files.
When no --metrics are given, a mixed pull request (e.g. both .ts and .sql
files) produces a single table whose columns are the union of each language’s
default metrics. Every row populates only the columns its analyzer publishes;
columns from another language render as –. This keeps SQL changes visible in
the default GitHub Action path — without it, SQL files would be measured only
against source-code metrics they never emit and silently drop out as unchanged.
The source-code default columns follow a one-column-per-dimension design:
Cognitive (control-flow understandability), ABC (computational volume),
MI (the one deliberate composite rollup), Hotspot
(history.hotspot = cognitive complexity × commit frequency — is this a
fragile, frequently-touched file?), and Churn (history.churn.relative —
how much of the file’s full repository history has moved, normalized by its
size). The two history columns are computed by walking the git history of each
compared revision — see the history metric family. All columns are review-prioritization signals, not quality
verdicts: a rising number flags where to look, not that the code is bad.
Coverage trend columns
mehen diff folds test coverage into both sides of the comparison:
- Head side —
--coveragewith the exact flag semantics ofmehen metricsandmehen top-offenders(explicit paths hard-error when missing; discovered reports degrade to warnings). - Base side —
--base-coverage=<PATH>, explicit paths only. The diff re-analyzes the base revision’s blobs, so a report generated at the base revision lines up with the recomputed base spans without any line mapping. mehen never retrieves reports itself (it stays network-free); a CI layer such as the GitHub Action fetches the base report and hands over plain paths.
--metrics list is given, a Coverage
column (the coverage.line rate — the one dimension every supported format measures) joins the
default columns. With explicit --metrics, select coverage columns yourself (coverage.line,
coverage.branch, coverage.function, or their .covered/.total counters).
The measured-or-absent doctrine extends to the diff:
A base report whose file modification time predates the base commit itself cannot describe that
commit’s code (recency-based retrieval fallbacks land exactly here); mehen warns on stderr, and the
warning honors the same
stale-warning key under [coverage] in mehen.toml.
Configured coverage.* thresholds gate the head side like every other metric this diff reports,
and a configured coverage threshold is itself a lazy ingestion trigger — no flag needed.
--fail-on thresholds
--fail-on accepts a comma-separated list of band-crossing rule IDs:
The rules align with the severity-1 / severity-2 indicators on the
PR comment design page.
Analysis diagnostics
A parser or analyzer can reject one side of one file without invalidating the whole comparison.mehen diff records the affected path, side (base or head), severity, reason code, and message
under the top-level JSON analysis_errors array. Markdown output adds the same information in an
Analysis diagnostics table.
Static metrics from an error- or fatal-severity side are withheld rather than computed from a
partial syntax tree. Parser-independent measurements such as repository history remain available,
and parseable files continue through normal metric and threshold evaluation. The command exits 0
by default when diagnostics are the only issue. Add --fail-on-analysis-error when incomplete
analysis should remain a strict CI gate; the complete report is still emitted before exit 1.
Configured thresholds
When amehen.toml is present, the head side of every changed source-code
file is checked against the configured limits for the metrics this diff reports (the explicit
--metrics list, or the per-language defaults). Evaluation happens before the unchanged-row
filter, so an unchanged-but-over-limit metric still fails the gate even when its row is hidden.
Deleted files have no head side to gate; Markdown files stay on the --fail-on documentation
gates above. Crossed limits print a grouped report on stderr — after the Markdown or JSON output,
which stays complete for machine consumers — and the command exits 1.
For delta gating (e.g. “fail if the cyclomatic delta exceeds 5”), use the
GitHub Action’s thresholds input, which post-processes the JSON report.
See Concepts → Thresholds and diffs for how the gates compose.
Exit codes
See also
- GitHub Action — runs
mehen difffor you and posts the report on PRs. - PR comment design — Markdown comment template.
- Concepts → Thresholds and diffs.