1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
use gix::repository::IndexPersistedOrInMemory;

use crate::OutputFormat;

pub struct Options {
    pub format: OutputFormat,
    pub statistics: bool,
}

pub(crate) mod function {
    use std::{io, path::Path};

    use anyhow::{anyhow, bail};
    use gix::{bstr::BStr, prelude::FindExt};

    use crate::{
        repository::{
            attributes::query::{attributes_cache, Options},
            PathsOrPatterns,
        },
        OutputFormat,
    };

    pub fn query(
        repo: gix::Repository,
        input: PathsOrPatterns,
        mut out: impl io::Write,
        mut err: impl io::Write,
        Options { format, statistics }: Options,
    ) -> anyhow::Result<()> {
        if format != OutputFormat::Human {
            bail!("JSON output isn't implemented yet");
        }

        let (mut cache, index) = attributes_cache(&repo)?;
        let mut matches = cache.attribute_matches();

        match input {
            PathsOrPatterns::Paths(paths) => {
                for path in paths {
                    let is_dir = gix::path::from_bstr(path.as_ref()).metadata().ok().map(|m| m.is_dir());

                    let entry = cache.at_entry(path.as_slice(), is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?;
                    if !entry.matching_attributes(&mut matches) {
                        continue;
                    }
                    print_match(&matches, path.as_ref(), &mut out)?;
                }
            }
            PathsOrPatterns::Patterns(patterns) => {
                let mut pathspec = repo.pathspec(
                    patterns,
                    true,
                    &index,
                    gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
                        .adjust_for_bare(repo.is_bare()),
                )?;
                for (path, _entry) in pathspec
                    .index_entries_with_paths(&index)
                    .ok_or_else(|| anyhow!("Pathspec didn't match a single path in the index"))?
                {
                    let entry = cache.at_entry(path, Some(false), |oid, buf| repo.objects.find_blob(oid, buf))?;
                    if !entry.matching_attributes(&mut matches) {
                        continue;
                    }
                    print_match(&matches, path, &mut out)?;
                }
            }
        }

        if let Some(stats) = statistics.then(|| cache.take_statistics()) {
            out.flush()?;
            writeln!(err, "{stats:#?}").ok();
        }
        Ok(())
    }

    fn print_match(
        matches: &gix::attrs::search::Outcome,
        path: &BStr,
        mut out: impl std::io::Write,
    ) -> std::io::Result<()> {
        for m in matches.iter() {
            writeln!(
                out,
                "{}:{}:{}\t{}\t{}",
                m.location.source.map(Path::to_string_lossy).unwrap_or_default(),
                m.location.sequence_number,
                m.pattern,
                path,
                m.assignment
            )?;
        }
        Ok(())
    }
}

pub(crate) fn attributes_cache(
    repo: &gix::Repository,
) -> anyhow::Result<(gix::worktree::Stack, IndexPersistedOrInMemory)> {
    let index = repo.index_or_load_from_head()?;
    let cache = repo.attributes(
        &index,
        if repo.is_bare() {
            gix::worktree::stack::state::attributes::Source::IdMapping
        } else {
            gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
        },
        gix::worktree::stack::state::ignore::Source::IdMapping,
        None,
    )?;
    Ok((cache, index))
}