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    /// The part of `paths` inside submodules whose gitlink the change moves; the outer index
26    /// cannot show them, so a staged run reads them from the submodule's checkout.
27    pub in_submodules: BTreeSet<String>,
28    /// Indices of documents whose own files changed.
29    pub touched: BTreeSet<usize>,
30    /// `touched` plus documents that link to them.
31    pub with_neighbours: BTreeSet<usize>,
32    /// The revision to compare against for transition checks.
33    pub base: Option<String>,
34    /// Paths the change deletes or renames away; they are not in `paths`.
35    pub gone: BTreeSet<String>,
36}
37
38impl Scope {
39    pub fn build(c: &Corpus, g: &Graph, range: Range) -> Result<Self> {
40        let root = &c.cfg.root;
41        let (mut paths, in_submodules, base): (BTreeSet<String>, BTreeSet<String>, _) = match &range {
42            Range::All => (BTreeSet::new(), BTreeSet::new(), None),
43            Range::Staged => {
44                let p = vcs::staged(root).ok_or_else(|| anyhow!("--staged needs a git repository"))?;
45                let sub = vcs::submodule_changes(root, vcs::Side::Staged);
46                (p.into_iter().collect(), sub.into_iter().collect(), Some("HEAD".to_string()))
47            }
48            Range::Changed(base) => {
49                let p = vcs::changed_since(root, base).ok_or_else(|| anyhow!("cannot diff against {base}"))?;
50                let sub = vcs::submodule_changes(root, vcs::Side::Since(base));
51                (p.into_iter().collect(), sub.into_iter().collect(), Some(base.clone()))
52            }
53        };
54        paths.extend(in_submodules.iter().cloned());
55        let gone: BTreeSet<String> = match &range {
56            Range::All => BTreeSet::new(),
57            Range::Staged => vcs::gone(root, None).unwrap_or_default().into_iter().collect(),
58            Range::Changed(base) => vcs::gone(root, Some(base)).unwrap_or_default().into_iter().collect(),
59        };
60        let mut touched = BTreeSet::new();
61        if !matches!(range, Range::All) {
62            for (i, d) in c.docs.iter().enumerate() {
63                let own = std::iter::once(&d.path).chain(&d.chapters).chain(&d.bundle_files);
64                if own.into_iter().any(|p| paths.contains(p)) {
65                    touched.insert(i);
66                }
67            }
68        }
69        let mut with_neighbours = touched.clone();
70        for &i in &touched {
71            for e in g.edges_to(i) {
72                with_neighbours.insert(e.from);
73            }
74        }
75        Ok(Scope { range, paths, in_submodules, touched, with_neighbours, base, gone })
76    }
77
78    pub fn is_all(&self) -> bool {
79        matches!(self.range, Range::All)
80    }
81
82    /// Whether checks about document `i` itself run.
83    pub fn covers(&self, i: usize) -> bool {
84        self.is_all() || self.touched.contains(&i)
85    }
86
87    /// Whether checks about links from document `i` run (they may break when a target moves).
88    pub fn covers_links(&self, i: usize) -> bool {
89        self.is_all() || self.with_neighbours.contains(&i)
90    }
91
92    pub fn covers_path(&self, p: &str) -> bool {
93        self.is_all() || self.paths.contains(p)
94    }
95}