use std::collections::BTreeSet;
use crate::wildmatch::{wildmatch, WM_PATHNAME};
#[derive(Debug, Clone)]
pub struct NonConePatterns {
lines: Vec<String>,
}
impl NonConePatterns {
#[must_use]
pub fn from_lines(lines: Vec<String>) -> Self {
Self { lines }
}
#[must_use]
pub fn lines(&self) -> &[String] {
&self.lines
}
#[must_use]
pub fn parse(content: &str) -> Self {
let lines = content
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(String::from)
.collect();
Self { lines }
}
#[must_use]
pub fn path_included(&self, path: &str) -> bool {
let mut included = false;
for raw in &self.lines {
let (negated, core) = match raw.strip_prefix('!') {
Some(rest) => (true, rest),
None => (false, raw.as_str()),
};
let core = core.trim();
if core.is_empty() || core.starts_with('#') {
continue;
}
if non_cone_line_matches(core, path) {
included = !negated;
}
}
included
}
}
fn glob_special_unescaped(name: &[u8]) -> bool {
let mut i = 0usize;
while i < name.len() {
if name[i] == b'\\' {
i += 2;
continue;
}
if matches!(name[i], b'*' | b'?' | b'[') {
return true;
}
i += 1;
}
false
}
fn sparse_glob_match_star_crosses_slash(pattern: &[u8], text: &[u8]) -> bool {
if pattern.contains(&b'[') || pattern.contains(&b'\\') {
return wildmatch(pattern, text, 0);
}
let (mut pi, mut ti) = (0usize, 0usize);
let (mut star_p, mut star_t) = (usize::MAX, 0usize);
while ti < text.len() {
if pi < pattern.len() && (pattern[pi] == b'?' || pattern[pi] == text[ti]) {
pi += 1;
ti += 1;
} else if pi < pattern.len() && pattern[pi] == b'*' {
star_p = pi;
star_t = ti;
pi += 1;
} else if star_p != usize::MAX {
pi = star_p + 1;
star_t += 1;
ti = star_t;
} else {
return false;
}
}
while pi < pattern.len() && pattern[pi] == b'*' {
pi += 1;
}
pi == pattern.len()
}
fn sparse_pattern_matches_git_non_cone(pattern: &str, path: &str) -> bool {
let pat = pattern.trim();
if pat.is_empty() {
return false;
}
let anchored = pat.starts_with('/');
let pat = pat.trim_start_matches('/');
if let Some(dir) = pat.strip_suffix('/') {
if anchored && dir == "*" {
return path.contains('/');
}
if anchored {
return path == dir || path.starts_with(&format!("{dir}/"));
}
return path == dir
|| path.starts_with(&format!("{dir}/"))
|| path.split('/').any(|component| component == dir);
}
if anchored {
return sparse_glob_match_star_crosses_slash(pat.as_bytes(), path.as_bytes());
}
sparse_glob_match_star_crosses_slash(pat.as_bytes(), path.as_bytes())
|| path.rsplit('/').next().is_some_and(|base| {
sparse_glob_match_star_crosses_slash(pat.as_bytes(), base.as_bytes())
})
}
fn non_cone_line_matches(pattern: &str, path: &str) -> bool {
sparse_pattern_matches_git_non_cone(pattern, path)
}
#[derive(Debug, Clone, Default)]
pub struct ConePatterns {
pub full_cone: bool,
pub recursive_slash: BTreeSet<String>,
pub parent_slash: BTreeSet<String>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ConeMatch {
Undecided,
Matched,
MatchedRecursive,
NotMatched,
}
impl ConePatterns {
#[must_use]
pub fn try_parse_with_warnings(content: &str, warnings: &mut Vec<String>) -> Option<Self> {
let lines: Vec<&str> = content
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
let mut full_cone = false;
let mut recursive: BTreeSet<String> = BTreeSet::new();
let mut parents: BTreeSet<String> = BTreeSet::new();
for line in lines {
let (negated, rest) = if let Some(r) = line.strip_prefix('!') {
(true, r)
} else {
(false, line)
};
if negated && (rest == "/*" || rest == "/*/") {
full_cone = false;
continue;
}
if !negated && rest == "/*" {
full_cone = true;
continue;
}
if negated && rest.ends_with("/*/") && rest.starts_with('/') && rest.len() > 4 {
let inner = &rest[1..rest.len() - 3];
if inner.is_empty()
|| inner.contains('/')
|| glob_special_unescaped(inner.as_bytes())
{
warnings.push(format!("warning: unrecognized negative pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
let key = format!("/{inner}");
if !recursive.contains(&key) {
warnings.push(format!("warning: unrecognized negative pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
recursive.remove(&key);
parents.insert(key);
continue;
}
if negated {
warnings.push(format!("warning: unrecognized negative pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
if rest == "/*" {
continue;
}
if !rest.starts_with('/') {
warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
if rest.contains("**") {
warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
if rest.len() < 2 {
warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
let must_be_dir = rest.ends_with('/');
let body = rest[1..].trim_end_matches('/');
if body.is_empty() {
warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
if !must_be_dir {
warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
if glob_special_unescaped(body.as_bytes()) {
warnings.push(format!("warning: unrecognized pattern: '{rest}'"));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
let key = format!("/{body}");
if parents.contains(&key) {
warnings.push(format!(
"warning: your sparse-checkout file may have issues: pattern '{rest}' is repeated"
));
warnings.push("warning: disabling cone pattern matching".to_string());
return None;
}
recursive.insert(key.clone());
let parts: Vec<&str> = body.split('/').collect();
for i in 1..parts.len() {
let prefix = parts[..i].join("/");
parents.insert(format!("/{prefix}"));
}
}
Some(ConePatterns {
full_cone,
recursive_slash: recursive,
parent_slash: parents,
})
}
#[must_use]
pub fn try_parse(content: &str) -> Option<Self> {
let mut w = Vec::new();
Self::try_parse_with_warnings(content, &mut w)
}
fn recursive_contains_parent(path: &str, recursive: &BTreeSet<String>) -> bool {
let mut buf = String::from("/");
buf.push_str(path);
let mut slash_pos = buf.rfind('/');
while let Some(pos) = slash_pos {
if pos == 0 {
break;
}
buf.truncate(pos);
if recursive.contains(&buf) {
return true;
}
slash_pos = buf.rfind('/');
}
false
}
fn path_matches_pattern_list(&self, pathname: &str) -> ConeMatch {
if self.full_cone {
return ConeMatch::Matched;
}
let mut parent_pathname = String::with_capacity(pathname.len() + 2);
parent_pathname.push('/');
parent_pathname.push_str(pathname);
let slash_pos = if parent_pathname.ends_with('/') {
let sp = parent_pathname.len() - 1;
parent_pathname.push('-');
sp
} else {
parent_pathname.rfind('/').unwrap_or(0)
};
if self.recursive_slash.contains(&parent_pathname) {
return ConeMatch::MatchedRecursive;
}
if slash_pos == 0 {
return ConeMatch::Matched;
}
let parent_key = parent_pathname[..slash_pos].to_string();
if self.parent_slash.contains(&parent_key) {
return ConeMatch::Matched;
}
if Self::recursive_contains_parent(pathname, &self.recursive_slash) {
return ConeMatch::MatchedRecursive;
}
ConeMatch::NotMatched
}
#[must_use]
pub fn path_included(&self, path: &str) -> bool {
if path.is_empty() {
return true;
}
let bytes = path.as_bytes();
let mut end = bytes.len();
let mut match_result = ConeMatch::Undecided;
while end > 0 && match_result == ConeMatch::Undecided {
let slice = path.get(..end).unwrap_or("");
match_result = self.path_matches_pattern_list(slice);
let mut slash = end.saturating_sub(1);
while slash > 0 && bytes[slash] != b'/' {
slash -= 1;
}
end = if bytes.get(slash) == Some(&b'/') {
slash
} else {
0
};
}
matches!(
match_result,
ConeMatch::Matched | ConeMatch::MatchedRecursive
)
}
}
#[must_use]
pub fn load_sparse_checkout(
git_dir: &std::path::Path,
cone_config: bool,
) -> (bool, Option<ConePatterns>, NonConePatterns) {
let mut w = Vec::new();
load_sparse_checkout_with_warnings(git_dir, cone_config, &mut w)
}
pub fn load_sparse_checkout_with_warnings(
git_dir: &std::path::Path,
cone_config: bool,
warnings: &mut Vec<String>,
) -> (bool, Option<ConePatterns>, NonConePatterns) {
let path = git_dir.join("info").join("sparse-checkout");
let Ok(content) = std::fs::read_to_string(&path) else {
return (false, None, NonConePatterns { lines: Vec::new() });
};
let non_cone = NonConePatterns::parse(&content);
if !cone_config {
return (false, None, non_cone);
}
match ConePatterns::try_parse_with_warnings(&content, warnings) {
Some(cone) => (true, Some(cone), non_cone),
None => (false, None, non_cone),
}
}
#[must_use]
pub fn path_in_sparse_checkout(
path: &str,
cone_config: bool,
cone: Option<&ConePatterns>,
non_cone: &NonConePatterns,
work_tree: Option<&std::path::Path>,
) -> bool {
if cone_config {
if let Some(c) = cone {
return c.path_included(path);
}
}
crate::ignore::path_in_sparse_checkout(path, non_cone.lines(), work_tree)
}
pub fn apply_sparse_checkout_skip_worktree(
git_dir: &std::path::Path,
work_tree: Option<&std::path::Path>,
index: &mut crate::index::Index,
skip_sparse_checkout: bool,
) {
if skip_sparse_checkout {
return;
}
let config = crate::config::ConfigSet::load(Some(git_dir), true)
.unwrap_or_else(|_| crate::config::ConfigSet::new());
let sparse_enabled = config
.get("core.sparsecheckout")
.map(|v| v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !sparse_enabled {
return;
}
let cone_config = config
.get("core.sparsecheckoutcone")
.map(|v| v.eq_ignore_ascii_case("true"))
.unwrap_or(true);
let mut warnings = Vec::new();
let (_cone_ok, _cone_loaded, non_cone) =
load_sparse_checkout_with_warnings(git_dir, cone_config, &mut warnings);
for line in warnings {
eprintln!("{line}");
}
let sparse_path = git_dir.join("info").join("sparse-checkout");
let file_content = std::fs::read_to_string(&sparse_path).unwrap_or_default();
let sparse_lines = parse_sparse_checkout_file(&file_content);
let cone_struct = if cone_config {
ConePatterns::try_parse(&file_content)
} else {
None
};
let effective_cone = cone_config && cone_struct.is_some();
let sparse_file_exists = sparse_path.is_file();
let exclude_all = sparse_file_exists && sparse_lines.is_empty();
let mut any_skip = false;
for entry in &mut index.entries {
if entry.stage() != 0 {
continue;
}
let path_str = String::from_utf8_lossy(&entry.path);
let included = if exclude_all {
false
} else if effective_cone {
path_in_sparse_checkout(
path_str.as_ref(),
true,
cone_struct.as_ref(),
&non_cone,
work_tree,
)
} else {
crate::ignore::path_in_sparse_checkout(path_str.as_ref(), non_cone.lines(), work_tree)
};
entry.set_skip_worktree(!included);
if !included {
any_skip = true;
}
}
if any_skip && index.version < 3 {
index.version = 3;
}
}
fn max_common_dir_prefix(path1: &str, path2: &str) -> usize {
let b1 = path1.as_bytes();
let b2 = path2.as_bytes();
let mut common_prefix = 0usize;
let mut i = 0usize;
while i < b1.len() && i < b2.len() {
if b1[i] != b2[i] {
break;
}
if b1[i] == b'/' {
common_prefix = i + 1;
}
i += 1;
}
common_prefix
}
struct PathFoundData {
dir: String,
}
fn path_found(path: &str, data: &mut PathFoundData) -> bool {
let pb = path.as_bytes();
let db = data.dir.as_bytes();
if !db.is_empty() && pb.len() >= db.len() && pb[..db.len()] == *db {
return false;
}
if std::fs::symlink_metadata(std::path::Path::new(path)).is_ok() {
return true;
}
let common_prefix = max_common_dir_prefix(path, &data.dir);
data.dir.truncate(common_prefix);
loop {
let rest = &path[data.dir.len()..];
if let Some(rel_slash) = rest.find('/') {
data.dir.push_str(&rest[..=rel_slash]);
if std::fs::symlink_metadata(std::path::Path::new(&data.dir)).is_err() {
return false;
}
} else {
data.dir.push_str(rest);
data.dir.push('/');
break;
}
}
false
}
pub fn clear_skip_worktree_from_present_files(
git_dir: &std::path::Path,
work_tree: &std::path::Path,
index: &mut crate::index::Index,
) {
let config = crate::config::ConfigSet::load(Some(git_dir), true)
.unwrap_or_else(|_| crate::config::ConfigSet::new());
let sparse_enabled = config
.get("core.sparsecheckout")
.map(|v| v.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if !sparse_enabled {
return;
}
if config
.get_bool("sparse.expectfilesoutsideofpatterns")
.and_then(|r| r.ok())
.unwrap_or(false)
{
return;
}
let mut found = PathFoundData { dir: String::new() };
for entry in &mut index.entries {
if entry.stage() != 0 || !entry.skip_worktree() {
continue;
}
if entry.assume_unchanged() {
continue;
}
let rel = String::from_utf8_lossy(&entry.path);
let abs = work_tree.join(rel.as_ref());
let abs_str = abs.to_string_lossy().into_owned();
if path_found(&abs_str, &mut found) {
entry.set_skip_worktree(false);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConeWorkspace {
pub recursive_slash: BTreeSet<String>,
pub parent_slash: BTreeSet<String>,
}
impl ConeWorkspace {
#[must_use]
pub fn from_cone_patterns(cp: &ConePatterns) -> Self {
Self {
recursive_slash: cp.recursive_slash.clone(),
parent_slash: cp.parent_slash.clone(),
}
}
#[must_use]
pub fn from_directory_list(dirs: &[String]) -> Self {
let mut pruned: Vec<String> = dirs
.iter()
.map(|s| s.trim_start_matches('/').trim_end_matches('/').to_string())
.filter(|s| !s.is_empty())
.collect();
pruned.sort();
let mut kept: Vec<String> = Vec::new();
for d in pruned {
if kept
.iter()
.any(|p| d.starts_with(p) && d.as_bytes().get(p.len()) == Some(&b'/'))
{
continue;
}
kept.retain(|k| !(k.starts_with(&d) && k.as_bytes().get(d.len()) == Some(&b'/')));
kept.push(d);
}
let mut ws = ConeWorkspace::default();
for d in kept {
ws.insert_directory(&d);
}
ws
}
pub fn insert_directory(&mut self, rel: &str) {
let rel = rel.trim_start_matches('/');
let rel = rel.trim_end_matches('/');
if rel.is_empty() {
return;
}
let key = format!("/{rel}");
if self.parent_slash.contains(&key) {
return;
}
self.recursive_slash.insert(key.clone());
let parts: Vec<&str> = rel.split('/').collect();
for i in 1..parts.len() {
let prefix = parts[..i].join("/");
self.parent_slash.insert(format!("/{prefix}"));
}
}
fn recursive_contains_parent(path_slash: &str, recursive: &BTreeSet<String>) -> bool {
let mut buf = String::from(path_slash);
let mut slash_pos = buf.rfind('/');
while let Some(pos) = slash_pos {
if pos == 0 {
break;
}
buf.truncate(pos);
if recursive.contains(&buf) {
return true;
}
slash_pos = buf.rfind('/');
}
false
}
#[must_use]
pub fn to_sparse_checkout_file(&self) -> String {
let mut parent_only: Vec<&String> = self
.parent_slash
.iter()
.filter(|p| {
!self.recursive_slash.contains(*p)
&& !Self::recursive_contains_parent(p, &self.recursive_slash)
})
.collect();
parent_only.sort();
let mut out = String::new();
out.push_str("/*\n!/*/\n");
for p in parent_only {
let esc = escape_cone_path_component(p);
out.push_str(&esc);
out.push_str("/\n!");
out.push_str(&esc);
out.push_str("/*/\n");
}
let mut rec_only: Vec<&String> = self
.recursive_slash
.iter()
.filter(|p| !Self::recursive_contains_parent(p, &self.recursive_slash))
.collect();
rec_only.sort();
for p in rec_only {
let esc = escape_cone_path_component(p);
out.push_str(&esc);
out.push_str("/\n");
}
out
}
#[must_use]
pub fn list_cone_directories(&self) -> Vec<String> {
let mut v: Vec<String> = self
.recursive_slash
.iter()
.map(|s| s.trim_start_matches('/').to_string())
.collect();
v.sort();
v
}
}
fn escape_cone_path_component(path_with_leading_slash: &str) -> String {
let mut out = String::new();
for ch in path_with_leading_slash.chars() {
if matches!(ch, '*' | '?' | '[' | '\\') {
out.push('\\');
}
out.push(ch);
}
out
}
pub fn parse_sparse_checkout_file(content: &str) -> Vec<String> {
content
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(String::from)
.collect()
}
pub fn sparse_checkout_lines_look_like_expanded_cone(lines: &[String]) -> bool {
lines.len() >= 2 && lines[0] == "/*" && lines[1] == "!/*/"
}
fn parse_expanded_cone_parent_recursive(lines: &[String]) -> Option<(Vec<String>, Vec<String>)> {
if !sparse_checkout_lines_look_like_expanded_cone(lines) {
return None;
}
let mut parents = Vec::new();
let mut recursive = Vec::new();
let mut i = 2usize;
while i + 1 < lines.len() {
let a = &lines[i];
let b = &lines[i + 1];
if !a.starts_with('/') || !a.ends_with('/') || !b.starts_with('!') {
break;
}
let inner_a = a.trim_start_matches('/').trim_end_matches('/');
let expected_neg = format!("!/{inner_a}/*/");
if b != &expected_neg {
break;
}
parents.push(inner_a.to_string());
i += 2;
}
while i < lines.len() {
let line = &lines[i];
if line.starts_with('!') {
return None;
}
if !line.starts_with('/') || !line.ends_with('/') {
return None;
}
let body = line.trim_start_matches('/').trim_end_matches('/');
if body.is_empty() {
return None;
}
recursive.push(body.to_string());
i += 1;
}
Some((parents, recursive))
}
fn path_in_expanded_cone(path: &str, lines: &[String]) -> bool {
let Some((parents, recursive)) = parse_expanded_cone_parent_recursive(lines) else {
return false;
};
let raw = path.trim_start_matches('/');
let is_directory_path = raw.ends_with('/');
let path = raw.trim_end_matches('/');
if !path.contains('/') {
if !is_directory_path {
return true;
}
if parents.is_empty() && recursive.is_empty() {
return false;
}
return parents.iter().any(|p| p == path)
|| recursive
.iter()
.any(|r| r == path || r.starts_with(&format!("{path}/")));
}
for r in &recursive {
if path == *r || path.starts_with(&format!("{r}/")) {
return true;
}
}
for p in &parents {
let p_slash = format!("{}/", p);
if path == *p {
return true;
}
if !path.starts_with(&p_slash) {
continue;
}
let rest = &path[p_slash.len()..];
let Some(slash_pos) = rest.find('/') else {
let combined = format!("{}/{}", p, rest);
return recursive
.iter()
.any(|r| r == &combined || r.starts_with(&format!("{combined}/")));
};
let first = &rest[..slash_pos];
let combined = format!("{}/{}", p, first);
for r in &recursive {
let under_r = path == *r || path.starts_with(&format!("{r}/"));
let r_covers = r == &combined || r.starts_with(&format!("{combined}/"));
if r_covers && under_r {
return true;
}
}
}
false
}
#[must_use]
pub fn effective_cone_mode_for_sparse_file(cone_config: bool, lines: &[String]) -> bool {
cone_config && sparse_checkout_lines_look_like_expanded_cone(lines)
}
pub fn build_expanded_cone_sparse_checkout_lines(dirs: &[String]) -> Vec<String> {
let mut recursive: BTreeSet<String> = BTreeSet::new();
for d in dirs {
let t = d.trim().trim_start_matches('/').trim_end_matches('/');
if t.is_empty() {
continue;
}
recursive.insert(format!("/{t}"));
}
let mut parents: BTreeSet<String> = BTreeSet::new();
for r in &recursive {
let mut cur = r.clone();
loop {
let Some(slash) = cur.rfind('/') else {
break;
};
if slash == 0 {
break;
}
cur.truncate(slash);
parents.insert(cur.clone());
}
}
let mut out = vec!["/*".to_owned(), "!/*/".to_owned()];
for p in parents.iter() {
if recursive.contains(p) {
continue;
}
if recursive_set_has_strict_ancestor(&recursive, p) {
continue;
}
let esc = escape_cone_pattern_path(p);
out.push(format!("{esc}/"));
out.push(format!("!{esc}/*/"));
}
for r in recursive.iter() {
if recursive_set_has_strict_ancestor(&recursive, r) {
continue;
}
let esc = escape_cone_pattern_path(r);
out.push(format!("{esc}/"));
}
out
}
fn escape_cone_pattern_path(path_with_leading_slash: &str) -> String {
let mut out = String::with_capacity(path_with_leading_slash.len() + 8);
for ch in path_with_leading_slash.chars() {
match ch {
'\\' | '[' | '*' | '?' | '#' => {
out.push('\\');
out.push(ch);
}
_ => out.push(ch),
}
}
out
}
fn recursive_set_has_strict_ancestor(recursive: &BTreeSet<String>, path: &str) -> bool {
let mut cur = path.to_string();
loop {
let Some(slash) = cur.rfind('/') else {
break;
};
if slash == 0 {
break;
}
cur.truncate(slash);
if recursive.contains(&cur) {
return true;
}
}
false
}
pub fn parse_expanded_cone_recursive_dirs(lines: &[String]) -> Vec<String> {
if !sparse_checkout_lines_look_like_expanded_cone(lines) {
return Vec::new();
}
let mut i = 2usize;
let mut out = Vec::new();
while i < lines.len() {
let line = &lines[i];
if line.starts_with('!') {
i += 1;
continue;
}
if !line.ends_with('/') || !line.starts_with('/') {
i += 1;
continue;
}
let trimmed = line.trim_end_matches('/');
let body = trimmed.trim_start_matches('/');
let esc = escape_cone_pattern_path(trimmed);
let expected_neg = format!("!{esc}/*/");
if i + 1 < lines.len() && lines[i + 1] == expected_neg {
i += 2;
continue;
}
out.push(body.to_owned());
i += 1;
}
out
}
#[must_use]
pub fn cone_directory_inputs_for_add(content: &str) -> Vec<String> {
let lines: Vec<String> = parse_sparse_checkout_file(content);
if sparse_checkout_lines_look_like_expanded_cone(&lines) {
let recursive = parse_expanded_cone_recursive_dirs(&lines);
if !recursive.is_empty() {
return recursive;
}
if lines.len() == 2 {
return Vec::new();
}
return lines[2..]
.iter()
.map(|s| {
s.trim()
.trim_start_matches('/')
.trim_end_matches('/')
.to_string()
})
.filter(|s| !s.is_empty())
.collect();
}
if let Some(cp) = ConePatterns::try_parse(content) {
return ConeWorkspace::from_cone_patterns(&cp).list_cone_directories();
}
lines
.iter()
.map(|s| {
s.trim()
.trim_start_matches('/')
.trim_end_matches('/')
.to_string()
})
.filter(|s| !s.is_empty())
.collect()
}
pub fn path_in_sparse_checkout_patterns(path: &str, patterns: &[String], cone_mode: bool) -> bool {
if path.is_empty() || patterns.is_empty() {
return true;
}
if sparse_checkout_lines_look_like_expanded_cone(patterns) {
return path_in_expanded_cone(path, patterns);
}
let use_cone_prefix = cone_mode;
let mut end = path.len();
while end > 0 {
if path_matches_sparse_patterns(&path[..end], patterns, use_cone_prefix) {
return true;
}
let Some(slash) = path[..end].rfind('/') else {
break;
};
end = slash;
}
false
}
pub fn path_in_cone_mode_sparse_checkout(
path: &str,
patterns: &[String],
cone_enabled: bool,
) -> bool {
if !cone_enabled || patterns.is_empty() {
return true;
}
path_in_sparse_checkout_patterns(path, patterns, true)
}
pub fn path_matches_sparse_patterns(path: &str, patterns: &[String], cone_mode: bool) -> bool {
let expanded_cone = sparse_checkout_lines_look_like_expanded_cone(patterns);
if expanded_cone {
return path_in_expanded_cone(path, patterns);
}
let raw_cone_prefix = cone_mode && !expanded_cone;
if raw_cone_prefix {
if !path.contains('/') {
return true;
}
for pattern in patterns {
let prefix = pattern.trim_end_matches('/');
if path.starts_with(prefix) && path.as_bytes().get(prefix.len()) == Some(&b'/') {
return true;
}
if path == prefix {
return true;
}
}
return false;
}
let mut included = false;
for raw_pattern in patterns {
let pattern = raw_pattern.trim();
if pattern.is_empty() || pattern.starts_with('#') {
continue;
}
let (negated, core_pattern) = if let Some(rest) = pattern.strip_prefix('!') {
(true, rest)
} else {
(false, pattern)
};
if core_pattern.is_empty() || core_pattern == "/" {
continue;
}
let matches = if let Some(prefix_with_slash) = core_pattern.strip_suffix('/') {
let inner = prefix_with_slash.trim_start_matches('/');
if inner.is_empty() {
false
} else if inner == "*" {
let trimmed = path.trim_end_matches('/');
trimmed.contains('/')
} else if inner.contains('*') || inner.contains('?') || inner.contains('[') {
let pat = format!("{prefix_with_slash}/");
let text = format!("/{path}/");
wildmatch(pat.as_bytes(), text.as_bytes(), WM_PATHNAME)
} else {
path == inner || path.starts_with(&format!("{inner}/"))
}
} else if core_pattern.starts_with('/') {
let text = format!("/{}", path.trim_start_matches('/'));
wildmatch(core_pattern.as_bytes(), text.as_bytes(), WM_PATHNAME)
} else {
wildmatch(core_pattern.as_bytes(), path.as_bytes(), WM_PATHNAME)
};
if matches {
included = !negated;
}
}
included
}
#[cfg(test)]
mod path_in_expanded_cone_tests {
use super::path_in_sparse_checkout_patterns;
#[test]
fn root_only_cone_includes_files_not_top_level_dirs() {
let lines = vec!["/*".to_string(), "!/*/".to_string()];
assert!(path_in_sparse_checkout_patterns("file.1.txt", &lines, true));
assert!(!path_in_sparse_checkout_patterns("a/", &lines, true));
assert!(!path_in_sparse_checkout_patterns("d/", &lines, true));
}
#[test]
fn expanded_cone_with_d_includes_d_tree_not_sibling_a() {
let lines = vec!["/*".to_string(), "!/*/".to_string(), "/d/".to_string()];
assert!(path_in_sparse_checkout_patterns("file.1.txt", &lines, true));
assert!(path_in_sparse_checkout_patterns("d/", &lines, true));
assert!(!path_in_sparse_checkout_patterns("a/", &lines, true));
assert!(path_in_sparse_checkout_patterns(
"d/e/file.1.txt",
&lines,
true
));
}
}
#[cfg(test)]
mod cone_directory_inputs_for_add_tests {
use super::cone_directory_inputs_for_add;
#[test]
fn expanded_header_with_non_cone_body_preserves_literal_dir() {
let content = "/*\n!/*/\ndir\n";
assert_eq!(
cone_directory_inputs_for_add(content),
vec!["dir".to_string()]
);
}
#[test]
fn pure_expanded_cone_uses_recursive_dirs_only() {
let content = "/*\n!/*/\n/sub/\n";
assert_eq!(
cone_directory_inputs_for_add(content),
vec!["sub".to_string()]
);
}
}