Skip to main content

specfold_core/
census.rs

1//! A census of the corpus: how much there is, where, and in what state. Used to measure a
2//! migration before and after, and to confirm an old layout is gone.
3
4use crate::corpus::Corpus;
5use crate::graph::{Graph, Resolution};
6use crate::workspace::owners::matcher;
7use ignore::WalkBuilder;
8use serde::Serialize;
9use std::collections::BTreeMap;
10
11#[derive(Debug, Serialize)]
12pub struct Census {
13    pub docs: usize,
14    pub by_root: BTreeMap<String, usize>,
15    pub by_type: BTreeMap<String, usize>,
16    pub by_status: BTreeMap<String, usize>,
17    pub owners_with_docs: usize,
18    pub strays: usize,
19    pub links: usize,
20    pub links_broken: usize,
21    pub links_via_alias: usize,
22    pub path_cites: usize,
23    pub attachments: usize,
24    pub attachment_bytes: u64,
25    pub aliases: usize,
26    /// Files matching `[forbid] paths`: zero once an old layout is fully retired.
27    pub legacy_paths: usize,
28}
29
30pub fn take(c: &Corpus, g: &Graph) -> Census {
31    let mut by_root = BTreeMap::new();
32    let mut by_type = BTreeMap::new();
33    let mut by_status = BTreeMap::new();
34    let (mut links, mut broken, mut via_alias, mut cites, mut attachments, mut bytes) = (0, 0, 0, 0, 0, 0u64);
35    for d in &c.docs {
36        let root = c
37            .roots
38            .iter()
39            .find(|r| d.path.starts_with(&format!("{}/", r.dir)))
40            .map(|r| r.dir.clone())
41            .unwrap_or_default();
42        *by_root.entry(root).or_insert(0) += 1;
43        *by_type.entry(d.id.ty.clone()).or_insert(0) += 1;
44        *by_status.entry(d.status().unwrap_or_else(|| "(none)".into())).or_insert(0) += 1;
45        for l in &d.links {
46            links += 1;
47            match g.resolve(&l.item) {
48                Resolution::Doc { via_alias: true, .. } => via_alias += 1,
49                Resolution::Doc { .. } => {}
50                _ => broken += 1,
51            }
52        }
53        cites += d.path_cites.len();
54        attachments += d.bundle_files.len();
55        bytes += d
56            .bundle_files
57            .iter()
58            .filter_map(|f| std::fs::metadata(c.cfg.root.join(f)).ok())
59            .map(|m| m.len())
60            .sum::<u64>();
61    }
62    let forbid: Vec<_> = c.cfg.raw.forbid.paths.iter().filter_map(|p| matcher(p)).collect();
63    let legacy_paths = if forbid.is_empty() {
64        0
65    } else {
66        WalkBuilder::new(&c.cfg.root)
67            .hidden(false)
68            .require_git(false)
69            .build()
70            .flatten()
71            .filter_map(|e| e.path().strip_prefix(&c.cfg.root).ok().map(|p| p.to_string_lossy().replace('\\', "/")))
72            .filter(|p| !p.is_empty() && forbid.iter().any(|m| m.is_match(p)))
73            .count()
74    };
75    Census {
76        docs: c.docs.len(),
77        owners_with_docs: by_root.len(),
78        by_root,
79        by_type,
80        by_status,
81        strays: c.strays.len(),
82        links,
83        links_broken: broken,
84        links_via_alias: via_alias,
85        path_cites: cites,
86        attachments,
87        attachment_bytes: bytes,
88        aliases: g.by_alias.len(),
89        legacy_paths,
90    }
91}