gitoxide-core 0.60.0

The library implementing all capabilities of the gitoxide CLI
Documentation
use std::{borrow::Cow, io};

use anyhow::bail;
use gix::bstr::{BStr, ByteSlice};

use crate::{OutputFormat, is_dir_to_mode, repository::PathsOrPatterns};

pub mod query {
    use std::ffi::OsString;

    use crate::OutputFormat;

    pub struct Options {
        pub format: OutputFormat,
        pub overrides: Vec<OsString>,
        pub show_ignore_patterns: bool,
        pub statistics: bool,
    }
}

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

    let index = repo.index()?;
    let mut cache = repo.excludes(
        &index,
        Some(gix::ignore::Search::from_overrides(
            overrides,
            repo.ignore_pattern_parser()?,
        )),
        Default::default(),
    )?;

    let paths: Box<dyn Iterator<Item = gix::bstr::BString>> = match input {
        PathsOrPatterns::Paths(paths) => paths,
        PathsOrPatterns::Patterns(paths) => Box::new(paths.into_iter()),
    };
    for path in paths {
        let mode = gix::path::from_bstr(Cow::Borrowed(path.as_ref()))
            .metadata()
            .ok()
            .map(|m| is_dir_to_mode(m.is_dir()))
            .or_else(|| path.ends_with(b"/").then_some(gix::index::entry::Mode::DIR));
        let query_path = repo.normalize_path(&path)?;
        let entry = cache.at_entry(query_path.as_bstr(), mode)?;
        let match_ = entry
            .matching_exclude_pattern()
            .filter(|m| show_ignore_patterns || !m.pattern.is_negative());
        print_match_unless_tracked(match_, &index, query_path.as_bstr(), path.as_ref(), &mut out)?;
    }

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

fn print_match_unless_tracked(
    match_: Option<gix::ignore::search::Match<'_>>,
    index: &gix::index::State,
    query_path: &BStr,
    display_path: &BStr,
    out: impl std::io::Write,
) -> std::io::Result<()> {
    print_match(match_.filter(|_| !is_tracked(index, query_path)), display_path, out)
}

fn is_tracked(index: &gix::index::State, path: &BStr) -> bool {
    let path = path.trim_end_with(|b| b == '/').as_bstr();
    index.entry_by_path(path).is_some() || index.path_is_directory(path)
}

fn print_match(
    m: Option<gix::ignore::search::Match<'_>>,
    path: &BStr,
    mut out: impl std::io::Write,
) -> std::io::Result<()> {
    match m {
        Some(m) => writeln!(
            out,
            "{}:{}:{}\t{}",
            m.source.map(std::path::Path::to_string_lossy).unwrap_or_default(),
            m.sequence_number,
            m.pattern,
            path
        ),
        None => writeln!(out, "::\t{path}"),
    }
}