1#![allow(clippy::expect_used)]
7use std::path::Path;
8
9pub struct FilterRules {
11 pub excludes: Vec<glob::Pattern>,
12 pub includes: Vec<glob::Pattern>,
13}
14
15impl FilterRules {
16 pub fn new(exclude: &[String], include: &[String]) -> Self {
18 Self {
19 excludes: exclude.iter()
20 .filter_map(|p| glob::Pattern::new(p).ok())
21 .collect(),
22 includes: include.iter()
23 .filter_map(|p| glob::Pattern::new(p).ok())
24 .collect(),
25 }
26 }
27
28 pub fn should_skip(&self, rel: &Path) -> bool {
32 let excluded = self.excludes.iter().any(|p| p.matches_path(rel));
33 if !excluded { return false; }
34 !self.includes.iter().any(|p| p.matches_path(rel))
36 }
37}
38
39pub fn read_patterns(path: &Path) -> anyhow::Result<Vec<String>> {
42 let content = std::fs::read_to_string(path)?;
43 Ok(content.lines()
44 .map(|l| l.trim())
45 .filter(|l| !l.is_empty() && !l.starts_with('#'))
46 .map(|l| l.to_string())
47 .collect())
48}
49
50pub fn split_paths(paths: Vec<std::path::PathBuf>) -> anyhow::Result<(Vec<std::path::PathBuf>, std::path::PathBuf)> {
53 if paths.len() < 2 {
54 anyhow::bail!("requires at least a source and destination");
55 }
56 let mut paths = paths;
57 #[allow(clippy::unwrap_used)]
58 let destination = paths.pop().unwrap();
59 Ok((paths, destination))
60}
61
62#[cfg(test)]
63mod tests {
64 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
65 use super::*;
66 use std::path::PathBuf;
67
68 #[test]
71 fn test_no_filters_skips_nothing() {
72 let rules = FilterRules::new(&[], &[]);
73 assert!(!rules.should_skip(Path::new("foo.rs")));
74 assert!(!rules.should_skip(Path::new("dir/bar.tmp")));
75 }
76
77 #[test]
78 fn test_exclude_matches() {
79 let rules = FilterRules::new(&["*.tmp".into()], &[]);
80 assert!(rules.should_skip(Path::new("foo.tmp")));
81 assert!(!rules.should_skip(Path::new("foo.rs")));
82 }
83
84 #[test]
85 fn test_exclude_glob_directory() {
86 let rules = FilterRules::new(&[".git/**".into()], &[]);
87 assert!(rules.should_skip(Path::new(".git/config")));
88 assert!(rules.should_skip(Path::new(".git/refs/heads/main")));
89 assert!(!rules.should_skip(Path::new("src/main.rs")));
90 }
91
92 #[test]
93 fn test_include_overrides_exclude() {
94 let rules = FilterRules::new(
95 &["*.tmp".into()],
96 &["important.tmp".into()],
97 );
98 assert!(rules.should_skip(Path::new("junk.tmp")));
99 assert!(!rules.should_skip(Path::new("important.tmp")));
100 assert!(!rules.should_skip(Path::new("foo.rs")));
101 }
102
103 #[test]
104 fn test_exclude_all_include_specific() {
105 let rules = FilterRules::new(
106 &["*".into()],
107 &["*.rs".into()],
108 );
109 assert!(!rules.should_skip(Path::new("main.rs")));
110 assert!(rules.should_skip(Path::new("readme.md")));
111 assert!(rules.should_skip(Path::new("data.bin")));
112 }
113
114 #[test]
115 fn test_multiple_excludes() {
116 let rules = FilterRules::new(
117 &["*.tmp".into(), "*.bak".into(), "*.swp".into()],
118 &[],
119 );
120 assert!(rules.should_skip(Path::new("foo.tmp")));
121 assert!(rules.should_skip(Path::new("bar.bak")));
122 assert!(rules.should_skip(Path::new("baz.swp")));
123 assert!(!rules.should_skip(Path::new("good.rs")));
124 }
125
126 #[test]
127 fn test_multiple_includes() {
128 let rules = FilterRules::new(
129 &["*".into()],
130 &["*.rs".into(), "*.toml".into(), "Makefile".into()],
131 );
132 assert!(!rules.should_skip(Path::new("main.rs")));
133 assert!(!rules.should_skip(Path::new("Cargo.toml")));
134 assert!(!rules.should_skip(Path::new("Makefile")));
135 assert!(rules.should_skip(Path::new("readme.md")));
136 }
137
138 #[test]
139 fn test_nested_path_matching() {
140 let rules = FilterRules::new(&["target/**".into()], &[]);
141 assert!(rules.should_skip(Path::new("target/debug/fxcp")));
142 assert!(rules.should_skip(Path::new("target/release/foxingd")));
143 assert!(!rules.should_skip(Path::new("src/main.rs")));
144 }
145
146 #[test]
147 fn test_invalid_pattern_ignored() {
148 let rules = FilterRules::new(&["[invalid".into(), "*.tmp".into()], &[]);
150 assert!(rules.should_skip(Path::new("foo.tmp")));
151 assert!(!rules.should_skip(Path::new("foo.rs")));
152 }
153
154 #[test]
157 fn test_split_two_args() {
158 let paths = vec![PathBuf::from("src"), PathBuf::from("dst")];
159 let (sources, dest) = split_paths(paths).unwrap();
160 assert_eq!(sources, vec![PathBuf::from("src")]);
161 assert_eq!(dest, PathBuf::from("dst"));
162 }
163
164 #[test]
165 fn test_split_three_args() {
166 let paths = vec![
167 PathBuf::from("file1"),
168 PathBuf::from("file2"),
169 PathBuf::from("dest/"),
170 ];
171 let (sources, dest) = split_paths(paths).unwrap();
172 assert_eq!(sources, vec![PathBuf::from("file1"), PathBuf::from("file2")]);
173 assert_eq!(dest, PathBuf::from("dest/"));
174 }
175
176 #[test]
177 fn test_split_many_args() {
178 let paths = vec![
179 PathBuf::from("a"), PathBuf::from("b"),
180 PathBuf::from("c"), PathBuf::from("d"),
181 PathBuf::from("target/"),
182 ];
183 let (sources, dest) = split_paths(paths).unwrap();
184 assert_eq!(sources.len(), 4);
185 assert_eq!(dest, PathBuf::from("target/"));
186 }
187
188 #[test]
189 fn test_split_one_arg_fails() {
190 let paths = vec![PathBuf::from("only")];
191 assert!(split_paths(paths).is_err());
192 }
193
194 #[test]
195 fn test_split_empty_fails() {
196 let paths: Vec<PathBuf> = vec![];
197 assert!(split_paths(paths).is_err());
198 }
199
200 #[test]
203 fn test_read_patterns_basic() {
204 let dir = tempfile::tempdir().unwrap();
205 let file = dir.path().join("patterns.txt");
206 std::fs::write(&file, "*.tmp\n*.bak\n*.swp\n").unwrap();
207 let patterns = read_patterns(&file).unwrap();
208 assert_eq!(patterns, vec!["*.tmp", "*.bak", "*.swp"]);
209 }
210
211 #[test]
212 fn test_read_patterns_comments_and_blanks() {
213 let dir = tempfile::tempdir().unwrap();
214 let file = dir.path().join("patterns.txt");
215 std::fs::write(&file, "# This is a comment\n*.tmp\n\n# Another comment\n*.bak\n \n").unwrap();
216 let patterns = read_patterns(&file).unwrap();
217 assert_eq!(patterns, vec!["*.tmp", "*.bak"]);
218 }
219
220 #[test]
221 fn test_read_patterns_empty_file() {
222 let dir = tempfile::tempdir().unwrap();
223 let file = dir.path().join("empty.txt");
224 std::fs::write(&file, "").unwrap();
225 let patterns = read_patterns(&file).unwrap();
226 assert!(patterns.is_empty());
227 }
228
229 #[test]
230 fn test_read_patterns_whitespace_trimmed() {
231 let dir = tempfile::tempdir().unwrap();
232 let file = dir.path().join("patterns.txt");
233 std::fs::write(&file, " *.tmp \n *.bak \n").unwrap();
234 let patterns = read_patterns(&file).unwrap();
235 assert_eq!(patterns, vec!["*.tmp", "*.bak"]);
236 }
237
238 #[test]
239 fn test_read_patterns_missing_file() {
240 assert!(read_patterns(Path::new("/nonexistent/file")).is_err());
241 }
242}