use std::fmt;
use crate::attributes::State;
use crate::display::quote_path;
use crate::workflow::Environment;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Context {
prefix: Vec<u8>,
glob: bool,
noglob: bool,
icase: bool,
literal: bool,
excludes_from_root: bool,
}
impl Context {
pub fn new() -> Self {
Self::default()
}
pub fn from_environment(environment: &Environment) -> Result<Self, ParseError> {
let flag = |name: &'static str| -> Result<bool, ParseError> {
let Some(value) = environment.get(name) else {
return Ok(false);
};
let value = value.as_encoded_bytes().to_vec();
crate::config::values::boolean(Some(&value)).ok_or(ParseError::Environment {
variable: name,
value,
})
};
let context = Self {
prefix: Vec::new(),
glob: flag("GIT_GLOB_PATHSPECS")?,
noglob: flag("GIT_NOGLOB_PATHSPECS")?,
icase: flag("GIT_ICASE_PATHSPECS")?,
literal: flag("GIT_LITERAL_PATHSPECS")?,
excludes_from_root: false,
};
if context.glob && context.noglob {
return Err(ParseError::GlobAndNoglob);
}
Ok(context)
}
#[must_use]
pub fn with_prefix(mut self, prefix: &[u8]) -> Self {
self.prefix = prefix
.split(|byte| *byte == b'/')
.filter(|component| !component.is_empty())
.collect::<Vec<_>>()
.join(&b'/');
self
}
#[must_use]
pub fn with_glob(mut self, glob: bool) -> Self {
self.glob = glob;
self
}
#[must_use]
pub fn with_noglob(mut self, noglob: bool) -> Self {
self.noglob = noglob;
self
}
#[must_use]
pub fn with_icase(mut self, icase: bool) -> Self {
self.icase = icase;
self
}
#[must_use]
pub fn with_literal(mut self, literal: bool) -> Self {
self.literal = literal;
self
}
#[must_use]
pub fn with_excludes_from_root(mut self, excludes_from_root: bool) -> Self {
self.excludes_from_root = excludes_from_root;
self
}
pub fn prefix(&self) -> &[u8] {
&self.prefix
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Pathspec {
includes: Vec<Item>,
excludes: Vec<Item>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Prune {
Skip,
Descend,
All,
}
impl Pathspec {
pub fn all() -> Self {
Self::default()
}
pub fn parse<I>(specs: I, context: &Context) -> Result<Self, ParseError>
where
I: IntoIterator,
I::Item: AsRef<[u8]>,
{
let mut pathspec = Self::default();
for spec in specs {
let item = Item::parse(spec.as_ref(), context)?;
if item.exclude {
pathspec.excludes.push(item);
} else {
pathspec.includes.push(item);
}
}
if pathspec.includes.is_empty() && !pathspec.excludes.is_empty() {
let start = if context.excludes_from_root {
Vec::new()
} else {
context.prefix.clone()
};
pathspec.includes.push(Item::literal(start));
}
Ok(pathspec)
}
pub fn is_all(&self) -> bool {
self.excludes.is_empty()
&& self
.includes
.iter()
.all(|item| item.pattern.is_empty() && item.attributes.is_empty())
}
pub fn matches(&self, path: &[u8]) -> Result<bool, AttributesRequired> {
self.matches_with(path, |_| Err(AttributesRequired))
}
pub(crate) fn matches_unchecked(&self, path: &[u8]) -> bool {
debug_assert!(!self.uses_attributes());
self.matches(path).unwrap_or(false)
}
pub fn matches_with<E>(
&self,
path: &[u8],
mut satisfied: impl FnMut(&Requirement) -> Result<bool, E>,
) -> Result<bool, E> {
let mut included = self.includes.is_empty();
for item in &self.includes {
if item.matches(path, &mut satisfied)? {
included = true;
break;
}
}
if !included {
return Ok(false);
}
for item in &self.excludes {
if item.matches(path, &mut satisfied)? {
return Ok(false);
}
}
Ok(true)
}
pub fn uses_attributes(&self) -> bool {
self.includes
.iter()
.chain(&self.excludes)
.any(|item| !item.attributes.is_empty())
}
pub fn directory(&self, directory: &[u8]) -> Prune {
if self.includes.is_empty() && self.excludes.is_empty() {
return Prune::All;
}
let mut excluded = Prune::Skip;
for item in &self.excludes {
excluded = excluded.max(item.directory(directory));
if excluded == Prune::All {
return Prune::Skip;
}
}
let included = self
.includes
.iter()
.map(|item| item.directory(directory))
.max()
.unwrap_or(Prune::All);
if included == Prune::All && excluded == Prune::Descend {
Prune::Descend
} else {
included
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct Item {
pattern: Vec<u8>,
literal_len: usize,
exclude: bool,
icase: bool,
glob: bool,
attributes: Vec<Requirement>,
fixed: usize,
}
impl Item {
fn literal(pattern: Vec<u8>) -> Self {
Self {
fixed: pattern.len(),
literal_len: pattern.len(),
pattern,
exclude: false,
icase: false,
glob: false,
attributes: Vec::new(),
}
}
fn parse(spec: &[u8], context: &Context) -> Result<Self, ParseError> {
if spec.is_empty() {
return Err(ParseError::Empty);
}
let error = |reason| ParseError::Invalid {
spec: spec.to_vec(),
reason,
};
let mut magic = Magic::default();
let path = if context.literal {
magic.literal = true;
spec
} else {
magic.parse(spec).map_err(error)?
};
if magic.glob && magic.literal {
return Err(error(Reason::GlobWithLiteral));
}
let literal = magic.literal || (context.noglob && !magic.glob);
let glob = !literal && (magic.glob || context.glob);
let joined;
let path = if magic.top || context.prefix.is_empty() {
path
} else {
joined = [&context.prefix[..], b"/", path].concat();
&joined
};
let pattern = normalize(path).ok_or_else(|| error(Reason::OutsideRepository))?;
let fixed = if magic.top || context.prefix.is_empty() {
0
} else {
fixed_prefix(&pattern, &context.prefix)
};
let literal_len = if literal {
pattern.len()
} else {
pattern[fixed..]
.iter()
.position(|byte| matches!(byte, b'*' | b'?' | b'[' | b'\\'))
.map_or(pattern.len(), |position| fixed + position)
};
Ok(Self {
pattern,
literal_len,
exclude: magic.exclude,
icase: magic.icase || context.icase,
glob,
attributes: magic.attributes.unwrap_or_default(),
fixed,
})
}
fn has_wildcards(&self) -> bool {
self.literal_len < self.pattern.len()
}
fn matches<E>(
&self,
path: &[u8],
satisfied: &mut impl FnMut(&Requirement) -> Result<bool, E>,
) -> Result<bool, E> {
if !self.matches_pattern(path) {
return Ok(false);
}
for requirement in &self.attributes {
if !satisfied(requirement)? {
return Ok(false);
}
}
Ok(true)
}
fn matches_pattern(&self, path: &[u8]) -> bool {
let prefix = &self.pattern[..self.literal_len];
if !path
.get(..prefix.len())
.is_some_and(|start| self.equal(start, prefix))
{
return false;
}
if self.names(path) {
return true;
}
self.has_wildcards()
&& crate::wildmatch::matches_with_pathname(
&self.pattern[self.fixed..],
&path[self.fixed..],
self.icase,
usize::MAX,
self.glob,
)
.unwrap_or(false)
}
fn names(&self, path: &[u8]) -> bool {
let pattern = &self.pattern[..];
if pattern.is_empty() {
return true;
}
let Some(start) = path.get(..pattern.len()) else {
return false;
};
self.equal(start, pattern)
&& (pattern.ends_with(b"/")
|| path.len() == pattern.len()
|| path.get(pattern.len()) == Some(&b'/'))
}
fn directory(&self, directory: &[u8]) -> Prune {
let prune = self.directory_pattern(directory);
if prune == Prune::All && !self.attributes.is_empty() {
Prune::Descend
} else {
prune
}
}
fn directory_pattern(&self, directory: &[u8]) -> Prune {
let named = self.pattern.strip_suffix(b"/").unwrap_or(&self.pattern);
if named.is_empty() || self.below(directory, named) {
return Prune::All;
}
let mut inside = directory.to_vec();
if !inside.is_empty() {
inside.push(b'/');
}
if self.above(&inside, named) {
return Prune::Descend;
}
let prefix = &self.pattern[..self.literal_len];
if self.has_wildcards() && (self.above(prefix, &inside) || self.above(&inside, prefix)) {
Prune::Descend
} else {
Prune::Skip
}
}
fn below(&self, directory: &[u8], named: &[u8]) -> bool {
directory.get(..named.len()).is_some_and(|start| {
self.equal(start, named)
&& (directory.len() == named.len() || directory.get(named.len()) == Some(&b'/'))
})
}
fn above(&self, short: &[u8], long: &[u8]) -> bool {
long.get(..short.len())
.is_some_and(|start| self.equal(start, short))
}
fn equal(&self, left: &[u8], right: &[u8]) -> bool {
if left.len() != right.len() {
return false;
}
let fixed = self.fixed.min(left.len());
let (left_fixed, left_rest) = left.split_at(fixed);
let (right_fixed, right_rest) = right.split_at(fixed);
left_fixed == right_fixed
&& if self.icase {
left_rest.eq_ignore_ascii_case(right_rest)
} else {
left_rest == right_rest
}
}
}
#[derive(Default)]
struct Magic {
top: bool,
exclude: bool,
icase: bool,
literal: bool,
glob: bool,
attributes: Option<Vec<Requirement>>,
}
impl Magic {
fn parse<'a>(&mut self, spec: &'a [u8]) -> Result<&'a [u8], Reason> {
let Some(rest) = spec.strip_prefix(b":") else {
return Ok(spec);
};
if let Some(long) = rest.strip_prefix(b"(") {
let mut start = 0;
let mut index = 0;
loop {
match long.get(index) {
None => return Err(Reason::UnclosedMagic),
Some(b'\\') => index += 2,
Some(b',') => {
self.word(&long[start..index])?;
index += 1;
start = index;
}
Some(b')') => {
self.word(&long[start..index])?;
return Ok(&long[index + 1..]);
}
Some(_) => index += 1,
}
}
}
let mut rest = rest;
while let [byte, after @ ..] = rest {
match byte {
b'/' => self.top = true,
b'!' | b'^' => self.exclude = true,
b':' => return Ok(after),
_ => break,
}
rest = after;
}
Ok(rest)
}
fn word(&mut self, word: &[u8]) -> Result<(), Reason> {
match word {
b"" => {}
b"top" => self.top = true,
b"exclude" => self.exclude = true,
b"icase" => self.icase = true,
b"literal" => self.literal = true,
b"glob" => self.glob = true,
_ if let Some(list) = word.strip_prefix(b"attr:") => {
if self.attributes.is_some() {
return Err(Reason::MultipleAttr);
}
self.attributes = Some(Requirement::parse_list(list)?);
}
_ => return Err(Reason::UnknownMagic(word.to_vec())),
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Requirement {
name: Vec<u8>,
expected: Expected,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
enum Expected {
Set,
Unset,
Unspecified,
Value(Vec<u8>),
}
impl Requirement {
pub fn name(&self) -> &[u8] {
&self.name
}
pub fn accepts(&self, state: State<'_>) -> bool {
match (&self.expected, state) {
(Expected::Set, State::Set)
| (Expected::Unset, State::Unset)
| (Expected::Unspecified, State::Unspecified) => true,
(Expected::Value(expected), State::Value(actual)) => expected == actual,
_ => false,
}
}
fn parse_list(list: &[u8]) -> Result<Vec<Self>, Reason> {
let requirements = list
.split(|byte| *byte == b' ')
.filter(|word| !word.is_empty())
.map(Self::parse)
.collect::<Result<Vec<_>, _>>()?;
if requirements.is_empty() {
return Err(Reason::EmptyAttr);
}
Ok(requirements)
}
fn parse(word: &[u8]) -> Result<Self, Reason> {
let (name, expected) = match word {
[b'-', name @ ..] => (name, Expected::Unset),
[b'!', name @ ..] => (name, Expected::Unspecified),
_ => match crate::bytes::split_once(word, b'=') {
Some((name, value)) => (name, Expected::Value(unescape(value)?)),
None => (word, Expected::Set),
},
};
if !crate::attributes::valid_name(name) {
return Err(Reason::InvalidAttrName(name.to_vec()));
}
Ok(Self {
name: name.to_vec(),
expected,
})
}
}
fn unescape(value: &[u8]) -> Result<Vec<u8>, Reason> {
let mut unescaped = Vec::with_capacity(value.len());
let mut bytes = value.iter();
while let Some(byte) = bytes.next() {
if *byte == b'\\' {
unescaped.push(*bytes.next().ok_or(Reason::TrailingEscape)?);
} else {
unescaped.push(*byte);
}
}
Ok(unescaped)
}
fn fixed_prefix(pattern: &[u8], prefix: &[u8]) -> usize {
let mut fixed = 0;
for component in prefix.split(|byte| *byte == b'/') {
let end = fixed + component.len();
if pattern.get(fixed..end) != Some(component)
|| !matches!(pattern.get(end), None | Some(b'/'))
{
break;
}
fixed = (end + 1).min(pattern.len());
}
fixed
}
fn normalize(path: &[u8]) -> Option<Vec<u8>> {
if path.starts_with(b"/") {
return None;
}
let mut components: Vec<&[u8]> = Vec::new();
let mut directory = false;
for component in path.split(|byte| *byte == b'/') {
directory = true;
match component {
b"" => {}
b"." => directory = false,
b".." => {
components.pop()?;
directory = false;
}
_ => {
components.push(component);
directory = false;
}
}
}
let mut normalized = components.join(&b'/');
if directory && !normalized.is_empty() {
normalized.push(b'/');
}
Some(normalized)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[error("pathspec has attr: magic; match it with attribute lookup")]
pub struct AttributesRequired;
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ParseError {
#[error("empty string is not a valid pathspec; use . to match all paths")]
Empty,
#[error("invalid pathspec {}: {reason}", quote_path(spec))]
Invalid {
spec: Vec<u8>,
reason: Reason,
},
#[error("bad boolean value {} for {variable}", quote_path(value))]
Environment {
variable: &'static str,
value: Vec<u8>,
},
#[error("global glob and noglob pathspec settings are incompatible")]
GlobAndNoglob,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Reason {
UnclosedMagic,
UnknownMagic(Vec<u8>),
GlobWithLiteral,
MultipleAttr,
EmptyAttr,
InvalidAttrName(Vec<u8>),
TrailingEscape,
OutsideRepository,
}
impl fmt::Display for Reason {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnclosedMagic => formatter.write_str("missing ')' after pathspec magic"),
Self::UnknownMagic(word) => write!(formatter, "unknown magic '{}'", quote_path(word)),
Self::GlobWithLiteral => formatter.write_str("'glob' and 'literal' are incompatible"),
Self::MultipleAttr => formatter.write_str("only one 'attr:' specification is allowed"),
Self::EmptyAttr => formatter.write_str("'attr:' must name an attribute"),
Self::InvalidAttrName(name) => {
write!(formatter, "invalid attribute name '{}'", quote_path(name))
}
Self::TrailingEscape => formatter.write_str("'attr:' value ends with '\\'"),
Self::OutsideRepository => formatter.write_str("path is outside the repository"),
}
}
}
#[cfg(test)]
mod tests;