1use std::fs;
12use std::io::IsTerminal;
13use std::path::PathBuf;
14
15use clap::{Parser, ValueEnum};
16use fleetreach_core::FleetReport;
17use fleetreach_report as report;
18
19use crate::cli::{fail, SeverityArg};
20
21#[derive(Parser)]
22pub struct DiffArgs {
23 baseline: PathBuf,
25 current: PathBuf,
27 #[arg(short, long, value_enum, default_value_t = DiffFormat::Table)]
28 format: DiffFormat,
29 #[arg(
30 long,
31 value_enum,
32 default_value_t = SeverityArg::Low,
33 help = "fail if any NEW vuln is at/above this severity (Unknown always counts)"
34 )]
35 fail_on: SeverityArg,
36 #[arg(long, help = "also fail if a NEW supply-chain warning appeared")]
37 fail_on_warnings: bool,
38 #[arg(long, help = "always exit 0 (report-only; never gate on new findings)")]
39 exit_zero: bool,
40}
41
42#[derive(Clone, Copy, ValueEnum)]
43enum DiffFormat {
44 Table,
45 Json,
46}
47
48pub fn run_diff(args: DiffArgs) -> u8 {
50 let baseline = match read_report(&args.baseline) {
51 Ok(r) => r,
52 Err(e) => return fail(&e),
53 };
54 let current = match read_report(&args.current) {
55 Ok(r) => r,
56 Err(e) => return fail(&e),
57 };
58
59 let diff = report::diff_reports(&baseline, ¤t);
60 match args.format {
61 DiffFormat::Json => match report::to_diff_json(&diff) {
62 Ok(json) => println!("{json}"),
63 Err(e) => return fail(&format!("serializing diff: {e}")),
64 },
65 DiffFormat::Table => {
66 println!(
67 "{}",
68 report::to_diff_table(&diff, std::io::stdout().is_terminal())
69 );
70 }
71 }
72
73 if args.exit_zero {
74 return 0;
75 }
76 u8::from(diff.regressions(args.fail_on.into(), args.fail_on_warnings) > 0)
77}
78
79fn read_report(path: &PathBuf) -> Result<FleetReport, String> {
81 let text = fs::read_to_string(path)
82 .map_err(|e| format!("reading report `{}`: {e}", path.display()))?;
83 serde_json::from_str(&text).map_err(|e| format!("parsing report `{}`: {e}", path.display()))
84}