Skip to main content

specfold_core/guards/
range.rs

1//! What a guard looks at: the index, the commits about to be pushed, or the whole tree.
2
3use crate::workspace::vcs;
4use anyhow::{Result, anyhow};
5use ignore::WalkBuilder;
6use std::path::Path;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum GuardRange {
10    Staged,
11    /// A git range such as `origin/main..HEAD`; `None` means upstream..HEAD.
12    Push(Option<String>),
13    All,
14}
15
16impl GuardRange {
17    pub fn parse(s: &str) -> Self {
18        match s {
19            "staged" => GuardRange::Staged,
20            "all" => GuardRange::All,
21            "push" => GuardRange::Push(None),
22            other => GuardRange::Push(Some(other.to_string())),
23        }
24    }
25}
26
27/// Where file contents are read from for a range.
28#[derive(Debug, Clone)]
29pub enum Source {
30    Index,
31    Rev(String),
32    WorkTree,
33}
34
35pub struct Resolved {
36    pub files: Vec<String>,
37    pub source: Source,
38    /// The git range for commit scanning, when there is one.
39    pub commits: Option<String>,
40    /// C231: versions the range's commits wrote that its end no longer has.
41    pub passing: Vec<vcs::Passing>,
42}
43
44/// Every file under `bases` (repository-relative directories; the whole repository when empty):
45/// the untracked ones the ignore files allow, and every tracked one even when an ignore file
46/// matches it, since a tracked file is published. Walking only where a guard looks keeps it cheap.
47fn walk(root: &Path, bases: &[String]) -> Vec<String> {
48    let dirs: Vec<std::path::PathBuf> = match bases {
49        [] => vec![root.to_path_buf()],
50        b => b.iter().map(|p| root.join(p)).filter(|p| p.exists()).collect(),
51    };
52    let Some((first, rest)) = dirs.split_first() else { return vec![] };
53    let mut builder = WalkBuilder::new(first);
54    for d in rest {
55        builder.add(d);
56    }
57    let mut files: Vec<String> = builder
58        .hidden(false)
59        .require_git(false)
60        .filter_entry(|e| e.file_name() != ".git")
61        .build()
62        .flatten()
63        .filter(|e| e.file_type().is_some_and(|t| t.is_file()))
64        .filter_map(|e| e.path().strip_prefix(root).ok().map(|p| p.to_string_lossy().replace('\\', "/")))
65        .collect();
66    let existing = vcs::tracked(root, bases).unwrap_or_default().into_iter().filter(|p| root.join(p).is_file());
67    files.extend(existing);
68    files.sort();
69    files.dedup();
70    files
71}
72
73pub fn resolve(root: &Path, range: &GuardRange, bases: &[String]) -> Result<Resolved> {
74    match range {
75        GuardRange::Staged => Ok(Resolved {
76            files: vcs::staged(root).ok_or_else(|| anyhow!("--staged needs a git repository"))?,
77            source: Source::Index,
78            commits: None,
79            passing: vec![],
80        }),
81        GuardRange::Push(r) => {
82            let r = match r {
83                Some(r) => r.clone(),
84                None => format!(
85                    "{}..HEAD",
86                    vcs::upstream(root).ok_or_else(|| anyhow!("no upstream branch; pass --range <base>..HEAD"))?
87                ),
88            };
89            let files = vcs::changed_in_range(root, &r).ok_or_else(|| anyhow!("cannot read the range {r}"))?;
90            // C230: contents come from the range's end, not from whatever is checked out.
91            let source = Source::Rev(vcs::range_end(&r));
92            Ok(Resolved { files, source, passing: vcs::passing(root, &r), commits: Some(r) })
93        }
94        GuardRange::All => {
95            Ok(Resolved { files: walk(root, bases), source: Source::WorkTree, commits: None, passing: vec![] })
96        }
97    }
98}
99
100pub fn read(root: &Path, source: &Source, path: &str) -> Option<Vec<u8>> {
101    match source {
102        Source::Index => vcs::show_staged(root, path).map(String::into_bytes),
103        Source::Rev(rev) => vcs::show(root, rev, path).map(String::into_bytes),
104        Source::WorkTree => std::fs::read(root.join(path)).ok(),
105    }
106}