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(overrides.into_iter())),
41        Default::default(),
42    )?;
43
44    match input {
45        PathsOrPatterns::Paths(paths) => {
46            for path in paths {
47                let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref()))
48                    .metadata()
49                    .ok()
50                    .map(|m| is_dir_to_mode(m.is_dir()));
51                let entry = cache.at_entry(&path, mode)?;
52                let match_ = entry
53                    .matching_exclude_pattern()
54                    .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m));
55                print_match(match_, path.as_ref(), &mut out)?;
56            }
57        }
58        PathsOrPatterns::Patterns(patterns) => {
59            let mut pathspec_matched_something = false;
60            let mut pathspec = repo.pathspec(
61                true,
62                patterns.iter(),
63                repo.workdir().is_some(),
64                &index,
65                gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
66            )?;
67
68            if let Some(it) = pathspec.index_entries_with_paths(&index) {
69                for (path, entry) in it {
70                    pathspec_matched_something = true;
71                    let entry = cache.at_entry(path, entry.mode.into())?;
72                    let match_ = entry
73                        .matching_exclude_pattern()
74                        .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m));
75                    print_match(match_, path, &mut out)?;
76                }
77            }
78
79            if !pathspec_matched_something {
80                // TODO(borrowchk): this shouldn't be necessary at all, but `pathspec` stays borrowed mutably for some reason.
81                //                  It's probably due to the strange lifetimes of `index_entries_with_paths()`.
82                let pathspec = repo.pathspec(
83                    true,
84                    patterns.iter(),
85                    repo.workdir().is_some(),
86                    &index,
87                    gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
88                        .adjust_for_bare(repo.is_bare()),
89                )?;
90                let workdir = repo.workdir();
91                for pattern in pathspec.search().patterns() {
92                    let path = pattern.path();
93                    let entry = cache.at_entry(
94                        path,
95                        Some(is_dir_to_mode(
96                            workdir.is_some_and(|wd| wd.join(gix::path::from_bstr(path)).is_dir())
97                                || pattern.signature.contains(gix::pathspec::MagicSignature::MUST_BE_DIR),
98                        )),
99                    )?;
100                    let match_ = entry
101                        .matching_exclude_pattern()
102                        .and_then(|m| (show_ignore_patterns || !m.pattern.is_negative()).then_some(m));
103                    print_match(match_, path, &mut out)?;
104                }
105            }
106        }
107    }
108
109    if let Some(stats) = statistics.then(|| cache.take_statistics()) {
110        out.flush()?;
111        writeln!(err, "{stats:#?}").ok();
112    }
113    Ok(())
114}
115
116fn print_match(
117    m: Option<gix::ignore::search::Match<'_>>,
118    path: &BStr,
119    mut out: impl std::io::Write,
120) -> std::io::Result<()> {
121    match m {
122        Some(m) => writeln!(
123            out,
124            "{}:{}:{}\t{}",
125            m.source.map(std::path::Path::to_string_lossy).unwrap_or_default(),
126            m.sequence_number,
127            m.pattern,
128            path
129        ),
130        None => writeln!(out, "::\t{path}"),
131    }
132}