use std::cell::RefCell;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
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: Vec<AttrRule>,
info_rules: Vec<AttrRule>,
dir_fragment: RefCell<HashMap<String, Vec<AttrRule>>>,
stack_without_info_by_dir: RefCell<HashMap<String, Vec<AttrRule>>>,
}
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_else(|| Error::Message("this operation must be run in a work tree".into()))?
.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)
}
#[must_use]
pub fn file_attrs(&self, rel_path: &str, is_dir: bool) -> FileAttrs {
let rules = self.attribute_rules_for_path(rel_path);
crlf::get_file_attrs(&rules, rel_path, is_dir, &self.config)
}
}
impl AttributeState {
fn merged_for_dir(&self, dir: &str) -> Vec<AttrRule> {
let mut merged = self.stack_without_info_for_dir(dir);
merged.extend_from_slice(&self.info_rules);
merged
}
fn stack_without_info_for_dir(&self, dir: &str) -> Vec<AttrRule> {
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 rules = self.stack_without_info_for_dir(&parent);
rules.extend(self.load_dir_fragment(dir));
rules
};
self.stack_without_info_by_dir
.borrow_mut()
.insert(dir.to_owned(), stack.clone());
stack
}
fn load_dir_fragment(&self, dir: &str) -> Vec<AttrRule> {
if let Some(hit) = self.dir_fragment.borrow().get(dir).cloned() {
return hit;
}
let mut rules = Vec::new();
let ga_rel = if dir.is_empty() {
PathBuf::from(".gitattributes")
} else {
Path::new(dir).join(".gitattributes")
};
let key = path_to_index_bytes(&ga_rel);
if !self.gitattributes_index_only {
let wt_ga = self.work_tree.join(&ga_rel);
if let Some(content) = read_worktree_gitattributes(&wt_ga) {
rules.extend(crlf::parse_gitattributes_content(&content));
self.dir_fragment
.borrow_mut()
.insert(dir.to_owned(), rules.clone());
return rules;
}
}
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) {
rules.extend(crlf::parse_gitattributes_content(&content));
}
}
}
self.dir_fragment
.borrow_mut()
.insert(dir.to_owned(), rules.clone());
rules
}
}
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<(Vec<AttrRule>, Vec<AttrRule>)> {
let mut stack_prefix = Vec::new();
if let Some(g) = global_attributes_path(repo.environment(), config)? {
if let Some(content) = read_worktree_gitattributes(&g) {
stack_prefix.extend(crlf::parse_gitattributes_content(&content));
}
}
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) {
stack_prefix.extend(crlf::parse_gitattributes_content(&content));
}
}
}
} else {
let root_ga = work_tree.join(".gitattributes");
if let Some(content) = read_worktree_gitattributes(&root_ga) {
stack_prefix.extend(crlf::parse_gitattributes_content(&content));
} 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) {
stack_prefix.extend(crlf::parse_gitattributes_content(&content));
}
}
}
}
let mut info_rules = Vec::new();
let info = repo.git_dir().join("info/attributes");
if let Some(content) = read_worktree_gitattributes(&info) {
info_rules.extend(crlf::parse_gitattributes_content(&content));
}
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")
}
fn path_to_index_bytes(path: &Path) -> Vec<u8> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
{
path.to_string_lossy().into_owned().into_bytes()
}
}
pub(crate) fn record_ignore_pattern_file_read(path: &Path) {
record_ignore_read(path);
}