cobalt_core/
source.rs

1use crate::Result;
2use crate::Status;
3
4#[derive(Debug, Clone)]
5pub struct Source {
6    root: std::path::PathBuf,
7    ignore: ignore::gitignore::Gitignore,
8}
9
10impl Source {
11    pub fn new<'i>(
12        root: &std::path::Path,
13        ignores: impl IntoIterator<Item = &'i str>,
14    ) -> Result<Self> {
15        let mut ignore = ignore::gitignore::GitignoreBuilder::new(root);
16        for line in ignores {
17            ignore
18                .add_line(None, line)
19                .map_err(|e| Status::new("Invalid ignore entry").with_source(e))?;
20        }
21        let ignore = ignore
22            .build()
23            .map_err(|e| Status::new("Invalid ignore entry").with_source(e))?;
24
25        let source = Self {
26            root: root.to_owned(),
27            ignore,
28        };
29        Ok(source)
30    }
31
32    pub fn root(&self) -> &std::path::Path {
33        &self.root
34    }
35
36    pub fn includes_file(&self, file: &std::path::Path) -> bool {
37        let is_dir = false;
38        self.includes_path(file, is_dir)
39    }
40
41    pub fn includes_dir(&self, dir: &std::path::Path) -> bool {
42        let is_dir = true;
43        self.includes_path(dir, is_dir)
44    }
45
46    pub fn iter(&self) -> impl Iterator<Item = crate::SourcePath> + '_ {
47        walkdir::WalkDir::new(&self.root)
48            .min_depth(1)
49            .follow_links(false)
50            .sort_by_file_name()
51            .into_iter()
52            .filter_entry(move |e| self.includes_entry(e))
53            .filter_map(|e| e.ok())
54            .filter(|e| e.file_type().is_file())
55            .filter_map(move |e| crate::SourcePath::from_root(&self.root, e.path()))
56    }
57
58    fn includes_path(&self, path: &std::path::Path, is_dir: bool) -> bool {
59        match self.ignore.matched_path_or_any_parents(path, is_dir) {
60            ignore::Match::None => true,
61            ignore::Match::Ignore(glob) => {
62                log::trace!("{:?}: ignored {:?}", path, glob.original());
63                false
64            }
65            ignore::Match::Whitelist(glob) => {
66                log::trace!("{:?}: allowed {:?}", path, glob.original());
67                true
68            }
69        }
70    }
71
72    fn includes_path_leaf(&self, path: &std::path::Path, is_dir: bool) -> bool {
73        match self.ignore.matched(path, is_dir) {
74            ignore::Match::None => true,
75            ignore::Match::Ignore(glob) => {
76                log::trace!("{:?}: ignored {:?}", path, glob.original());
77                false
78            }
79            ignore::Match::Whitelist(glob) => {
80                log::trace!("{:?}: allowed {:?}", path, glob.original());
81                true
82            }
83        }
84    }
85
86    fn includes_entry(&self, entry: &walkdir::DirEntry) -> bool {
87        let path = entry.path();
88
89        // Assumption: The parent paths will have been checked before we even get to this point.
90        let is_dir = entry.file_type().is_dir();
91        self.includes_path_leaf(path, is_dir)
92    }
93}
94
95#[cfg(test)]
96#[allow(clippy::bool_assert_comparison)]
97mod tests {
98    use super::*;
99
100    macro_rules! assert_includes_dir {
101        ($root:expr_2021, $ignores:expr_2021, $test:expr_2021, $included:expr_2021) => {
102            let root = $root;
103            let ignores = $ignores.clone();
104            let files = Source::new(std::path::Path::new(root), ignores).unwrap();
105            assert_eq!(files.includes_dir(std::path::Path::new($test)), $included);
106        };
107    }
108    macro_rules! assert_includes_file {
109        ($root:expr_2021, $ignores:expr_2021, $test:expr_2021, $included:expr_2021) => {
110            let root = $root;
111            let ignores = $ignores.clone();
112            let files = Source::new(std::path::Path::new(root), ignores).unwrap();
113            assert_eq!(files.includes_file(std::path::Path::new($test)), $included);
114        };
115    }
116
117    #[test]
118    fn files_includes_root_dir() {
119        assert_includes_dir!("/usr/cobalt/site", &[], "/usr/cobalt/site", true);
120
121        assert_includes_dir!("./", &[], "./", true);
122    }
123
124    #[test]
125    fn files_includes_child_dir() {
126        assert_includes_dir!("/usr/cobalt/site", &[], "/usr/cobalt/site/child", true);
127
128        assert_includes_dir!("./", &[], "./child", true);
129    }
130
131    #[test]
132    fn files_includes_file() {
133        assert_includes_file!("/usr/cobalt/site", &[], "/usr/cobalt/site/child.txt", true);
134
135        assert_includes_file!("./", &[], "./child.txt", true);
136    }
137
138    #[test]
139    fn files_ignore_hidden() {
140        assert_includes_file!(
141            "/usr/cobalt/site",
142            &[".*"],
143            "/usr/cobalt/site/.child.txt",
144            false
145        );
146    }
147
148    #[test]
149    fn files_not_ignored_by_parent() {
150        assert_includes_file!(
151            "/tmp/.foo/cobalt/site",
152            &[".*"],
153            "/tmp/.foo/cobalt/site/child.txt",
154            true
155        );
156    }
157
158    #[test]
159    fn files_includes_child_dir_file() {
160        assert_includes_file!(
161            "/usr/cobalt/site",
162            &[],
163            "/usr/cobalt/site/child/child.txt",
164            true
165        );
166
167        assert_includes_file!("./", &[], "./child/child.txt", true);
168    }
169}