Skip to main content

ztr_lib/
ignore_rules.rs

1use anyhow::{Context, Result};
2use ignore::gitignore::{Gitignore, GitignoreBuilder};
3use std::path::{Path, PathBuf};
4
5/// 管理文件和目录的忽略规则。
6/// 使用 Gitignore 语法来匹配路径。
7pub struct IgnoreRules {
8    gitignore: Gitignore,
9    base_dir: PathBuf,
10}
11
12impl IgnoreRules {
13    /// 创建新的 `IgnoreRules` 实例。
14    ///
15    /// # 参数
16    /// - `rules`: 忽略规则的字符串切片。
17    /// - `base_dir`: 基础目录,所有路径都将相对于此目录进行匹配。
18    ///
19    /// # 返回
20    /// `Result<Self>`: 成功时返回 `IgnoreRules` 实例,失败时返回错误信息。
21    pub fn new(rules: &[String], base_dir: &Path) -> Result<Self> {
22        let mut builder = GitignoreBuilder::new(base_dir);
23
24        for rule in rules {
25            builder
26                .add_line(None, rule)
27                .with_context(|| format!("无效的忽略规则: {}", rule))?;
28        }
29
30        let gitignore = builder.build().with_context(|| "构建忽略规则失败")?;
31
32        Ok(Self {
33            gitignore,
34            base_dir: base_dir.to_path_buf(),
35        })
36    }
37
38    /// 检查给定的路径是否应该被忽略。
39    ///
40    /// # 参数
41    /// - `path`: 要检查的路径。
42    /// - `is_dir`: 指示路径是否是目录。
43    ///
44    /// # 返回
45    /// `bool`: 如果路径应该被忽略,则返回 `true`,否则返回 `false`。
46    pub fn should_ignore(&self, path: &Path, is_dir: bool) -> bool {
47        let relative_path = match path.strip_prefix(&self.base_dir) {
48            Ok(p) => p,
49            Err(_) => return false, // 如果无法获取相对路径,则不忽略
50        };
51
52        self.gitignore
53            .matched_path_or_any_parents(relative_path, is_dir)
54            .is_ignore()
55    }
56
57    /// 过滤给定的文件路径列表,移除所有应该被忽略的文件。
58    ///
59    /// # 参数
60    /// - `files_to_filter`: 一个包含文件路径的迭代器。
61    ///
62    /// # 返回
63    /// `Result<Vec<PathBuf>>`: 包含所有不被忽略的文件路径的向量。
64    pub fn filter_files<I>(&self, files_to_filter: I) -> Result<Vec<PathBuf>>
65    where
66        I: Iterator<Item = PathBuf>,
67    {
68        let mut files = Vec::new();
69        for path in files_to_filter {
70            if !self.should_ignore(&path, path.is_dir()) {
71                files.push(path);
72            }
73        }
74        Ok(files)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use std::fs;
82    use tempfile::tempdir;
83
84    #[test]
85    fn test_ignore_rules_creation() -> Result<()> {
86        let dir = tempdir()?;
87        let base_dir = dir.path();
88        let rules = vec!["*.txt".to_string(), "temp_dir/".to_string()];
89        let ignore_rules = IgnoreRules::new(&rules, base_dir)?;
90
91        assert!(!ignore_rules.should_ignore(&base_dir.join("file.rs"), false));
92        assert!(ignore_rules.should_ignore(&base_dir.join("file.txt"), false));
93        assert!(ignore_rules.should_ignore(&base_dir.join("temp_dir"), true));
94        assert!(!ignore_rules.should_ignore(&base_dir.join("another_dir"), true));
95
96        Ok(())
97    }
98
99    #[test]
100    fn test_filter_files() -> Result<()> {
101        let dir = tempdir()?;
102        let base_dir = dir.path();
103
104        // Create some files and directories
105        fs::write(base_dir.join("file1.txt"), "content")?;
106        fs::write(base_dir.join("file2.rs"), "content")?;
107        fs::create_dir(base_dir.join("ignore_me"))?;
108        fs::write(base_dir.join("ignore_me/file3.txt"), "content")?;
109        fs::write(base_dir.join("ignore_me/file4.rs"), "content")?;
110
111        let rules = vec!["*.txt".to_string(), "ignore_me/".to_string()];
112        let ignore_rules = IgnoreRules::new(&rules, base_dir)?;
113
114        let all_files = vec![
115            base_dir.join("file1.txt"),
116            base_dir.join("file2.rs"),
117            base_dir.join("ignore_me"),
118            base_dir.join("ignore_me/file3.txt"),
119            base_dir.join("ignore_me/file4.rs"),
120        ];
121
122        let files = ignore_rules.filter_files(all_files.into_iter())?;
123
124        assert_eq!(files.len(), 1);
125        assert!(files.contains(&base_dir.join("file2.rs")));
126        assert!(!files.contains(&base_dir.join("file1.txt")));
127        assert!(!files.contains(&base_dir.join("ignore_me/file3.txt")));
128        assert!(!files.contains(&base_dir.join("ignore_me/file4.rs")));
129
130        Ok(())
131    }
132
133    #[test]
134    fn test_should_ignore_nested_dir() -> Result<()> {
135        let dir = tempdir()?;
136        let base_dir = dir.path();
137
138        fs::create_dir_all(base_dir.join("src/components"))?;
139        fs::write(base_dir.join("src/components/foo.txt"), "content")?;
140        fs::write(base_dir.join("src/components/bar.js"), "content")?;
141
142        let rules = vec!["src/components/".to_string()];
143        let ignore_rules = IgnoreRules::new(&rules, base_dir)?;
144
145        assert!(ignore_rules.should_ignore(&base_dir.join("src/components"), true));
146        assert!(ignore_rules.should_ignore(&base_dir.join("src/components/foo.txt"), false));
147        assert!(ignore_rules.should_ignore(&base_dir.join("src/components/bar.js"), false));
148
149        Ok(())
150    }
151}