blob: 171e7759e5d5f2fb9e61c846de6e458d284331a2 [file] [log] [blame] [edit]
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(".") {
self.nil = true;
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)
}
/// Returns `true` is this pattern is supposed to always match, as it's either empty or designated `nil`.
/// Note that technically the pattern might still be excluded.
pub fn always_matches(&self) -> bool {
self.is_nil() || self.path.is_empty()
}
/// 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)
}
}