specfold_core/guards/
range.rs1use 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 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#[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 pub commits: Option<String>,
40}
41
42fn walk(root: &Path, bases: &[String]) -> Vec<String> {
46 let dirs: Vec<std::path::PathBuf> = match bases {
47 [] => vec![root.to_path_buf()],
48 b => b.iter().map(|p| root.join(p)).filter(|p| p.exists()).collect(),
49 };
50 let Some((first, rest)) = dirs.split_first() else { return vec![] };
51 let mut builder = WalkBuilder::new(first);
52 for d in rest {
53 builder.add(d);
54 }
55 let mut files: Vec<String> = builder
56 .hidden(false)
57 .require_git(false)
58 .filter_entry(|e| e.file_name() != ".git")
59 .build()
60 .flatten()
61 .filter(|e| e.file_type().is_some_and(|t| t.is_file()))
62 .filter_map(|e| e.path().strip_prefix(root).ok().map(|p| p.to_string_lossy().replace('\\', "/")))
63 .collect();
64 let existing = vcs::tracked(root, bases).unwrap_or_default().into_iter().filter(|p| root.join(p).is_file());
65 files.extend(existing);
66 files.sort();
67 files.dedup();
68 files
69}
70
71pub fn resolve(root: &Path, range: &GuardRange, bases: &[String]) -> Result<Resolved> {
72 match range {
73 GuardRange::Staged => Ok(Resolved {
74 files: vcs::staged(root).ok_or_else(|| anyhow!("--staged needs a git repository"))?,
75 source: Source::Index,
76 commits: None,
77 }),
78 GuardRange::Push(r) => {
79 let r = match r {
80 Some(r) => r.clone(),
81 None => format!(
82 "{}..HEAD",
83 vcs::upstream(root).ok_or_else(|| anyhow!("no upstream branch; pass --range <base>..HEAD"))?
84 ),
85 };
86 let files = vcs::changed_in_range(root, &r).ok_or_else(|| anyhow!("cannot read the range {r}"))?;
87 Ok(Resolved { files, source: Source::Rev("HEAD".into()), commits: Some(r) })
88 }
89 GuardRange::All => Ok(Resolved { files: walk(root, bases), source: Source::WorkTree, commits: None }),
90 }
91}
92
93pub fn read(root: &Path, source: &Source, path: &str) -> Option<Vec<u8>> {
94 match source {
95 Source::Index => vcs::show_staged(root, path).map(String::into_bytes),
96 Source::Rev(rev) => vcs::show(root, rev, path).map(String::into_bytes),
97 Source::WorkTree => std::fs::read(root.join(path)).ok(),
98 }
99}