gitoxide_core/repository/attributes/
query.rs

1use gix::worktree::IndexPersistedOrInMemory;
2
3use crate::OutputFormat;
4
5pub struct Options {
6    pub format: OutputFormat,
7    pub statistics: bool,
8}
9
10pub(crate) mod function {
11    use anyhow::bail;
12    use gix::bstr::BStr;
13    use std::borrow::Cow;
14    use std::{io, path::Path};
15
16    use crate::{
17        is_dir_to_mode,
18        repository::{
19            attributes::query::{attributes_cache, Options},
20            PathsOrPatterns,
21        },
22        OutputFormat,
23    };
24
25    pub fn query(
26        repo: gix::Repository,
27        input: PathsOrPatterns,
28        mut out: impl io::Write,
29        mut err: impl io::Write,
30        Options { format, statistics }: Options,
31    ) -> anyhow::Result<()> {
32        if format != OutputFormat::Human {
33            bail!("JSON output isn't implemented yet");
34        }
35
36        let (mut cache, index) = attributes_cache(&repo)?;
37        let mut matches = cache.attribute_matches();
38
39        match input {
40            PathsOrPatterns::Paths(paths) => {
41                for path in paths {
42                    let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref()))
43                        .metadata()
44                        .ok()
45                        .map(|m| is_dir_to_mode(m.is_dir()));
46
47                    let entry = cache.at_entry(&path, mode)?;
48                    if !entry.matching_attributes(&mut matches) {
49                        continue;
50                    }
51                    print_match(&matches, path.as_ref(), &mut out)?;
52                }
53            }
54            PathsOrPatterns::Patterns(patterns) => {
55                let mut pathspec = repo.pathspec(
56                    true,
57                    patterns.iter(),
58                    true,
59                    &index,
60                    gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
61                        .adjust_for_bare(repo.is_bare()),
62                )?;
63                let mut pathspec_matched_entry = false;
64                if let Some(it) = pathspec.index_entries_with_paths(&index) {
65                    for (path, entry) in it {
66                        pathspec_matched_entry = true;
67                        let entry = cache.at_entry(path, entry.mode.into())?;
68                        if !entry.matching_attributes(&mut matches) {
69                            continue;
70                        }
71                        print_match(&matches, path, &mut out)?;
72                    }
73                }
74
75                if !pathspec_matched_entry {
76                    // TODO(borrowchk): this shouldn't be necessary at all, but `pathspec` stays borrowed mutably for some reason.
77                    //                  It's probably due to the strange lifetimes of `index_entries_with_paths()`.
78                    let pathspec = repo.pathspec(
79                        true,
80                        patterns.iter(),
81                        true,
82                        &index,
83                        gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
84                            .adjust_for_bare(repo.is_bare()),
85                    )?;
86                    let workdir = repo.workdir();
87                    for pattern in pathspec.search().patterns() {
88                        let path = pattern.path();
89                        let entry = cache.at_entry(
90                            path,
91                            Some(is_dir_to_mode(
92                                workdir.is_some_and(|wd| wd.join(gix::path::from_bstr(path)).is_dir())
93                                    || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR),
94                            )),
95                        )?;
96                        if !entry.matching_attributes(&mut matches) {
97                            continue;
98                        }
99                        print_match(&matches, path, &mut out)?;
100                    }
101                }
102            }
103        }
104
105        if let Some(stats) = statistics.then(|| cache.take_statistics()) {
106            out.flush()?;
107            writeln!(err, "{stats:#?}").ok();
108        }
109        Ok(())
110    }
111
112    fn print_match(
113        matches: &gix::attrs::search::Outcome,
114        path: &BStr,
115        mut out: impl std::io::Write,
116    ) -> std::io::Result<()> {
117        for m in matches.iter() {
118            writeln!(
119                out,
120                "{}:{}:{}\t{}\t{}",
121                m.location.source.map(Path::to_string_lossy).unwrap_or_default(),
122                m.location.sequence_number,
123                m.pattern,
124                path,
125                m.assignment
126            )?;
127        }
128        Ok(())
129    }
130}
131
132pub(crate) fn attributes_cache(
133    repo: &gix::Repository,
134) -> anyhow::Result<(gix::AttributeStack<'_>, IndexPersistedOrInMemory)> {
135    let index = repo.index_or_load_from_head()?;
136    let cache = repo.attributes(
137        &index,
138        if repo.is_bare() {
139            gix::worktree::stack::state::attributes::Source::IdMapping
140        } else {
141            gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
142        },
143        gix::worktree::stack::state::ignore::Source::IdMapping,
144        None,
145    )?;
146    Ok((cache, index))
147}