use std::collections::{HashMap, HashSet};
use anyhow::{Result, bail};
use bob::PackageState;
use bob::db::{BuildDiff, Database, DiffEntry};
use super::{
Cell, Column, ColumnSource, OutputFormat, OutputOptions, Writer, cols_help,
parse_status_filter, select_columns,
};
const SUPPORTED: &[Column] = &[
Column::Pkgname,
Column::Pkgpath,
Column::Breaks,
Column::Stage,
Column::StagePrev,
Column::Outcome,
Column::OutcomePrev,
Column::PkgnamePrev,
];
const DEFAULT: &[Column] = &[
Column::Pkgname,
Column::Pkgpath,
Column::Breaks,
Column::Stage,
];
#[derive(Debug, clap::Args)]
#[command(after_long_help = diff_after_help())]
pub struct DiffArgs {
pub build1: Option<String>,
pub build2: Option<String>,
#[arg(short, long)]
pub all: bool,
#[arg(short = 'H')]
pub no_header: bool,
#[arg(short = 'l', long)]
pub long: bool,
#[arg(short = 'o', value_delimiter = ',')]
pub columns: Option<Vec<String>>,
#[arg(short = 'f', long = "from", value_delimiter = ',', value_parser = parse_status_filter)]
pub from: Vec<Vec<PackageState>>,
#[arg(short = 't', long = "to", value_delimiter = ',', value_parser = parse_status_filter)]
pub to: Vec<Vec<PackageState>>,
}
fn diff_after_help() -> String {
cols_help(SUPPORTED, DEFAULT)
}
impl ColumnSource for DiffEntry {
type Ctx = HashMap<String, usize>;
fn cell(&self, col: Column, breaks: &Self::Ctx) -> Cell {
match col {
Column::Pkgname => self
.build2_pkgname
.as_deref()
.or(self.build1_pkgname.as_deref())
.map_or(Cell::Null, Cell::from),
Column::Pkgpath => self.pkgpath.as_str().into(),
Column::Breaks => get_breaks(self, breaks).into(),
Column::Stage => match self.build2_outcome {
Some(k) if k.is_success() => Cell::Null,
_ => self
.build2_stage
.map_or(Cell::Null, |s| <&str>::from(s).into()),
},
Column::StagePrev => self
.build1_stage
.map_or(Cell::Null, |s| <&str>::from(s).into()),
Column::Outcome => self
.build2_outcome
.map_or(Cell::Null, |o| o.as_str().into()),
Column::OutcomePrev => self
.build1_outcome
.map_or(Cell::Null, |o| o.as_str().into()),
Column::PkgnamePrev => self
.build1_pkgname
.as_deref()
.map_or(Cell::Null, Cell::from),
_ => unreachable!("column {:?} not supported by bob diff", col),
}
}
}
pub fn run(db: &Database, args: DiffArgs) -> Result<()> {
let chosen = select_columns(args.columns.as_deref(), args.long, DEFAULT, SUPPORTED)?;
let (build1_id, build2_id) = match (args.build1, args.build2) {
(Some(b1), Some(b2)) => (b1, b2),
(Some(b1), None) => {
let builds = db.history_build_ids()?;
if builds.is_empty() {
bail!("No builds in history");
}
(b1, builds[0].clone())
}
(None, Some(_)) => {
bail!("Specify both build IDs, or none for the two most recent");
}
(None, None) => {
let builds = db.history_build_ids()?;
if builds.len() < 2 {
bail!(
"Need at least two builds to compare. \
Use 'bob list builds' to see available builds."
);
}
(builds[1].clone(), builds[0].clone())
}
};
let diff = db.compute_build_diff(&build1_id, &build2_id)?;
let breaks: HashMap<String, usize> = db.dep_counts()?;
let from: Option<HashSet<PackageState>> =
(!args.from.is_empty()).then(|| args.from.iter().flatten().copied().collect());
let to: Option<HashSet<PackageState>> =
(!args.to.is_empty()).then(|| args.to.iter().flatten().copied().collect());
let opts = OutputOptions {
format: OutputFormat::Table,
no_header: args.no_header,
raw: false,
};
if from.is_some() || to.is_some() {
print_filtered(&diff, &breaks, chosen, opts, from.as_ref(), to.as_ref())
} else {
print_diff(&diff, &breaks, args.all, chosen, opts)
}
}
fn matches_filter(
e: &DiffEntry,
from: Option<&HashSet<PackageState>>,
to: Option<&HashSet<PackageState>>,
) -> bool {
let ok = |set: Option<&HashSet<PackageState>>, state: Option<PackageState>| match set {
Some(s) => state.is_some_and(|k| s.contains(&k)),
None => true,
};
ok(from, e.build1_outcome) && ok(to, e.build2_outcome)
}
fn print_filtered(
diff: &BuildDiff,
breaks: &HashMap<String, usize>,
chosen: Vec<Column>,
opts: OutputOptions,
from: Option<&HashSet<PackageState>>,
to: Option<&HashSet<PackageState>>,
) -> Result<()> {
let entries: Vec<&DiffEntry> = diff
.new_failures
.iter()
.chain(diff.version_changes.iter())
.chain(diff.fixes.iter())
.chain(diff.other_changes.iter())
.filter(|e| matches_filter(e, from, to))
.collect();
let label = |set: Option<&HashSet<PackageState>>| -> String {
set.map(|s| {
let mut names: Vec<&str> = s.iter().map(|k| k.as_str()).collect();
names.sort_unstable();
names.join(",")
})
.unwrap_or_else(|| "any".into())
};
let summary = format!(
"@@ {}: from {} to {} @@",
entries.len(),
label(from),
label(to),
);
write_diff(diff, breaks, chosen, opts, &summary, vec![(' ', entries)])
}
fn get_breaks(e: &DiffEntry, breaks: &HashMap<String, usize>) -> usize {
e.build2_pkgname
.as_deref()
.or(e.build1_pkgname.as_deref())
.and_then(|n| breaks.get(n).copied())
.unwrap_or(0)
}
fn write_diff(
diff: &BuildDiff,
breaks: &HashMap<String, usize>,
chosen: Vec<Column>,
opts: OutputOptions,
summary: &str,
sections: Vec<(char, Vec<&DiffEntry>)>,
) -> Result<()> {
let mut out = Writer::stdout(chosen, opts);
out.message(&format!("--- {}", diff.build1_id));
out.message(&format!("+++ {}", diff.build2_id));
out.message(summary);
for (prefix, mut entries) in sections {
entries.sort_by_key(|e| std::cmp::Reverse(get_breaks(e, breaks)));
for e in entries {
out.write(Some(prefix), e, breaks);
}
}
out.finish()
}
fn print_diff(
diff: &BuildDiff,
breaks: &HashMap<String, usize>,
show_all: bool,
chosen: Vec<Column>,
opts: OutputOptions,
) -> Result<()> {
let nf = diff.new_failures.len();
let fx = diff.fixes.len();
let vc = diff.version_changes.len();
let oc = diff.other_changes.len();
let mut parts = Vec::new();
if nf > 0 {
parts.push(format!("+{} failure{}", nf, if nf == 1 { "" } else { "s" }));
}
if fx > 0 {
parts.push(format!("-{} fix{}", fx, if fx == 1 { "" } else { "es" }));
}
if vc > 0 {
parts.push(format!("~{} change{}", vc, if vc == 1 { "" } else { "s" }));
}
if show_all && oc > 0 {
parts.push(format!(
"{} other change{}",
oc,
if oc == 1 { "" } else { "s" }
));
}
let summary = if parts.is_empty() {
"@@ no changes @@".to_string()
} else {
format!("@@ {} @@", parts.join(", "))
};
let mut sections: Vec<(char, Vec<&DiffEntry>)> = vec![
('+', diff.new_failures.iter().collect()),
('~', diff.version_changes.iter().collect()),
('-', diff.fixes.iter().collect()),
];
if show_all {
sections.push((' ', diff.other_changes.iter().collect()));
}
write_diff(diff, breaks, chosen, opts, &summary, sections)
}