patchcov
Patch coverage for git diffs. patchcov attributes a per-line coverage report to a
git diff and tells you what share of the lines a change added are covered, which
new lines are not, and whether anything moved on code the change did not touch.
- Reads lcov, llvm-cov JSON, Cobertura, JaCoCo XML and Go coverprofiles, detected
from the content. Branch-aware scoring
(
--branch-coverage) reads lcov and Cobertura only and fails explicitly on the other formats. - Reports patch coverage, the uncovered new lines, per-file project deltas and indirect changes, as a markdown PR comment, YAML or JSON.
- Gates a branch with
--fail-under-patchand--fail-under-lines. - Merges the reports of a sharded CI run into one file, or takes them directly.
- Silences known-flaky regions with source markers, and files with a repo-level ignore list, without hiding real coverage.
What it prints
patchcov diff writes the pull-request comment as markdown to stdout. This one is for a
change that adds 12 lines to src/parser.rs and a new src/cache.rs, with a baseline
report so the per-file table appears:
Coverage
Total: 85.42% 🔴 -14.58 pp vs
main
File Before After Δ src/cache.rs— 66.67% 🆕 new src/parser.rs100% 84.38% 🔴 -15.63 pp Patch coverage
Patch: 61.11% (11/18 new lines covered)
File Patch Uncovered new lines src/cache.rs66.67% (4/6) 5-6 src/parser.rs58.33% (7/12) 28-32
src/cache.rs:5src/cache.rs:6src/parser.rs:28src/parser.rs:29src/parser.rs:30src/parser.rs:31src/parser.rs:32Full per-file summary is attached as the coverage-summary build artifact.
Without --baseline-report there is no delta on the Total line, no per-file table and no
indirect changes; a "No baseline available yet" notice stands in for them and the patch
section is unchanged. With -o json or -o yaml the same result is structured
data, in the shape described in the output schema:
Both are the output of docs/examples/sample.sh, which builds a
tiny git repository and runs the command, so you can reproduce them.
Mission
Help projects that use agentic workflows keep their new code properly tested, with minimal friction: a precise, machine-readable answer to "what did this change leave uncovered, and where?", in a gate that depends on nothing but your own CI. The result should not need a human to interpret it: an agent or a CI step can act on the exit code and the
file:linelist, and loop until the gate passes.
The aim is adequately tested new code, found and fixed quickly, not a coverage number for its own sake: patchcov measures that lines ran, not that tests assert anything (see the end of When to choose patchcov).
Why patchcov
patchcov does one job: it tells you, precisely and repeatably, whether a change is covered by tests. It is built to be a clear signal, not a dashboard.
- A precise signal. The headline number is the share of added lines that are
covered, with the uncovered ones listed as
file:line. Whoever or whatever reads it (a reviewer, a script, an AI agent) knows what to do next without interpreting charts or trends. - Machine-readable and gateable. JSON and YAML output with a
documented schema, and exit codes from
--fail-under-patchand--fail-under-lines, make it easy to automate, including in a loop where an agent adds tests until the gate passes. - No service in the loop, so no outage to block you. It reads report files from disk and sends nothing anywhere: no account, upload token or server to be down, slow or rate-limited. It runs on the same runners as your other CI jobs, so the coverage gate is available whenever your CI is, and a hosted quality server going down for days cannot block merges. It runs the same on a laptop or in a sandbox, so a coverage check is a command you can run before you push, not something you learn about afterwards.
- Quiet by construction. Coverage that flaps between runs (a region gated on a
runtime CPU feature, say) is the usual source of phantom regressions. Per-file
scoping, a tolerance on the headline delta and
toleratesource markers keep that noise from reading as a regression while the reported numbers stay honest. - Fails loudly. An empty report, a failed shard, a report whose paths match no tracked
file or a malformed marker is an error, not a quietly lower number. Everything that
silences coverage needs a stated reason and is listed in the PR comment, and settings live
in
.patchcov/config.yamlin version control, so a reviewer can see what a gate does and what was excluded. - One tool across languages. Five report formats are detected from content, so the same command works for Rust, Go, Java and Kotlin, JavaScript and TypeScript, Python, C and C++ and more.
When to choose patchcov
Choose patchcov when:
- the question you need answered is "did this change add untested code, and where?";
- coverage is checked by automation, including AI agents, as much as by people;
- you want the coverage gate to depend on nothing but your own CI runners, so an outage of an external quality server or coverage service cannot block your work;
- you want the check to run locally and in CI without sending code coverage to an external service;
- your coverage is noisy and you need regressions you can trust;
- you run a sharded CI job and want a single combined result and gate.
Choose something else, or use patchcov alongside it, when you need:
- a hosted dashboard with coverage history, trend graphs or cross-repository views;
- per-branch coverage.
diff --branch-coveragescores lcov and Cobertura branches per line (mergedrops branch records), and does not read JaCoCo or llvm-cov JSON branches or report per-branch percentages; see opt-in branch coverage; - a broader code-quality platform that covers coverage along with static analysis and security scanning.
patchcov measures whether lines ran, not whether the tests assert anything useful about them. Treat it as a regression signal, and pair it with review, or mutation testing, for confidence in the tests themselves.
Install
--locked builds with the dependency versions CI tests, on Rust 1.88 or newer. Without it,
cargo resolves the newest dependencies, and a transitive release may need a newer compiler
than 1.88; a weekly CI job checks for that, but only --locked is guaranteed.
Prebuilt binaries for Linux (glibc 2.35 or newer; x86_64 and aarch64), macOS (Apple silicon
and Intel) and Windows (x86_64 MSVC) are attached to each
GitHub release, along with SHA-256
checksums. Linux and macOS builds use .tar.gz archives; Windows builds use
patchcov-v<version>-x86_64-pc-windows-msvc.zip. Extract the Windows ZIP and add the
directory containing patchcov.exe to your PATH. The binaries are not signed or
notarized, so macOS quarantines a downloaded one; cargo install avoids that.
To install a prebuilt binary without downloading an archive by hand, use cargo-binstall:
It fetches the release archive for your platform and falls back to building from source on
any other. Recent cargo-binstall releases (1.25 was tested) already find these archives by
guessing their names; the crate's [package.metadata.binstall] states the layout, so the
lookup no longer depends on that guess. Because cargo-binstall reads that metadata from the
published crate, it applies from the first release after 0.2.0. cargo-binstall downloads
with its own client, so the macOS quarantine caveat above does not apply.
Verify a download
Each archive has a .sha256 file next to it holding <hash> <archive name>. Download both
into one directory and check the archive against it.
# Linux and macOS
# patchcov-v0.2.0-x86_64-unknown-linux-gnu.tar.gz: OK
# Windows (PowerShell)
$expected = (Get-Content patchcov-v0.2.0-x86_64-pc-windows-msvc.zip.sha256).Split(' ')[0]
$actual = (Get-FileHash patchcov-v0.2.0-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash
if ($actual -eq $expected) { 'OK' } else { 'MISMATCH' }
Checksums are published beside the archives, so they catch a corrupted download but not a compromised release; they are not a signature.
Use
# Produce a per-line report (any tool that writes one of the supported formats).
# Patch coverage against the merge base with the default branch (origin/HEAD, else main or master).
# Fail the job if patch coverage is under 80% or overall coverage under 70%.
# Merge the shards of a sharded run, then gate as usual.
# Check source markers without a coverage report.
Measure the report at the revision you diff. The report must come from a test run on
the code at HEAD (or at --head-ref). A report measured on older code gives line numbers
that no longer match the diff, and patchcov cannot tell: the result is silently wrong. The
merge base must also resolve, which needs full git history in CI (fetch-depth: 0).
The command prints the report to stdout and exits 0, or 1 when a gate fails, so a CI job
can post the comment and then fail. Other failures have their own codes (2 usage, 3 report,
4 marker, 5 config, 6 git, 7 path mismatch, 8 other), so a script can tell a failed
gate from an unreadable report. See the exit codes, and
--error-format json for the cause as a JSON object on stderr, and warnings,
lint-markers findings and the merge summary as JSON lines
(error output).
Where to go next:
- Usage: input formats and path mapping for non-Rust languages, gating, sharded runs, the ignore list and source markers, CI.
- Reference: every flag, the
.patchcov/config.yamlkeys, the JSON/YAML schema, exit codes and environment variables. - Explanation: why the total differs from llvm-cov's summary, how
toleratemasking and diff scoping work. - Troubleshooting: an empty result, no files matching, an unresolvable merge base, a report from the wrong revision.
GitHub Action
action-works/patchcov-action runs
patchcov in a pull-request workflow. It installs a cached patchcov binary, runs
cargo-llvm-cov (or takes a report you produced in any language), posts a sticky PR
comment with patch coverage and the uncovered new lines, publishes the baseline on
main and applies the gates after the comment posts. It also combines the reports
of sharded runs.
jobs:
coverage:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # to post the coverage comment
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history so the merge base resolves
- uses: action-works/patchcov-action@v1
with:
fail-under-patch: 80
See the action's README for thin mode, sharded runs and its inputs.
Configuration
Settings that belong to a repository live in .patchcov/config.yaml, found by walking up from
the repository root. --config-dir and the PATCHCOV_CONFIG_DIR environment variable
override the location. There are no user-level or machine-level settings, so what a gate
reports is visible in version control.
| Key | Purpose |
|---|---|
diff.ignore-filename-regex |
Regexes for files to exclude from both reports; unioned with the flag |
diff.path-mappings |
from/to directory replacements for reports whose paths differ from git's |
diff.require-measured |
Globs of touched files that must appear in a report; unioned with the flag |
diff.allow-path-mismatch |
Warn instead of failing when a report matches no tracked file; enabled by either this or the flag |
lint-markers.include |
Globs narrowing which files lint-markers scans; replaced by the flag |
# .patchcov/config.yaml
diff:
ignore-filename-regex:
- 'src/bits/popcount\.rs' # CPU-gated, flaps between runners
lint-markers:
include:
- '**/*.rs'
Types, defaults and how each key combines with its command-line flag are in the
config reference. This repository's own
.patchcov/config.yaml is a working example.
Library
The analysis is a library as well as a command, published on crates.io with API documentation
at https://docs.rs/patchcov. patchcov::parse reads a report, patchcov::DiffModel builds
the added-line sets from git2, patchcov::analyze attributes coverage to the diff and
patchcov::render formats the result:
use Repository;
use ;
This example is compiled as a doctest in src/lib.rs, which also holds the
architecture overview. The API is not stable at 0.x. Breaking changes are allowed in minor
releases (0.2 to 0.3), and the release process runs cargo-semver-checks so each one is
accompanied by a minor version bump. Depend on patchcov = "~0.2" if you need to avoid surprises,
and read the changelog when upgrading. The command line is
the better-supported interface.
Contributing
See CONTRIBUTING.md for how to build, run the tests and add a fixture, and for the merge queue flow.
Releasing
See docs/RELEASE.md.
License
BSD-3-Clause. See LICENSE.