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 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}