use std::collections::BTreeSet;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use crate::cond::Releases;
use crate::merge::{self, Kind};
#[derive(Debug, Clone)]
pub struct Input {
pub minor: u32,
pub root: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Record {
pub path: String,
pub kind: Kind,
pub branches: usize,
pub guarded: bool,
pub releases: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Report {
pub releases: Vec<String>,
pub records: Vec<Record>,
pub bytes_in: u64,
pub bytes_newest: u64,
pub bytes_out: u64,
pub problems: Vec<String>,
}
impl Report {
pub fn counted(&self, kind: Kind) -> usize {
self.records.iter().filter(|r| r.kind == kind).count()
}
pub fn busiest(&self, how_many: usize) -> Vec<&Record> {
let mut sorted: Vec<&Record> = self.records.iter().filter(|r| r.branches > 0).collect();
sorted.sort_by(|a, b| b.branches.cmp(&a.branches).then(a.path.cmp(&b.path)));
sorted.truncate(how_many);
sorted
}
pub fn listing(&self) -> String {
let mut out = String::new();
for record in &self.records {
let kind = match record.kind {
Kind::Same => "same",
Kind::Conditional => "merged",
Kind::PerRelease => "per-release",
};
let absent = if record.guarded { "guarded" } else { "everywhere" };
out.push_str(&format!(
"{}\t{kind}\t{}\t{absent}\t{}\n",
record.path, record.branches, record.releases
));
}
out
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "headers: {} releases, {}", self.releases.len(), self.releases.join(" "))?;
writeln!(
f,
"headers: {} files, {} with no conditional, {} merged, {} per release, {} guarded",
self.records.len(),
self.counted(Kind::Same),
self.counted(Kind::Conditional),
self.counted(Kind::PerRelease),
self.records.iter().filter(|r| r.guarded).count(),
)?;
let branches: usize = self.records.iter().map(|r| r.branches).sum();
writeln!(f, "headers: {branches} conditionals written")?;
writeln!(
f,
"headers: {} KiB, against {} KiB for the newest install alone and {} KiB for all {}",
self.bytes_out / 1024,
self.bytes_newest / 1024,
self.bytes_in / 1024,
self.releases.len(),
)?;
for record in self.busiest(10) {
writeln!(f, "headers: {} has {} conditionals", record.path, record.branches)?;
}
Ok(())
}
}
pub fn merge_trees(inputs: &[Input], out: &Path) -> Result<Report, String> {
let releases = Releases::new(inputs.iter().map(|i| i.minor).collect())?;
if out.exists() && fs::read_dir(out).map(|mut d| d.next().is_some()).unwrap_or(false) {
return Err(format!("{} is not empty, and a merge writes a whole tree", out.display()));
}
let mut report = Report {
releases: (0..releases.count()).map(|n| releases.spelled(n)).collect(),
..Report::default()
};
let mut paths: BTreeSet<String> = BTreeSet::new();
let mut found: Vec<BTreeSet<String>> = Vec::new();
for input in inputs {
let mut one = BTreeSet::new();
walk(&input.root, &input.root, &mut one)
.map_err(|why| format!("{}: {why}", input.root.display()))?;
paths.extend(one.iter().cloned());
found.push(one);
}
if paths.is_empty() {
return Err("none of the trees has a file in it".to_owned());
}
for path in &paths {
let mut texts: Vec<Option<String>> = Vec::with_capacity(inputs.len());
for (n, input) in inputs.iter().enumerate() {
if !found[n].contains(path) {
texts.push(None);
continue;
}
let whole = input.root.join(path);
let text =
fs::read_to_string(&whole).map_err(|why| format!("{}: {why}", whole.display()))?;
report.bytes_in += text.len() as u64;
if n + 1 == inputs.len() {
report.bytes_newest += text.len() as u64;
}
texts.push(Some(text));
}
let given: Vec<Option<&str>> = texts.iter().map(|t| t.as_deref()).collect();
let merged = merge::one(&releases, path, &given)?;
report.problems.extend(merged.problems.iter().cloned());
report.records.push(Record {
path: path.clone(),
kind: merged.kind,
branches: merged.branches,
guarded: merged.guarded,
releases: given.iter().filter(|t| t.is_some()).count(),
});
report.bytes_out += merged.text.len() as u64;
let whole = out.join(path);
if let Some(dir) = whole.parent() {
fs::create_dir_all(dir).map_err(|why| format!("{}: {why}", dir.display()))?;
}
fs::write(&whole, &merged.text).map_err(|why| format!("{}: {why}", whole.display()))?;
}
Ok(report)
}
fn walk(root: &Path, dir: &Path, out: &mut BTreeSet<String>) -> Result<(), String> {
let listing = fs::read_dir(dir).map_err(|why| format!("{}: {why}", dir.display()))?;
for entry in listing {
let entry = entry.map_err(|why| why.to_string())?;
let path = entry.path();
let kind = entry.file_type().map_err(|why| format!("{}: {why}", path.display()))?;
if kind.is_dir() {
walk(root, &path, out)?;
continue;
}
let relative = path
.strip_prefix(root)
.map_err(|_| format!("{} is not under {}", path.display(), root.display()))?;
let Some(name) = relative.to_str() else {
return Err(format!("{} is not a name this can write down", relative.display()));
};
out.insert(name.replace('\\', "/"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("rucc-headers-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("a temporary directory");
dir
}
fn write(root: &Path, path: &str, text: &str) {
let whole = root.join(path);
fs::create_dir_all(whole.parent().expect("a parent")).expect("the directory");
fs::write(whole, text).expect("the file");
}
#[test]
fn two_trees_become_one_and_the_report_says_what_happened() {
let dir = scratch("two-trees");
let (old, new, out) = (dir.join("2.28"), dir.join("2.31"), dir.join("out"));
write(&old, "stdio.h", "int puts (const char *);\n");
write(&new, "stdio.h", "int puts (const char *);\nint newer (void);\n");
write(&old, "sys/gone.h", "int gone (void);\n");
write(&new, "sys/here.h", "int here (void);\n");
write(&old, "same.h", "int same (void);\n");
write(&new, "same.h", "int same (void);\n");
let inputs = vec![Input { minor: 28, root: old }, Input { minor: 31, root: new.clone() }];
let report = merge_trees(&inputs, &out).expect("a merge");
assert_eq!(report.problems, Vec::<String>::new());
assert_eq!(report.records.len(), 4);
assert_eq!(report.counted(Kind::Same), 3);
assert_eq!(report.records.iter().filter(|r| r.guarded).count(), 2);
assert_eq!(fs::read_to_string(out.join("same.h")).expect("written"), "int same (void);\n");
let here = fs::read_to_string(out.join("sys/here.h")).expect("written");
assert!(here.contains("#error"), "{here}");
assert!(here.contains("int here (void);"), "{here}");
assert_eq!(report.bytes_newest, newest_bytes(&new));
let _ = fs::remove_dir_all(&dir);
}
fn newest_bytes(root: &Path) -> u64 {
let mut paths = BTreeSet::new();
walk(root, root, &mut paths).expect("a walk");
paths.iter().map(|p| fs::metadata(root.join(p)).expect("there").len()).sum()
}
#[test]
fn a_tree_already_there_is_not_written_over() {
let dir = scratch("not-over");
let (old, new, out) = (dir.join("2.28"), dir.join("2.31"), dir.join("out"));
write(&old, "a.h", "int a (void);\n");
write(&new, "a.h", "int a (void);\n");
write(&out, "leftover.h", "from the last run\n");
let inputs = vec![Input { minor: 28, root: old }, Input { minor: 31, root: new }];
let why = merge_trees(&inputs, &out).expect_err("it is not empty");
assert!(why.contains("is not empty"), "{why}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn one_release_is_not_a_merge() {
let dir = scratch("one-release");
let root = dir.join("2.28");
write(&root, "a.h", "int a (void);\n");
let why = merge_trees(&[Input { minor: 28, root }], &dir.join("out"))
.expect_err("one release is a copy");
assert!(why.contains("at least two releases"), "{why}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn the_listing_has_one_line_per_file() {
let dir = scratch("listing");
let (old, new, out) = (dir.join("2.28"), dir.join("2.31"), dir.join("out"));
write(&old, "a.h", "int a (void);\n");
write(&new, "a.h", "int a (void);\nint b (void);\n");
let inputs = vec![Input { minor: 28, root: old }, Input { minor: 31, root: new }];
let report = merge_trees(&inputs, &out).expect("a merge");
assert_eq!(report.listing(), "a.h\tmerged\t1\teverywhere\t2\n");
assert!(format!("{report}").contains("1 files, 0 with no conditional, 1 merged"));
let _ = fs::remove_dir_all(&dir);
}
}