use std::collections::{BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use super::rules::validate_path;
use super::{Attributes, Case, Error, Limits, Source, State};
use crate::source_files::{Ancestors, FileError, native, os_path, read_optional};
use crate::workflow::Environment;
use crate::{EntryMode, ObjectId, Objects, ReadLimits, Repository};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DirectorySource {
Worktree,
Tree(ObjectId),
None,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LoadError {
#[error("unsupported attribute configuration: {0}")]
Unsupported(&'static str),
#[error("reading attribute configuration")]
Config(#[from] crate::config::InvalidValue),
#[error("resolving GIT_ATTR_SOURCE")]
Source(#[source] Box<crate::Error>),
#[error("reading attribute file {path:?}")]
Io {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("reading attribute file {path:?} from the attribute tree")]
Object {
path: Vec<u8>,
#[source]
source: Box<crate::Error>,
},
#[error("applying attributes for {path:?}")]
Rules {
path: Vec<u8>,
#[source]
source: Error,
},
}
impl From<FileError> for LoadError {
fn from(error: FileError) -> Self {
match error {
FileError::Io { path, source } => Self::Io { path, source },
FileError::TooLarge { path } => Self::Rules {
path: path.to_string_lossy().into_owned().into_bytes(),
source: Error::Limit("source bytes"),
},
FileError::NotFile => Self::Unsupported("attribute file is not a regular file"),
}
}
}
#[derive(Debug)]
pub struct Lookup {
#[cfg(any(target_os = "linux", target_os = "macos"))]
environment: Environment,
attributes: Attributes,
directories: Directories,
loaded: BTreeSet<Vec<u8>>,
}
#[derive(Debug)]
enum Directories {
Worktree {
root: PathBuf,
walk: Option<Ancestors>,
},
Tree {
objects: Box<Objects>,
limits: ReadLimits,
trees: HashMap<Vec<u8>, Option<ObjectId>>,
},
None,
}
impl Lookup {
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn environment(&self) -> &Environment {
&self.environment
}
#[must_use]
pub fn attributes(&self) -> &Attributes {
&self.attributes
}
pub fn get(&mut self, path: &[u8], name: &[u8]) -> Result<State<'_>, LoadError> {
let rules = |source| LoadError::Rules {
path: path.to_vec(),
source,
};
validate_path(path, false, self.attributes.limits().path_bytes).map_err(rules)?;
self.load_ancestors(path)?;
self.attributes.get(path, name).map_err(rules)
}
pub(crate) fn missing_blobs<'p>(
&mut self,
paths: impl IntoIterator<Item = &'p [u8]>,
) -> Result<Vec<ObjectId>, LoadError> {
let Directories::Tree {
objects,
limits,
trees,
} = &mut self.directories
else {
return Ok(Vec::new());
};
let mut missing = Vec::new();
if !objects.partial_clone {
return Ok(missing);
}
let mut visited = BTreeSet::new();
for path in paths {
let ends = std::iter::once(0).chain(
path.iter()
.enumerate()
.filter_map(|(end, byte)| (*byte == b'/').then_some(end)),
);
for end in ends {
let directory = path.get(..end).unwrap_or_default();
if self.loaded.contains(directory) || !visited.insert(directory.to_vec()) {
continue;
}
let Some(tree) = tree_at(objects, *limits, trees, directory)? else {
break;
};
let object = |source: crate::Error| LoadError::Object {
path: directory.to_vec(),
source: Box::new(source),
};
let entries = objects
.read_tree_with(tree, *limits)
.map_err(|e| object(e.into()))?;
let blob = entries.entries().iter().find(|entry| {
entry.name == b".gitattributes"
&& matches!(entry.mode, EntryMode::Blob | EntryMode::Executable)
});
if let Some(entry) = blob
&& !objects.contains(entry.id).map_err(|e| object(e.into()))?
{
missing.push(entry.id);
}
}
}
Ok(missing)
}
fn load_ancestors(&mut self, path: &[u8]) -> Result<(), LoadError> {
let ends = std::iter::once(0).chain(
path.iter()
.enumerate()
.filter_map(|(end, byte)| (*byte == b'/').then_some(end)),
);
let directories: Vec<&[u8]> = ends
.map(|end| path.get(..end).unwrap_or_default())
.collect();
let Some(first) = directories
.iter()
.position(|directory| !self.loaded.contains(*directory))
else {
return Ok(());
};
match &mut self.directories {
Directories::None => Ok(()),
Directories::Worktree { root, walk } => {
let walk = match walk {
Some(walk) => walk,
None => walk.insert(Ancestors::open(root)?),
};
for (index, directory) in directories.iter().copied().enumerate().skip(first) {
if !walk.enter(root, directory)? {
for blocked in directories.get(index..).unwrap_or_default() {
self.loaded.insert(blocked.to_vec());
}
break;
}
if self.loaded.contains(directory) {
continue;
}
let file = native(root, directory)
.unwrap_or_else(|| root.clone())
.join(".gitattributes");
let remaining = self.attributes.remaining_bytes();
if let Some(content) = walk.read(".gitattributes", &file, remaining)? {
self.attributes
.add(Source::Directory(directory), &content)
.map_err(|source| LoadError::Rules {
path: file.to_string_lossy().into_owned().into_bytes(),
source,
})?;
}
self.loaded.insert(directory.to_vec());
}
Ok(())
}
Directories::Tree {
objects,
limits,
trees,
} => {
for (index, directory) in directories.iter().copied().enumerate().skip(first) {
if self.loaded.contains(directory) {
continue;
}
let Some(tree) = tree_at(objects, *limits, trees, directory)? else {
for absent in directories.get(index..).unwrap_or_default() {
self.loaded.insert(absent.to_vec());
}
break;
};
let file = if directory.is_empty() {
b".gitattributes".to_vec()
} else {
[directory, b"/.gitattributes"].concat()
};
let object = |source: crate::Error| LoadError::Object {
path: file.clone(),
source: Box::new(source),
};
let entries = objects
.read_tree_with(tree, *limits)
.map_err(|e| object(e.into()))?;
let blob = entries.entries().iter().find(|entry| {
entry.name == b".gitattributes"
&& matches!(entry.mode, EntryMode::Blob | EntryMode::Executable)
});
if let Some(entry) = blob {
let remaining = self.attributes.remaining_bytes();
let limits = limits.with_max_object_bytes(
limits.max_object_bytes.min(remaining.saturating_add(1)),
);
let content = objects
.read_blob_with(entry.id, limits)
.map_err(|e| object(e.into()))?;
self.attributes
.add(Source::Directory(directory), &content)
.map_err(|source| LoadError::Rules {
path: file.clone(),
source,
})?;
}
self.loaded.insert(directory.to_vec());
}
Ok(())
}
}
}
}
fn tree_at(
objects: &Objects,
limits: ReadLimits,
trees: &mut HashMap<Vec<u8>, Option<ObjectId>>,
directory: &[u8],
) -> Result<Option<ObjectId>, LoadError> {
if let Some(id) = trees.get(directory) {
return Ok(*id);
}
let Some((parent, name)) = crate::bytes::rsplit_once(directory, b'/')
.or_else(|| (!directory.is_empty()).then_some((&b""[..], directory)))
else {
return Ok(None);
};
let found = match tree_at(objects, limits, trees, parent)? {
None => None,
Some(parent_tree) => {
let tree =
objects
.read_tree_with(parent_tree, limits)
.map_err(|e| LoadError::Object {
path: parent.to_vec(),
source: Box::new(e.into()),
})?;
tree.entries()
.iter()
.find(|entry| entry.name == name && entry.mode == EntryMode::Tree)
.map(|entry| entry.id)
}
};
trees.insert(directory.to_vec(), found);
Ok(found)
}
impl Repository {
pub fn attribute_source(
&self,
environment: &Environment,
) -> Result<DirectorySource, LoadError> {
let named = environment
.get("GIT_ATTR_SOURCE")
.map(|value| (value.to_string_lossy().into_owned(), true))
.or_else(|| {
self.config()
.string("attr", None, "tree")
.map(|value| (String::from_utf8_lossy(value).into_owned(), false))
});
if let Some((revision, required)) = named {
match self.resolve_tree(&revision) {
Ok(tree) => return Ok(DirectorySource::Tree(tree)),
Err(error) if required => return Err(LoadError::Source(Box::new(error))),
Err(_) => {}
}
}
Ok(match self.worktree() {
Some(_) => DirectorySource::Worktree,
None => DirectorySource::None,
})
}
fn resolve_tree(&self, revision: &str) -> Result<ObjectId, crate::Error> {
let objects = self.objects()?;
let resolver = crate::revision::Resolver::new(self, &objects)?;
let revision = revision.parse()?;
Ok(resolver.resolve_to(&revision, crate::ObjectKind::Tree)?)
}
pub fn attributes(
&self,
environment: &Environment,
directories: DirectorySource,
) -> Result<Lookup, LoadError> {
self.attributes_with(
environment,
directories,
Limits::default(),
ReadLimits::default(),
)
}
pub fn attributes_with(
&self,
environment: &Environment,
directories: DirectorySource,
limits: Limits,
read: ReadLimits,
) -> Result<Lookup, LoadError> {
let case = match self.config().boolean("core", None, "ignorecase")? {
Some(true) => Case::AsciiInsensitive,
_ => Case::Sensitive,
};
let directories = match directories {
DirectorySource::Worktree => Directories::Worktree {
root: self
.worktree()
.ok_or(LoadError::Unsupported("repository has no working tree"))?
.to_owned(),
walk: None,
},
DirectorySource::Tree(tree) => Directories::Tree {
objects: Box::new(self.objects().map_err(|e| LoadError::Object {
path: Vec::new(),
source: Box::new(e.into()),
})?),
limits: read,
trees: HashMap::from([(Vec::new(), Some(tree))]),
},
DirectorySource::None => Directories::None,
};
let mut lookup = Lookup {
#[cfg(any(target_os = "linux", target_os = "macos"))]
environment: environment.clone(),
attributes: Attributes::new(case, limits),
directories,
loaded: BTreeSet::new(),
};
let mut files = Vec::new();
if environment.get("GIT_ATTR_NOSYSTEM").is_none()
&& let Some(path) = crate::config::system_file("gitattributes")
{
files.push((Source::System, path));
}
if let Some(path) = global_attributes(self, environment)? {
files.push((Source::Global, path));
}
files.push((
Source::Info,
self.common_dir().join("info").join("attributes"),
));
for (source, path) in files {
let remaining = lookup.attributes.remaining_bytes();
if let Some(content) = read_optional(&path, remaining)? {
lookup
.attributes
.add(source, &content)
.map_err(|source| LoadError::Rules {
path: path.to_string_lossy().into_owned().into_bytes(),
source,
})?;
}
}
Ok(lookup)
}
}
fn global_attributes(
repository: &Repository,
environment: &Environment,
) -> Result<Option<PathBuf>, LoadError> {
let Some(value) = repository.config().string("core", None, "attributesfile") else {
let xdg = environment
.get("XDG_CONFIG_HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(|| {
environment
.get("HOME")
.map(|home| Path::new(home).join(".config"))
});
return Ok(xdg.map(|directory| directory.join("git").join("attributes")));
};
if value.is_empty() {
return Ok(None);
}
let unicode = LoadError::Unsupported("core.attributesFile is not Unicode");
if let Some(rest) = value.strip_prefix(b"~/") {
let home = environment.get("HOME").ok_or(LoadError::Unsupported(
"core.attributesFile uses ~/ but HOME is unset",
))?;
return Ok(Some(Path::new(home).join(os_path(rest).ok_or(unicode)?)));
}
if value.starts_with(b"~") {
return Err(LoadError::Unsupported(
"core.attributesFile uses ~user/ expansion",
));
}
let path = os_path(value).ok_or(unicode)?;
Ok(Some(match repository.worktree() {
Some(root) => root.join(path),
None => path,
}))
}