use std::{
collections::{HashMap, HashSet},
io,
path::Path,
};
use bstr::{BStr, ByteSlice};
use gix_error::{Result, ResultExt, message};
use gix_ignore::glob::pattern::{Case, Mode};
use gix_index::entry::Flags;
pub fn read_patterns(git_dir: &Path) -> io::Result<Option<Vec<u8>>> {
match std::fs::read(git_dir.join("info/sparse-checkout")) {
Ok(patterns) => Ok(Some(patterns)),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
pub fn apply_patterns(
index: &mut gix_index::State,
patterns: &[u8],
source: &Path,
cone: bool,
case: Case,
) -> Result<()> {
let mut search = gix_ignore::Search::default();
search
.add_patterns_buffer(patterns, source, None, Default::default())
.or_raise(|| message("Could not parse sparse-checkout patterns"))?;
let cone = cone.then(|| Cone::from_search(&search, case)).flatten();
if cone.as_ref().is_some_and(|cone| cone.full) {
return Ok(());
}
let mut cone_directories = Directories::default();
let mut pattern_directories = Directories::default();
let mut folded_path = Vec::new();
for (entry, path) in index.entries_mut_with_paths() {
let included = match &cone {
Some(cone) => {
let path = if case == Case::Fold {
folded_path.clear();
folded_path.extend_from_slice(path);
folded_path.make_ascii_lowercase();
folded_path.as_bstr()
} else {
path
};
let parent = cone_directories.decision(parent_path(path), |path, parent| cone.decide(path, parent));
cone.decide(path, parent) != ConeDecision::Excluded
}
None => search
.pattern_matching_relative_path(path, Some(entry.mode.is_dir_for_pattern_matching()), case)
.map_or_else(
|| {
pattern_directories.decision(parent_path(path), |path, parent| {
search
.pattern_matching_relative_path(path, Some(true), case)
.map_or(parent, |matched| !matched.pattern.is_negative())
})
},
|matched| !matched.pattern.is_negative(),
),
};
if !included {
entry.flags.insert(Flags::SKIP_WORKTREE | Flags::EXTENDED);
}
}
Ok(())
}
fn parent_path(path: &BStr) -> &BStr {
path[..path.rfind_byte(b'/').unwrap_or(0)].as_bstr()
}
type DirectoryPath = Vec<u8>;
#[derive(Default)]
struct Directories<T> {
path: DirectoryPath,
decisions: Vec<(usize, T)>,
}
impl<T: Copy + Default> Directories<T> {
fn decision(&mut self, path: &BStr, mut decide: impl FnMut(&BStr, T) -> T) -> T {
if self.path.as_bstr() == path {
return self.decisions.last().map_or_else(T::default, |&(_, decision)| decision);
}
let common = self.path.iter().zip(path.iter()).take_while(|(a, b)| a == b).count();
while self
.decisions
.last()
.is_some_and(|&(end, _)| end > common || path.get(end).is_some_and(|&byte| byte != b'/'))
{
self.decisions.pop();
}
let (start, mut parent) = self
.decisions
.last()
.map_or((0, T::default()), |&(end, decision)| (end + 1, decision));
if start < path.len() {
for end in path[start..].find_iter(b"/").map(|pos| start + pos).chain([path.len()]) {
parent = decide(path[..end].as_bstr(), parent);
self.decisions.push((end, parent));
}
}
self.path.clear();
self.path.extend_from_slice(path);
parent
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq)]
enum ConeDecision {
Recursive,
#[default]
Parent,
Unselected,
Excluded,
}
#[derive(Default)]
struct Cone {
recursive: HashMap<DirectoryPath, usize>,
parents: HashSet<DirectoryPath>,
full: bool,
}
impl Cone {
fn from_search(search: &gix_ignore::Search, case: Case) -> Option<Self> {
let mut cone = Self::default();
for mapping in search.patterns.iter().flat_map(|list| &list.patterns) {
let pattern = &mapping.pattern;
let text = pattern.text.as_bytes();
let negative = pattern.is_negative();
let directory = pattern.mode.contains(Mode::MUST_BE_DIR);
if pattern.mode.contains(Mode::ABSOLUTE) && text == b"*" && negative == directory {
cone.full = !negative;
continue;
}
if !pattern.mode.contains(Mode::ABSOLUTE)
|| text.is_empty()
|| !directory
|| text.windows(2).any(|pair| pair == b"**")
{
return None;
}
for (pos, &byte) in text.iter().enumerate() {
if !matches!(byte, b'*' | b'?' | b'[' | b'\\')
|| pos > 0 && text[pos - 1] == b'\\'
|| byte == b'\\' && text.get(pos + 1).is_some_and(|next| b"*?[\\".contains(next))
|| byte == b'*' && (pos == 0 || text[pos - 1] == b'/') && pos + 1 == text.len()
{
continue;
}
return None;
}
if negative != text.ends_with(b"/*") {
return None;
}
let mut path = DirectoryPath::with_capacity(text.len());
let mut bytes = text.iter().copied();
while let Some(byte) = bytes.next() {
path.push(if byte == b'\\' { bytes.next()? } else { byte });
}
if path.len() > 1 && path.ends_with(b"/*") {
path.truncate(path.len() - 2);
}
if case == Case::Fold {
path.make_ascii_lowercase();
}
if negative {
let count = cone.recursive.get_mut(&path)?;
*count -= 1;
if *count == 0 {
cone.recursive.remove(&path);
}
cone.parents.insert(path);
} else {
if cone.parents.contains(&path) {
return None;
}
*cone.recursive.entry(path).or_default() += 1;
}
}
Some(cone)
}
fn decide(&self, path: &BStr, parent: ConeDecision) -> ConeDecision {
match parent {
ConeDecision::Recursive | ConeDecision::Excluded => parent,
_ if self.recursive.contains_key(path.as_bytes()) => ConeDecision::Recursive,
ConeDecision::Unselected => ConeDecision::Excluded,
_ if self.parents.contains(path.as_bytes()) => ConeDecision::Parent,
_ => ConeDecision::Unselected,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn directory_decisions_reuse_only_complete_shared_ancestors() {
let mut directories = Directories::<usize>::default();
let mut visited = Vec::new();
for path in ["a/b", "a/b", "a/b/c", "a/bb", "a", "ab/c", "", "a/b"] {
directories.decision(path.as_bytes().as_bstr(), |path, parent| {
visited.push(path.to_owned());
parent + 1
});
}
assert_eq!(
visited,
["a", "a/b", "a/b/c", "a/bb", "ab", "ab/c", "a", "a/b"],
"siblings reuse their ancestors, ascents retain parents, and similarly named directories don't share decisions"
);
}
}