Skip to main content

weavatrix_scan/ignore/repository/
configuration.rs

1use super::{
2    Error, HashMap, IgnoreRules, IgnoreSourceKind, OverrideRules, Path, RepositoryMatcher, Result,
3    ScanOptions, SourceRank, find_repository_root, gitconfig_excludes_path,
4    normalized_evidence_location, normalized_relative_path, resolve_git_directory, source_enabled,
5};
6
7impl RepositoryMatcher {
8    /// Builds a matcher with the scanner's reproducible default policy.
9    ///
10    /// # Errors
11    ///
12    /// Returns an error when the root cannot be resolved or inspected.
13    pub fn new(root: impl AsRef<Path>) -> Result<Self> {
14        Self::with_options(root, &ScanOptions::default())
15    }
16
17    /// Builds a matcher from scanner selection options.
18    ///
19    /// # Errors
20    ///
21    /// Returns an error for an invalid root or, under `ErrorPolicy::Abort`,
22    /// when a configured ignore source cannot be read.
23    pub fn with_options(root: impl AsRef<Path>, options: &ScanOptions) -> Result<Self> {
24        let requested = root.as_ref();
25        let scan_root = requested
26            .canonicalize()
27            .map_err(|source| Error::io(requested, source))?;
28        if !scan_root.is_dir() {
29            return Err(Error::InvalidRoot(scan_root));
30        }
31        let repository_root = find_repository_root(&scan_root);
32        let git_sources_enabled = !options.ignore_policy.require_git || repository_root.is_some();
33        let match_root = if options.ignore_policy.parent_rules {
34            repository_root.clone().unwrap_or_else(|| scan_root.clone())
35        } else {
36            scan_root.clone()
37        };
38        let (overrides, override_errors, override_evidence) =
39            OverrideRules::new(&options.override_rules, options.ignore_case_insensitive);
40        let mut matcher = Self {
41            scan_base: scan_root
42                .strip_prefix(&match_root)
43                .map_or_else(|_| String::new(), normalized_relative_path),
44            scan_root,
45            options: options.clone(),
46            match_root,
47            ignore_files: options
48                .ignore_files
49                .iter()
50                .filter(|name| source_enabled(name, &options.ignore_policy, git_sources_enabled))
51                .cloned()
52                .collect(),
53            case_insensitive: options.ignore_case_insensitive,
54            error_policy: options.walk.error_policy,
55            overrides,
56            skip_hidden: options.skip_hidden,
57            base_rules: IgnoreRules::default(),
58            directories: HashMap::new(),
59            sources: override_evidence.into_iter().collect(),
60            warnings: Vec::new(),
61            portable: true,
62        };
63        matcher.handle_errors(override_errors)?;
64        matcher.load_configured_sources(
65            options,
66            repository_root.as_deref(),
67            git_sources_enabled,
68        )?;
69        let root = matcher.match_root.clone();
70        matcher.prepare_directory_inner(&root)?;
71        if matcher.scan_root != matcher.match_root {
72            let scan_root = matcher.scan_root.clone();
73            matcher.prepare_directory_inner(&scan_root)?;
74        }
75        Ok(matcher)
76    }
77
78    fn load_configured_sources(
79        &mut self,
80        options: &ScanOptions,
81        repository_root: Option<&Path>,
82        git_sources_enabled: bool,
83    ) -> Result<()> {
84        if git_sources_enabled
85            && options.ignore_policy.git_global
86            && let Some(path) = gitconfig_excludes_path(repository_root)
87        {
88            self.load_static_source(
89                &path,
90                "",
91                SourceRank::GitGlobal,
92                IgnoreSourceKind::GitGlobal,
93                "<git-global>",
94            )?;
95            self.portable &= !path.is_file();
96        }
97        if git_sources_enabled
98            && options.ignore_policy.git_exclude
99            && let Some(git_directory) = repository_root.and_then(resolve_git_directory)
100        {
101            let path = git_directory.join("info").join("exclude");
102            self.load_static_source(
103                &path,
104                "",
105                SourceRank::GitExclude,
106                IgnoreSourceKind::GitExclude,
107                ".git/info/exclude",
108            )?;
109            self.portable &= !path.is_file();
110        }
111        for path in &options.ignore_policy.explicit_files {
112            self.load_explicit_source(path)?;
113        }
114        Ok(())
115    }
116
117    fn load_explicit_source(&mut self, path: &Path) -> Result<()> {
118        let configured = if path.is_absolute() {
119            path.to_path_buf()
120        } else {
121            self.scan_root.join(path)
122        };
123        let absolute = configured.canonicalize().unwrap_or(configured);
124        let location = normalized_evidence_location(&absolute, &self.match_root);
125        let base = self
126            .scan_root
127            .strip_prefix(&self.match_root)
128            .map_or_else(|_| String::new(), normalized_relative_path);
129        self.load_static_source(
130            &absolute,
131            &base,
132            SourceRank::Explicit,
133            IgnoreSourceKind::Explicit,
134            &location,
135        )?;
136        if absolute.is_file() && !absolute.starts_with(&self.match_root) {
137            self.portable = false;
138        }
139        Ok(())
140    }
141}