use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::attributes::{
parse_gitattributes_file_content, parse_gitattributes_file_content_with_base,
ParsedGitAttributes, MAX_ATTR_FILE_BYTES,
};
use crate::config::parse_path;
use crate::config::ConfigSet;
use crate::crlf::{self, AttrRule, ConversionConfig, FileAttrs};
use crate::environment::Environment;
use crate::error::{Error, Result};
use crate::filter_process::FilterProcessState;
use crate::ignore::IgnoreMatcher;
use crate::index::Index;
use crate::objects::ObjectId;
use crate::odb::Odb;
use crate::repo::Repository;
use crate::repo_caches::RepoCaches;
#[cfg(test)]
pub mod file_load_counters {
use crate::git_path::CanonicalizeExt;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
thread_local! {
static MAP: RefCell<HashMap<PathBuf, u32>> = RefCell::new(HashMap::new());
}
pub fn reset() {
MAP.with(|m| m.borrow_mut().clear());
}
pub fn record(path: &Path) {
let key = path
.canonicalize_simplified()
.unwrap_or_else(|_| path.to_path_buf());
MAP.with(|m| *m.borrow_mut().entry(key).or_insert(0) += 1);
}
#[must_use]
pub fn max_reads_per_file() -> u32 {
MAP.with(|m| m.borrow().values().copied().max().unwrap_or(0))
}
#[must_use]
pub fn reads_for(path: &Path) -> u32 {
let key = path
.canonicalize_simplified()
.unwrap_or_else(|_| path.to_path_buf());
MAP.with(|m| m.borrow().get(&key).copied().unwrap_or(0))
}
}
fn record_attr_read(path: &Path) {
#[cfg(test)]
file_load_counters::record(path);
let _ = path;
}
fn record_ignore_read(path: &Path) {
#[cfg(test)]
file_load_counters::record(path);
let _ = path;
}
pub struct WorktreeRules {
config: Arc<ConfigSet>,
conversion: ConversionConfig,
ignore: RefCell<IgnoreMatcher>,
attrs: AttributeState,
caches: Arc<RepoCaches>,
}
struct AttributeState {
work_tree: PathBuf,
_git_dir: PathBuf,
odb: Odb,
gitattributes_index: HashMap<Vec<u8>, ObjectId>,
gitattributes_index_only: bool,
stack_prefix: Arc<ParsedGitAttributes>,
info_rules: ParsedGitAttributes,
dir_fragment: RefCell<HashMap<String, Arc<ParsedGitAttributes>>>,
dir_stack: RefCell<Vec<DirFrame>>,
root_merged: RefCell<Option<Arc<ParsedGitAttributes>>>,
}
struct DirFrame {
dir: String,
stack: Arc<ParsedGitAttributes>,
merged: Option<Arc<ParsedGitAttributes>>,
}
impl WorktreeRules {
pub fn from_repository(repo: &Repository, index: &Index) -> Result<Self> {
let config = repo.config()?.clone();
Self::from_parts(repo, index, config)
}
pub fn from_parts(repo: &Repository, index: &Index, config: Arc<ConfigSet>) -> Result<Self> {
Self::from_parts_with_attribute_source(repo, index, config, false)
}
pub(crate) fn from_parts_index_attributes_only(
repo: &Repository,
index: &Index,
config: Arc<ConfigSet>,
) -> Result<Self> {
Self::from_parts_with_attribute_source(repo, index, config, true)
}
fn from_parts_with_attribute_source(
repo: &Repository,
index: &Index,
config: Arc<ConfigSet>,
gitattributes_index_only: bool,
) -> Result<Self> {
let work_tree = repo
.work_tree()
.ok_or(Error::RequiresWorkTree)?
.to_path_buf();
let conversion = ConversionConfig::from_config_with_runner(&config, repo.command_runner());
let ignore = RefCell::new(IgnoreMatcher::from_repository(repo)?);
let (stack_prefix, info_rules) = load_attribute_stack_prefix_and_info(
repo,
&work_tree,
&config,
index,
repo.odb(),
gitattributes_index_only,
)?;
let attrs = AttributeState {
work_tree,
_git_dir: repo.git_dir().to_path_buf(),
odb: repo.odb().clone(),
gitattributes_index: gitattributes_index_map(index),
gitattributes_index_only,
stack_prefix: Arc::new(stack_prefix),
info_rules,
dir_fragment: RefCell::new(HashMap::new()),
dir_stack: RefCell::new(Vec::new()),
root_merged: RefCell::new(None),
};
Ok(Self {
config,
conversion,
ignore,
attrs,
caches: Arc::clone(repo.caches()),
})
}
pub fn from_checkout_prospective_index(
repo: &Repository,
prospective_index: &Index,
config: Arc<ConfigSet>,
) -> Result<Self> {
Self::from_parts_index_attributes_only(repo, prospective_index, config)
}
#[must_use]
pub fn filter_process(&self) -> &FilterProcessState {
self.caches.filters()
}
#[must_use]
pub fn config(&self) -> &ConfigSet {
&self.config
}
#[must_use]
pub fn config_arc(&self) -> Arc<ConfigSet> {
Arc::clone(&self.config)
}
#[must_use]
pub fn conversion(&self) -> &ConversionConfig {
&self.conversion
}
pub fn ignore(&self) -> std::cell::Ref<'_, IgnoreMatcher> {
self.ignore.borrow()
}
pub fn ignore_mut(&self) -> std::cell::RefMut<'_, IgnoreMatcher> {
self.ignore.borrow_mut()
}
#[must_use]
pub fn attribute_rules_for_path(&self, rel_path: &str) -> Vec<AttrRule> {
let parent = parent_dir_key(rel_path);
self.attrs.merged_for_dir(&parent).rules.clone()
}
#[must_use]
pub fn file_attrs(&self, rel_path: &str, is_dir: bool) -> FileAttrs {
let parent = parent_dir_key(rel_path);
let parsed = self.attrs.merged_for_dir(&parent);
crlf::file_attrs_from_gitattributes(
&parsed.rules,
&parsed.macros,
rel_path,
is_dir,
&self.config,
)
}
}
impl AttributeState {
fn merged_for_dir(&self, dir: &str) -> Arc<ParsedGitAttributes> {
if dir.is_empty() {
let mut root = self.root_merged.borrow_mut();
return Arc::clone(root.get_or_insert_with(|| self.with_info(&self.stack_prefix)));
}
self.enter_dir(dir);
let mut frames = self.dir_stack.borrow_mut();
let Some(frame) = frames.last_mut() else {
return self.with_info(&self.stack_prefix);
};
Arc::clone(
frame
.merged
.get_or_insert_with(|| self.with_info(&frame.stack)),
)
}
fn with_info(&self, stack: &Arc<ParsedGitAttributes>) -> Arc<ParsedGitAttributes> {
let info = &self.info_rules;
if info.rules.is_empty() && info.macros.defs.is_empty() && info.warnings.is_empty() {
return Arc::clone(stack);
}
let mut merged = ParsedGitAttributes::clone(stack);
append_parsed(&mut merged, info.clone(), true);
Arc::new(merged)
}
fn enter_dir(&self, dir: &str) {
let ends: Vec<usize> = dir
.match_indices('/')
.map(|(at, _)| at)
.chain(std::iter::once(dir.len()))
.collect();
let mut frames = self.dir_stack.borrow_mut();
let shared = (1..=frames.len().min(ends.len()))
.rev()
.find(|&depth| frames[depth - 1].dir == dir[..ends[depth - 1]])
.unwrap_or(0);
frames.truncate(shared);
for &end in &ends[shared..] {
let prefix = &dir[..end];
let parent = frames
.last()
.map_or(&self.stack_prefix, |frame| &frame.stack);
let stack = match self.load_dir_fragment(prefix) {
Some(fragment) if !fragment.rules.is_empty() || !fragment.warnings.is_empty() => {
let mut stack = ParsedGitAttributes::clone(parent);
append_parsed(&mut stack, ParsedGitAttributes::clone(&fragment), false);
Arc::new(stack)
}
_ => Arc::clone(parent),
};
frames.push(DirFrame {
dir: prefix.to_owned(),
stack,
merged: None,
});
}
}
fn load_dir_fragment(&self, dir: &str) -> Option<Arc<ParsedGitAttributes>> {
if let Some(hit) = self.dir_fragment.borrow().get(dir) {
return Some(Arc::clone(hit));
}
let mut parsed = ParsedGitAttributes::default();
let ga_stored = if dir.is_empty() {
".gitattributes".to_owned()
} else {
format!("{dir}/.gitattributes")
};
let key = crate::repo_relpath::storage_bytes(&ga_stored).into_owned();
if !self.gitattributes_index_only {
let wt_ga = if dir.is_empty() {
self.work_tree.join(".gitattributes")
} else {
self.work_tree.join(dir).join(".gitattributes")
};
if let Some(content) = read_worktree_gitattributes(&wt_ga) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let mut p =
parse_gitattributes_file_content_with_base(&content, &ga_stored, dir);
parsed.rules.append(&mut p.rules);
parsed.warnings.append(&mut p.warnings);
}
return Some(self.remember_fragment(dir, parsed));
}
}
let oid = self.gitattributes_index.get(&key)?;
if let Ok(obj) = self.odb.read(oid) {
if let Ok(content) = String::from_utf8(obj.data) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let mut p =
parse_gitattributes_file_content_with_base(&content, &ga_stored, dir);
parsed.rules.append(&mut p.rules);
parsed.warnings.append(&mut p.warnings);
}
}
}
Some(self.remember_fragment(dir, parsed))
}
fn remember_fragment(
&self,
dir: &str,
parsed: ParsedGitAttributes,
) -> Arc<ParsedGitAttributes> {
let parsed = Arc::new(parsed);
self.dir_fragment
.borrow_mut()
.insert(dir.to_owned(), Arc::clone(&parsed));
parsed
}
}
fn append_parsed(
merged: &mut ParsedGitAttributes,
mut parsed: ParsedGitAttributes,
take_macros: bool,
) {
merged.rules.append(&mut parsed.rules);
if take_macros {
merged.macros.defs.extend(parsed.macros.defs.drain());
}
merged.warnings.append(&mut parsed.warnings);
}
fn parent_dir_key(rel_path: &str) -> String {
Path::new(rel_path)
.parent()
.map(|p| p.to_string_lossy().replace('\\', "/"))
.filter(|s| !s.is_empty())
.unwrap_or_default()
}
fn load_attribute_stack_prefix_and_info(
repo: &Repository,
work_tree: &Path,
config: &ConfigSet,
index: &Index,
odb: &Odb,
gitattributes_index_only: bool,
) -> Result<(ParsedGitAttributes, ParsedGitAttributes)> {
let mut stack_prefix = ParsedGitAttributes::default();
if let Some(g) = global_attributes_path(repo.environment(), config)? {
if let Some(content) = read_worktree_gitattributes(&g) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let p = parse_gitattributes_file_content(&content, g.to_string_lossy().as_ref());
append_parsed(&mut stack_prefix, p, true);
}
}
}
if gitattributes_index_only {
if let Some(entry) = index.get(b".gitattributes", 0) {
if let Ok(obj) = odb.read(&entry.oid) {
if let Ok(content) = String::from_utf8(obj.data) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let p = parse_gitattributes_file_content(&content, ".gitattributes");
append_parsed(&mut stack_prefix, p, true);
}
}
}
}
} else {
let root_ga = work_tree.join(".gitattributes");
if let Some(content) = read_worktree_gitattributes(&root_ga) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let p = parse_gitattributes_file_content(&content, ".gitattributes");
append_parsed(&mut stack_prefix, p, true);
}
} else if let Some(entry) = index.get(b".gitattributes", 0) {
if let Ok(obj) = odb.read(&entry.oid) {
if let Ok(content) = String::from_utf8(obj.data) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let p = parse_gitattributes_file_content(&content, ".gitattributes");
append_parsed(&mut stack_prefix, p, true);
}
}
}
}
}
let mut info_rules = ParsedGitAttributes::default();
let info = repo.git_dir().join("info/attributes");
if let Some(content) = read_worktree_gitattributes(&info) {
if content.len() <= MAX_ATTR_FILE_BYTES {
let p = parse_gitattributes_file_content(&content, "info/attributes");
append_parsed(&mut info_rules, p, true);
}
}
Ok((stack_prefix, info_rules))
}
fn global_attributes_path(env: &Environment, config: &ConfigSet) -> Result<Option<PathBuf>> {
if let Some(path) = config.get("core.attributesfile") {
return Ok(Some(PathBuf::from(parse_path(env, &path))));
}
Ok(env.default_xdg_config_git_file("attributes"))
}
fn read_worktree_gitattributes(path: &Path) -> Option<String> {
let meta = std::fs::symlink_metadata(path).ok()?;
if meta.file_type().is_symlink() {
return None;
}
record_attr_read(path);
std::fs::read_to_string(path).ok()
}
fn gitattributes_index_map(index: &Index) -> HashMap<Vec<u8>, ObjectId> {
index
.entries()
.iter()
.filter(|e| e.stage() == 0 && is_gitattributes_index_path(&e.path))
.map(|e| (e.path.clone(), e.oid))
.collect()
}
fn is_gitattributes_index_path(path: &[u8]) -> bool {
path == b".gitattributes" || path.ends_with(b"/.gitattributes")
}
pub(crate) fn record_ignore_pattern_file_read(path: &Path) {
record_ignore_read(path);
}