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 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().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().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: ParsedGitAttributes,
info_rules: ParsedGitAttributes,
dir_fragment: RefCell<HashMap<String, ParsedGitAttributes>>,
stack_without_info_by_dir: RefCell<HashMap<String, 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,
info_rules,
dir_fragment: RefCell::new(HashMap::new()),
stack_without_info_by_dir: RefCell::new(HashMap::new()),
};
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
}
#[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) -> ParsedGitAttributes {
let mut merged = self.stack_without_info_for_dir(dir);
append_parsed(&mut merged, self.info_rules.clone(), true);
merged
}
fn stack_without_info_for_dir(&self, dir: &str) -> ParsedGitAttributes {
if let Some(hit) = self.stack_without_info_by_dir.borrow().get(dir).cloned() {
return hit;
}
let stack = if dir.is_empty() {
self.stack_prefix.clone()
} else {
let parent = parent_of_dir(dir);
let mut parsed = self.stack_without_info_for_dir(&parent);
append_parsed(&mut parsed, self.load_dir_fragment(dir), false);
parsed
};
self.stack_without_info_by_dir
.borrow_mut()
.insert(dir.to_owned(), stack.clone());
stack
}
fn load_dir_fragment(&self, dir: &str) -> ParsedGitAttributes {
if let Some(hit) = self.dir_fragment.borrow().get(dir).cloned() {
return 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);
}
self.dir_fragment
.borrow_mut()
.insert(dir.to_owned(), parsed.clone());
return parsed;
}
}
if let Some(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);
}
}
}
}
self.dir_fragment
.borrow_mut()
.insert(dir.to_owned(), parsed.clone());
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 parent_of_dir(dir: &str) -> String {
Path::new(dir)
.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);
}