use crate::error::{Error, Result};
use crate::git_path::pathspec_traverses_symlink_dir;
use crate::index::MODE_TREE;
use crate::objects::{parse_commit, parse_tree, ObjectId};
use crate::pathspec::{has_glob_chars, pathspec_is_exclude, sorted_paths_match_pathspec};
use crate::repo::Repository;
use crate::state::resolve_head;
use crate::unicode_normalization::resolve_worktree_path_for_staging;
use std::collections::HashSet;
pub fn pathspec_matches_known_in_repository(repo: &Repository, resolved: &str) -> Result<bool> {
pathspec_matches_known_in_paths(repo, resolved, &known_paths(repo)?)
}
pub fn ensure_pathspec_matches_known_in_repository(
repo: &Repository,
display_spec: &str,
resolved: &str,
) -> Result<()> {
if let Some(work_tree) = repo.work_tree() {
if pathspec_traverses_symlink_dir(
work_tree,
resolved,
repo.pathspec_precompose_enabled(),
repo_pathspec_ignorecase(repo),
) {
return Err(Error::PathspecBeyondSymlink {
spec: display_spec.to_owned(),
});
}
}
if pathspec_matches_known_in_repository(repo, resolved)? {
Ok(())
} else {
Err(Error::PathspecNoMatch {
spec: display_spec.to_owned(),
})
}
}
fn pathspec_matches_known_in_paths(
repo: &Repository,
resolved: &str,
known: &[String],
) -> Result<bool> {
if sorted_paths_match_pathspec(resolved, known, repo.pathspec_match_context()) {
return Ok(true);
}
if has_glob_chars(resolved) || resolved.starts_with(":(") {
return Ok(false);
}
if pathspec_is_exclude(resolved) {
return Ok(false);
}
if !resolved.starts_with(':') {
if let Some(work_tree) = repo.work_tree() {
let precompose = repo.pathspec_precompose_enabled();
let ignorecase = repo_pathspec_ignorecase(repo);
if pathspec_traverses_symlink_dir(work_tree, resolved, precompose, ignorecase) {
return Ok(false);
}
if precompose {
let staged = resolve_worktree_path_for_staging(work_tree, resolved, true);
if staged.abs.exists() {
return Ok(true);
}
} else if work_tree.join(resolved).exists() {
return Ok(true);
}
}
}
Ok(false)
}
fn repo_pathspec_ignorecase(repo: &Repository) -> bool {
repo.config()
.ok()
.and_then(|cfg| cfg.get_bool("core.ignorecase").and_then(|r| r.ok()))
.unwrap_or(false)
}
fn known_paths(repo: &Repository) -> Result<Vec<String>> {
let index = repo.load_index()?;
let mut set = HashSet::<String>::new();
for entry in &index.entries {
if entry.stage() == 0 {
set.insert(String::from_utf8_lossy(&entry.path).into_owned());
}
}
set.extend(sorted_head_tree_paths(repo)?);
let mut known: Vec<String> = set.into_iter().collect();
known.sort_unstable();
Ok(known)
}
pub(crate) fn sorted_head_tree_paths(repo: &Repository) -> Result<Vec<String>> {
let head = resolve_head(repo.git_dir())?;
let Some(head_oid) = head.oid() else {
return Ok(Vec::new());
};
let obj = repo.odb().read(head_oid)?;
let commit = parse_commit(&obj.data)?;
let mut paths = Vec::new();
collect_tree_paths(repo, commit.tree, "", &mut paths)?;
paths.sort_unstable();
Ok(paths)
}
fn collect_tree_paths(
repo: &Repository,
tree_oid: ObjectId,
prefix: &str,
out: &mut Vec<String>,
) -> Result<()> {
let obj = repo.odb().read(&tree_oid)?;
for entry in parse_tree(&obj.data)? {
let name = String::from_utf8_lossy(&entry.name);
let path = if prefix.is_empty() {
name.into_owned()
} else {
format!("{prefix}/{name}")
};
if entry.mode == MODE_TREE {
collect_tree_paths(repo, entry.oid, &path, out)?;
} else {
out.push(path);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::environment::RepositoryOptions;
use crate::repo::Repository;
use std::fs;
use tempfile::TempDir;
#[test]
fn unknown_literal_pathspec_does_not_match_empty_repo() {
let dir = TempDir::new().expect("tempdir");
crate::repo::init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.expect("init");
let repo =
Repository::discover(Some(dir.path()), &RepositoryOptions::empty()).expect("discover");
assert!(!pathspec_matches_known_in_repository(&repo, "does-not-exist").expect("check"));
}
#[test]
#[cfg(unix)]
fn literal_pathspec_through_symlink_dir_is_not_known() {
use std::os::unix::fs::symlink;
let dir = TempDir::new().expect("tempdir");
crate::repo::init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.expect("init");
std::fs::create_dir_all(dir.path().join("real")).expect("mkdir");
std::fs::write(dir.path().join("real/f"), b"x").expect("write");
symlink("real", dir.path().join("dl")).expect("symlink");
let repo =
Repository::discover(Some(dir.path()), &RepositoryOptions::empty()).expect("discover");
assert!(
!pathspec_matches_known_in_repository(&repo, "dl/f").expect("check"),
"must not follow symlink directory for pathspec matching"
);
}
#[test]
fn literal_pathspec_matches_worktree_file() {
let dir = TempDir::new().expect("tempdir");
crate::repo::init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Files,
)
.expect("init");
fs::write(dir.path().join("tracked.txt"), b"x").expect("write");
let repo =
Repository::discover(Some(dir.path()), &RepositoryOptions::empty()).expect("discover");
assert!(pathspec_matches_known_in_repository(&repo, "tracked.txt").expect("check"));
}
}