Skip to main content

specfold_core/check/
scope.rs

1//! Which documents a run looks at: all of them, or the ones touched by a change and the
2//! documents that point at them.
3
4use crate::corpus::Corpus;
5use crate::graph::Graph;
6use crate::workspace::vcs;
7use anyhow::{Result, anyhow};
8use std::collections::BTreeSet;
9
10#[derive(Debug, Clone)]
11pub enum Range {
12    /// Everything.
13    All,
14    /// The index (pre-commit).
15    Staged,
16    /// Working tree and untracked files against a base revision (pre-push).
17    Changed(String),
18}
19
20#[derive(Debug, Clone)]
21pub struct Scope {
22    pub range: Range,
23    /// Changed repository paths; empty for `All`.
24    pub paths: BTreeSet<String>,
25    /// Indices of documents whose own files changed.
26    pub touched: BTreeSet<usize>,
27    /// `touched` plus documents that link to them.
28    pub with_neighbours: BTreeSet<usize>,
29    /// The revision to compare against for transition checks.
30    pub base: Option<String>,
31}
32
33impl Scope {
34    pub fn build(c: &Corpus, g: &Graph, range: Range) -> Result<Self> {
35        let (paths, base) = match &range {
36            Range::All => (BTreeSet::new(), None),
37            Range::Staged => {
38                let p = vcs::staged(&c.cfg.root).ok_or_else(|| anyhow!("--staged needs a git repository"))?;
39                (p.into_iter().collect(), Some("HEAD".to_string()))
40            }
41            Range::Changed(base) => {
42                let p = vcs::changed_since(&c.cfg.root, base).ok_or_else(|| anyhow!("cannot diff against {base}"))?;
43                (p.into_iter().collect(), Some(base.clone()))
44            }
45        };
46        let mut touched = BTreeSet::new();
47        if !matches!(range, Range::All) {
48            for (i, d) in c.docs.iter().enumerate() {
49                let own = std::iter::once(&d.path).chain(&d.chapters).chain(&d.bundle_files);
50                if own.into_iter().any(|p| paths.contains(p)) {
51                    touched.insert(i);
52                }
53            }
54        }
55        let mut with_neighbours = touched.clone();
56        for &i in &touched {
57            for e in g.edges_to(i) {
58                with_neighbours.insert(e.from);
59            }
60        }
61        Ok(Scope { range, paths, touched, with_neighbours, base })
62    }
63
64    pub fn is_all(&self) -> bool {
65        matches!(self.range, Range::All)
66    }
67
68    /// Whether checks about document `i` itself run.
69    pub fn covers(&self, i: usize) -> bool {
70        self.is_all() || self.touched.contains(&i)
71    }
72
73    /// Whether checks about links from document `i` run (they may break when a target moves).
74    pub fn covers_links(&self, i: usize) -> bool {
75        self.is_all() || self.with_neighbours.contains(&i)
76    }
77
78    pub fn covers_path(&self, p: &str) -> bool {
79        self.is_all() || self.paths.contains(p)
80    }
81}