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
114
115
116
117
use gix::odb::FindExt;

use crate::OutputFormat;

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

pub(crate) mod function {
    use std::io;

    use anyhow::bail;
    use gix::prelude::FindExt;

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

    pub fn query(
        repo: gix::Repository,
        pathspecs: impl Iterator<Item = gix::path::Spec>,
        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 = attributes_cache(&repo)?;
        let prefix = repo.prefix().expect("worktree - we have an index by now")?;
        let mut matches = cache.attribute_matches();

        for mut spec in pathspecs {
            for path in spec.apply_prefix(&prefix).items() {
                let is_dir = gix::path::from_bstr(path).metadata().ok().map(|m| m.is_dir());
                let entry = cache.at_entry(path, is_dir, |oid, buf| repo.objects.find_blob(oid, buf))?;

                if !entry.matching_attributes(&mut matches) {
                    continue;
                }
                for m in matches.iter() {
                    writeln!(
                        out,
                        "{}:{}:{}\t{}\t{}",
                        m.location.source.map(|p| p.to_string_lossy()).unwrap_or_default(),
                        m.location.sequence_number,
                        m.pattern,
                        path,
                        m.assignment
                    )?;
                }
            }
        }

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

pub(crate) enum Index {
    Shared(gix::worktree::Index),
    Owned(gix::index::File),
}

impl std::ops::Deref for Index {
    type Target = gix::index::File;

    fn deref(&self) -> &Self::Target {
        match self {
            Index::Shared(i) => i,
            Index::Owned(i) => i,
        }
    }
}

impl Index {
    pub fn into_owned(self) -> gix::index::File {
        match self {
            Index::Shared(i) => gix::index::File::clone(&i),
            Index::Owned(i) => i,
        }
    }
}

pub(crate) fn index_on_demand(repo: &gix::Repository) -> anyhow::Result<Index> {
    Ok(match repo.index() {
        Ok(index) => Index::Shared(index),
        Err(gix::worktree::open_index::Error::IndexFile(_)) => {
            let tree = repo.head_commit()?.tree_id()?;
            Index::Owned(gix::index::File::from_state(
                gix::index::State::from_tree(&tree, |oid, buf| repo.objects.find_tree_iter(oid, buf).ok())?,
                repo.git_dir().join("index"),
            ))
        }
        Err(err) => return Err(err.into()),
    })
}

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