gitoxide_core/repository/
exclude.rs

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