gix-pathspec 0.4.1

A crate of the gitoxide project dealing magical pathspecs
Documentation
use std::path::{Component, Path, PathBuf};

use bstr::{BStr, BString, ByteSlice, ByteVec};

use crate::{normalize, MagicSignature, Pattern, SearchMode};

/// Access
impl Pattern {
    /// Returns `true` if this seems to be a pathspec that indicates that 'there is no pathspec'.
    ///
    /// Note that such a spec is `:`.
    pub fn is_nil(&self) -> bool {
        self.nil
    }

    /// Return the prefix-portion of the `path` of this spec, which is a *directory*.
    /// It can be empty if there is no prefix.
    ///
    /// A prefix is effectively the CWD seen as relative to the working tree, and it's assumed to
    /// match case-sensitively. This makes it useful for skipping over large portions of input by
    /// directly comparing them.
    pub fn prefix_directory(&self) -> &BStr {
        self.path[..self.prefix_len].as_bstr()
    }

    /// Return the path of this spec, typically used for matching.
    pub fn path(&self) -> &BStr {
        self.path.as_ref()
    }
}

/// Mutation
impl Pattern {
    /// Normalize the pattern's path by assuring it's relative to the root of the working tree, and contains
    /// no relative path components. Further, it assures that `/` are used as path separator.
    ///
    /// If `self.path` is a relative path, it will be put in front of the pattern path if `self.signature` isn't indicating `TOP` already.
    /// If `self.path` is an absolute path, we will use `root` to make it worktree relative if possible.
    ///
    /// `prefix` can be empty, we will still normalize this pathspec to resolve relative path components, and
    /// it is assumed not to contain any relative path components, e.g. '', 'a', 'a/b' are valid.
    /// `root` is the absolute path to the root of either the worktree or the repository's `git_dir`.
    pub fn normalize(&mut self, prefix: &Path, root: &Path) -> Result<&mut Self, normalize::Error> {
        fn prefix_components_to_subtract(path: &Path) -> usize {
            let parent_component_end_bound = path.components().enumerate().fold(None::<usize>, |acc, (idx, c)| {
                matches!(c, Component::ParentDir).then_some(idx + 1).or(acc)
            });
            let count = path
                .components()
                .take(parent_component_end_bound.unwrap_or(0))
                .map(|c| match c {
                    Component::ParentDir => 1_isize,
                    Component::Normal(_) => -1,
                    _ => 0,
                })
                .sum::<isize>();
            (count > 0).then_some(count as usize).unwrap_or_default()
        }

        let mut path = gix_path::from_bstr(self.path.as_bstr());
        let mut num_prefix_components = 0;
        let mut was_absolute = false;
        if gix_path::is_absolute(path.as_ref()) {
            was_absolute = true;
            let rela_path = match path.strip_prefix(root) {
                Ok(path) => path,
                Err(_) => {
                    return Err(normalize::Error::AbsolutePathOutsideOfWorktree {
                        path: path.into_owned(),
                        worktree_path: root.into(),
                    })
                }
            };
            path = rela_path.to_owned().into();
        } else if !prefix.as_os_str().is_empty() && !self.signature.contains(MagicSignature::TOP) {
            debug_assert_eq!(
                prefix
                    .components()
                    .filter(|c| matches!(c, Component::Normal(_)))
                    .count(),
                prefix.components().count(),
                "BUG: prefixes must not have relative path components, or calculations here will be wrong so pattern won't match"
            );
            num_prefix_components = prefix
                .components()
                .count()
                .saturating_sub(prefix_components_to_subtract(path.as_ref()));
            path = prefix.join(path).into();
        }

        let assure_path_cannot_break_out_upwards = Path::new("");
        let path = match gix_path::normalize(path.as_ref().into(), assure_path_cannot_break_out_upwards) {
            Some(path) => {
                if was_absolute {
                    num_prefix_components = path.components().count().saturating_sub(
                        if self.signature.contains(MagicSignature::MUST_BE_DIR) {
                            0
                        } else {
                            1
                        },
                    );
                }
                path
            }
            None => {
                return Err(normalize::Error::OutsideOfWorktree {
                    path: path.into_owned(),
                })
            }
        };

        self.path = if path == Path::new(".") {
            BString::from(".")
        } else {
            let cleaned = PathBuf::from_iter(path.components().filter(|c| !matches!(c, Component::CurDir)));
            let mut out = gix_path::to_unix_separators_on_windows(gix_path::into_bstr(cleaned)).into_owned();
            self.prefix_len = {
                if self.signature.contains(MagicSignature::MUST_BE_DIR) {
                    out.push(b'/');
                }
                let len = out
                    .find_iter(b"/")
                    .take(num_prefix_components)
                    .last()
                    .unwrap_or_default();
                if self.signature.contains(MagicSignature::MUST_BE_DIR) {
                    out.pop();
                }
                len
            };
            out
        };

        Ok(self)
    }
}

/// Access
impl Pattern {
    /// Return `true` if this pathspec is negated, which means it will exclude an item from the result set instead of including it.
    pub fn is_excluded(&self) -> bool {
        self.signature.contains(MagicSignature::EXCLUDE)
    }

    /// Translate ourselves to a long display format, that when parsed back will yield the same pattern.
    ///
    /// Note that the
    pub fn to_bstring(&self) -> BString {
        if self.is_nil() {
            ":".into()
        } else {
            let mut buf: BString = ":(".into();
            if self.signature.contains(MagicSignature::TOP) {
                buf.push_str("top,");
            }
            if self.signature.contains(MagicSignature::EXCLUDE) {
                buf.push_str("exclude,");
            }
            if self.signature.contains(MagicSignature::ICASE) {
                buf.push_str("icase,");
            }
            match self.search_mode {
                SearchMode::ShellGlob => {}
                SearchMode::Literal => buf.push_str("literal,"),
                SearchMode::PathAwareGlob => buf.push_str("glob,"),
            }
            if self.attributes.is_empty() {
                if buf.last() == Some(&b',') {
                    buf.pop();
                }
            } else {
                buf.push_str("attr:");
                for attr in &self.attributes {
                    let attr = attr.as_ref().to_string().replace(',', "\\,");
                    buf.push_str(&attr);
                    buf.push(b' ');
                }
                buf.pop(); // trailing ' '
            }
            buf.push(b')');
            buf.extend_from_slice(&self.path);
            if self.signature.contains(MagicSignature::MUST_BE_DIR) {
                buf.push(b'/');
            }
            buf
        }
    }
}

impl std::fmt::Display for Pattern {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.to_bstring().fmt(f)
    }
}