1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use covrs::diff::{DiffSource, GitDiff, GitHubDiff, StdinDiff};
use covrs::{cli, db};
/// covrs — Multi-format code coverage ingestion into a unified SQLite store.
#[derive(Parser)]
#[command(name = "covrs", version, about)]
struct Cli {
/// Path to the SQLite database (default: ./.covrs.db)
#[arg(long, global = true, default_value = ".covrs.db")]
db: PathBuf,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Ingest a coverage file into the database.
Ingest {
/// Path to the coverage file.
file: PathBuf,
/// Override format detection (cobertura, lcov).
#[arg(long)]
format: Option<String>,
/// Name for this report (default: filename).
#[arg(long)]
name: Option<String>,
/// Overwrite existing report with the same name.
#[arg(long)]
overwrite: bool,
},
/// Show a coverage summary across all reports.
Summary,
/// List all reports in the database.
Reports,
/// List per-file coverage across all reports.
Files {
/// Sort by coverage rate ascending (show worst files first).
#[arg(long)]
sort_by_coverage: bool,
},
/// Show line-level coverage for a source file.
Lines {
/// The source file path (as stored in the coverage data).
source_file: String,
/// Show only uncovered lines as compact ranges.
#[arg(long)]
uncovered: bool,
},
/// Compute coverage for lines in a git diff (diff coverage).
///
/// By default, reads a diff from stdin or via --git-diff and prints a
/// plain-text coverage summary to stdout. Use --style to control the
/// output format.
///
/// With --comment, posts (or updates) the output as a comment on a
/// GitHub pull request. The diff is fetched from the GitHub API and
/// the PR number, repo, and SHA are detected from standard GitHub
/// Actions environment variables (GITHUB_TOKEN, GITHUB_REF,
/// GITHUB_REPOSITORY, GITHUB_SHA).
DiffCoverage {
/// Git diff arguments, e.g. "HEAD~1" or "main..HEAD".
/// If omitted, reads a unified diff from stdin.
/// Ignored when --comment is used.
#[arg(long)]
git_diff: Option<String>,
/// Optional path prefix to prepend to diff paths for matching
/// against coverage data paths.
#[arg(long)]
path_prefix: Option<String>,
/// Output format.
#[arg(long, value_enum, default_value_t = cli::Style::Text)]
style: cli::Style,
/// Post results as a comment on a GitHub pull request.
/// The diff is fetched via the GitHub API and all required
/// parameters are read from the environment (GITHUB_TOKEN,
/// GITHUB_REPOSITORY, GITHUB_REF, GITHUB_SHA).
#[arg(long)]
comment: bool,
},
}
fn main() -> Result<()> {
let args = Cli::parse();
let mut conn = db::open(&args.db).context("Failed to open database")?;
db::init_schema(&conn).context("Failed to initialize schema")?;
match args.command {
Commands::Ingest {
file,
format,
name,
overwrite,
} => {
let out = cli::cmd_ingest(
&mut conn,
&file,
format.as_deref(),
name.as_deref(),
overwrite,
)?;
print!("{out}");
}
Commands::Summary => print!("{}", cli::cmd_summary(&conn)?),
Commands::Reports => print!("{}", cli::cmd_reports(&conn)?),
Commands::Files { sort_by_coverage } => {
print!("{}", cli::cmd_files(&conn, sort_by_coverage)?)
}
Commands::Lines {
source_file,
uncovered,
} => print!("{}", cli::cmd_lines(&conn, &source_file, uncovered)?),
Commands::DiffCoverage {
git_diff,
path_prefix,
style,
comment,
} => run_diff_coverage(&conn, git_diff, path_prefix, style, comment)?,
}
Ok(())
}
/// Orchestrates I/O (stdin / git / GitHub) then delegates to [`cli::cmd_diff_coverage`].
fn run_diff_coverage(
conn: &rusqlite::Connection,
git_diff: Option<String>,
path_prefix: Option<String>,
style: cli::Style,
comment: bool,
) -> Result<()> {
if comment {
// GitHub mode: fetch diff from the API and post results as a PR comment
let gh = GitHubDiff::from_env()?;
let diff_text = gh.fetch_diff()?;
let output =
cli::cmd_diff_coverage(conn, &diff_text, path_prefix.as_deref(), &style, gh.sha())?;
gh.post_comment(&output)?;
} else {
// Local mode: read diff from git or stdin
let source: Box<dyn DiffSource> = if let Some(args) = git_diff {
Box::new(GitDiff { args })
} else {
Box::new(StdinDiff)
};
let diff_text = source.fetch_diff()?;
let output =
cli::cmd_diff_coverage(conn, &diff_text, path_prefix.as_deref(), &style, None)?;
print!("{output}");
}
Ok(())
}