ccsync_core/config/
patterns.rs1use std::path::Path;
4
5use anyhow::Context;
6use ignore::gitignore::{Gitignore, GitignoreBuilder};
7
8use crate::error::Result;
9
10pub struct PatternMatcher {
12 gitignore: Option<Gitignore>,
13}
14
15impl PatternMatcher {
16 #[must_use]
18 pub const fn new() -> Self {
19 Self { gitignore: None }
20 }
21
22 pub fn with_patterns(ignore_patterns: &[String], include_patterns: &[String]) -> Result<Self> {
28 let mut builder = GitignoreBuilder::new("");
29
30 for pattern in ignore_patterns {
32 builder
33 .add_line(None, pattern)
34 .with_context(|| format!("Invalid ignore pattern: '{pattern}'"))?;
35 }
36
37 for pattern in include_patterns {
39 builder
40 .add_line(None, &format!("!{pattern}"))
41 .with_context(|| format!("Invalid include pattern: '{pattern}'"))?;
42 }
43
44 let gitignore = builder.build()?;
45
46 Ok(Self {
47 gitignore: Some(gitignore),
48 })
49 }
50
51 #[must_use]
53 pub fn should_include(&self, path: &Path, is_dir: bool) -> bool {
54 self.gitignore
55 .as_ref()
56 .is_none_or(|gi| !gi.matched(path, is_dir).is_ignore())
57 }
58}
59
60impl Default for PatternMatcher {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use std::path::PathBuf;
70
71 #[test]
72 fn test_no_patterns() {
73 let matcher = PatternMatcher::new();
74 assert!(matcher.should_include(&PathBuf::from("any/file.txt"), false));
75 }
76
77 #[test]
78 fn test_ignore_pattern() {
79 let matcher = PatternMatcher::with_patterns(&["*.tmp".to_string()], &[]).unwrap();
80
81 assert!(!matcher.should_include(&PathBuf::from("file.tmp"), false));
82 assert!(matcher.should_include(&PathBuf::from("file.txt"), false));
83 }
84
85 #[test]
86 fn test_include_overrides_ignore() {
87 let matcher =
88 PatternMatcher::with_patterns(&["*.tmp".to_string()], &["important.tmp".to_string()])
89 .unwrap();
90
91 assert!(!matcher.should_include(&PathBuf::from("file.tmp"), false));
92 assert!(matcher.should_include(&PathBuf::from("important.tmp"), false));
93 }
94
95 #[test]
96 fn test_directory_patterns() {
97 let matcher = PatternMatcher::with_patterns(&["node_modules/".to_string()], &[]).unwrap();
98
99 assert!(!matcher.should_include(&PathBuf::from("node_modules"), true));
100 assert!(matcher.should_include(&PathBuf::from("src"), true));
101 }
102
103 #[test]
104 fn test_relative_path_wildcards() {
105 let matcher = PatternMatcher::with_patterns(&["agents/git-*".to_string()], &[]).unwrap();
106
107 assert!(!matcher.should_include(&PathBuf::from("agents/git-commit.md"), false));
109 assert!(!matcher.should_include(&PathBuf::from("agents/git-helper.md"), false));
110
111 assert!(matcher.should_include(&PathBuf::from("agents/other-agent.md"), false));
113 assert!(matcher.should_include(&PathBuf::from("skills/test.md"), false));
114 }
115}