---
title: "mehen diff"
description: "Compare metrics between two git revisions and emit a per-file delta report as JSON or GitHub-flavored Markdown."
keywords: ["mehen diff", "metric delta", "git diff", "pull request"]
---

`mehen diff` compares metrics between two git revisions and emits a per-file delta report. It is the
engine behind the [GitHub Action](/guides/github-action) and the recommended way to wire mehen into a
PR workflow.

```text
mehen diff [OPTIONS]
```

| Flag | Description |
|---|---|
| `--from <FROM>` | Base revision to compare from. |
| `--to <TO>` | Head revision to compare to. |
| `-p, --paths <PATHS>...` | Repository-relative files or directories to compare. |
| `-M, --metrics <METRICS>` | Comma-separated metrics. When omitted, defaults are resolved **per file language**: source-code files use `cognitive,abc,mi.visual_studio,history.hotspot,history.churn.relative`; SQL files use `sql.change_risk_score,sql.maintainability_index,sql.review_burden_index,sql.cognitive_complexity,sql.loc.code`. An explicit list applies to every source-code file; Markdown files always use the [documentation pipeline](/guides/pr-comment-design) with its own fixed columns. Prefix with `+` for higher-is-better, `-` for lower-is-better. `history.*` metrics (including two of the defaults) walk the repository history of both revisions, so they need a full clone (`fetch-depth: 0`). |
| `-I, --include <INCLUDE>...` | Glob to include files. |
| `-X, --exclude <EXCLUDE>...` | Glob to exclude files. |
| `-O, --output-format <OUTPUT_FORMAT>` | `markdown` or `json`. |
| `--show-unchanged` | Show files where every metric is unchanged. |
| `--ignore-git-attributes[=<BOOL>]` | Skip files marked `linguist-generated`, `linguist-vendored`, or `binary` (default `true`; `--ignore-generated` remains an alias). |
| `--fail-on <FAIL_ON>` | Exit non-zero when named thresholds are crossed. Comma-separated: `dmi-drop`, `new-broken-link`, `filler-high`, `all`. |
| `--config <PATH>` | Pin a [configuration file](/configuration) instead of discovering `mehen.toml` upward from the working directory. |

## Examples

<Tabs>
  <Tab title="PR head vs main (Markdown)">
    ```bash
    mehen diff --from main --to HEAD --paths src --output-format markdown
    ```
  </Tab>
  <Tab title="JSON for a CI script">
    ```bash
    mehen diff --from "$BASE_SHA" --to "$HEAD_SHA" --paths . \
               --output-format json > metrics-diff.json
    ```
  </Tab>
  <Tab title="Custom metrics + polarity">
    ```bash
    mehen diff --from main --to HEAD --paths src \
               --metrics "+mi.visual_studio,-cognitive,-loc.lloc"
    ```
  </Tab>
  <Tab title="Strict gate on docs">
    ```bash
    mehen diff --from main --to HEAD --paths docs \
               --fail-on dmi-drop,new-broken-link,filler-high
    ```
  </Tab>
</Tabs>

## Output formats

<Tabs>
  <Tab title="Markdown">
    GitHub-flavored Markdown table optimized for sticky PR comments. This is what the
    [GitHub Action](/guides/github-action) posts on each PR.
  </Tab>
  <Tab title="JSON">
    Per-file deltas with both old and new values for every metric. Designed to be parsed by CI scripts
    that want their own threshold logic.
  </Tab>
</Tabs>

## What gets compared

mehen asks Git for the changed-file set, runs the equivalent of `mehen 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`](/metrics/history/hotspot) = cognitive complexity × commit frequency — is this a
fragile, frequently-touched file?), and **Churn** ([`history.churn.relative`](/metrics/history/churn) —
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](/metrics/history/overview). All columns are review-prioritization signals, not quality
verdicts: a rising number flags *where to look*, not *that the code is bad*.

## `--fail-on` thresholds

`--fail-on` accepts a comma-separated list of band-crossing rule IDs:

| Rule | Triggers when… |
|---|---|
| `dmi-drop` | A Markdown file's [DMI](/metrics/markdown/dmi) crosses a band downward. |
| `new-broken-link` | At least one new broken link is added. |
| `filler-high` | A Markdown file's [Filler / Lazy Risk](/metrics/markdown/filler-lazy-risk) is `≥ 0.60`. |
| `all` | Any of the above. |

The rules align with the severity-1 / severity-2 indicators on the
[PR comment design](/guides/pr-comment-design) page.

## Configured thresholds

When a [`mehen.toml`](/configuration) 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](/guides/github-action), which post-processes the JSON report.
See [Concepts → Thresholds and diffs](/concepts/thresholds-and-diffs) for how the gates compose.

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Success — the comment is advisory. |
| 1 | IO, git, parser-fatal, or unsupported-language error. Also: a [configured metric threshold](/configuration) crossed, after the report is emitted. |
| 2 | One or more `--fail-on` rules crossed (gating exit). |

## See also

- [GitHub Action](/guides/github-action) — runs `mehen diff` for you and posts the report on PRs.
- [PR comment design](/guides/pr-comment-design) — Markdown comment template.
- [Concepts → Thresholds and diffs](/concepts/thresholds-and-diffs).
