mod remote; pub use remote::GitRemoteRepo; use crate::env::DeltaEnv; use regex::Regex; use std::collections::HashMap; use std::path::Path; use lazy_static::lazy_static; pub struct GitConfig { config: git2::Config, config_from_env_var: HashMap, pub enabled: bool, repo: Option, // To make GitConfig cloneable when testing (in turn to make Config cloneable): #[cfg(test)] path: std::path::PathBuf, } #[cfg(test)] impl Clone for GitConfig { fn clone(&self) -> Self { assert!(self.repo.is_none()); GitConfig { // Assumes no test modifies the file pointed to by `path` config: git2::Config::open(&self.path).unwrap(), config_from_env_var: self.config_from_env_var.clone(), enabled: self.enabled, repo: None, path: self.path.clone(), } } } impl GitConfig { #[cfg(not(test))] pub fn try_create(env: &DeltaEnv) -> Option { use crate::fatal; let repo = match &env.current_dir { Some(dir) => git2::Repository::discover(dir).ok(), _ => None, }; let config = match &repo { Some(repo) => repo.config().ok(), None => git2::Config::open_default().ok(), }; match config { Some(mut config) => { let config = config.snapshot().unwrap_or_else(|err| { fatal(format!("Failed to read git config: {err}")); }); Some(Self { config, config_from_env_var: parse_config_from_env_var(env), repo, enabled: true, }) } None => None, } } #[cfg(test)] pub fn try_create(_env: &DeltaEnv) -> Option { // Do not read local git configs when testing None } #[cfg(test)] pub fn for_testing() -> Option { Some(GitConfig { config: git2::Config::new().unwrap(), config_from_env_var: HashMap::new(), enabled: true, repo: None, path: std::path::PathBuf::from("/invalid_null.git"), }) } pub fn from_path(env: &DeltaEnv, path: &Path, honor_env_var: bool) -> Self { use crate::fatal; match git2::Config::open(path) { Ok(mut config) => { let config = config.snapshot().unwrap_or_else(|err| { fatal(format!("Failed to read git config: {err}")); }); Self { config, config_from_env_var: if honor_env_var { parse_config_from_env_var(env) } else { HashMap::new() }, repo: None, enabled: true, #[cfg(test)] path: path.into(), } } Err(e) => { fatal(format!("Failed to read git config: {}", e.message())); } } } pub fn get(&self, key: &str) -> Option where T: GitConfigGet, { if self.enabled { T::git_config_get(key, self) } else { None } } #[cfg(not(test))] pub fn get_remote_url(&self) -> Option { use std::str::FromStr; self.repo .as_ref()? .find_remote("origin") .ok()? .url() .and_then(|url| GitRemoteRepo::from_str(url).ok()) } pub fn for_each(&self, regex: &str, mut f: F) where F: FnMut(&str, Option<&str>), { let mut entries = self.config.entries(Some(regex)).unwrap(); while let Some(entry) = entries.next() { let entry = entry.unwrap(); let name = entry.name().unwrap(); f(name, entry.value()); } } } fn parse_config_from_env_var(env: &DeltaEnv) -> HashMap { if let Some(s) = &env.git_config_parameters { parse_config_from_env_var_value(s) } else { HashMap::new() } } lazy_static! { static ref GIT_CONFIG_PARAMETERS_REGEX: Regex = Regex::new( r"(?x) (?: # Non-capturing group containing union '(delta\.[a-z-]+)=([^']+)' # Git <2.31.0 format | '(delta\.[a-z-]+)'='([^']+)' # Git ≥2.31.0 format ) " ) .unwrap(); } fn parse_config_from_env_var_value(s: &str) -> HashMap { GIT_CONFIG_PARAMETERS_REGEX .captures_iter(s) .map(|captures| { let (i, j) = match ( captures.get(1), captures.get(2), captures.get(3), captures.get(4), ) { (Some(_), Some(_), None, None) => (1, 2), (None, None, Some(_), Some(_)) => (3, 4), _ => (0, 0), }; if (i, j) == (0, 0) { ("".to_string(), "".to_string()) } else { (captures[i].to_string(), captures[j].to_string()) } }) .collect() } pub trait GitConfigGet { fn git_config_get(key: &str, git_config: &GitConfig) -> Option where Self: Sized; } impl GitConfigGet for String { fn git_config_get(key: &str, git_config: &GitConfig) -> Option { match git_config.config_from_env_var.get(key) { Some(val) => Some(val.to_string()), None => git_config.config.get_string(key).ok(), } } } impl GitConfigGet for Option { fn git_config_get(key: &str, git_config: &GitConfig) -> Option { match git_config.config_from_env_var.get(key) { Some(val) => Some(Some(val.to_string())), None => match git_config.config.get_string(key) { Ok(val) => Some(Some(val)), _ => None, }, } } } impl GitConfigGet for bool { fn git_config_get(key: &str, git_config: &GitConfig) -> Option { match git_config.config_from_env_var.get(key).map(|s| s.as_str()) { Some("true") => Some(true), Some("false") => Some(false), _ => git_config.config.get_bool(key).ok(), } } } impl GitConfigGet for usize { fn git_config_get(key: &str, git_config: &GitConfig) -> Option { if let Some(s) = git_config.config_from_env_var.get(key) { if let Ok(n) = s.parse::() { return Some(n); } } match git_config.config.get_i64(key) { Ok(value) => Some(value as usize), _ => None, } } } impl GitConfigGet for f64 { fn git_config_get(key: &str, git_config: &GitConfig) -> Option { if let Some(s) = git_config.config_from_env_var.get(key) { if let Ok(n) = s.parse::() { return Some(n); } } match git_config.config.get_string(key) { Ok(value) => value.parse::().ok(), _ => None, } } } #[cfg(test)] mod tests { use super::parse_config_from_env_var_value; #[test] fn test_parse_config_from_env_var_value() { // To generate test cases, use git -c ... with // [core] // pager = env | grep GIT_CONFIG_PARAMETERS // We test multiple formats because the format of the value stored by // git in this environment variable has changed in recent versions of // Git. See // https://github.com/git/git/blob/311531c9de557d25ac087c1637818bd2aad6eb3a/Documentation/RelNotes/2.31.0.txt#L127-L130 for env_var_value in &["'user.name=xxx'", "'user.name'='xxx'"] { let config = parse_config_from_env_var_value(env_var_value); assert!(config.is_empty()); } for env_var_value in &["'delta.plus-style=green'", "'delta.plus-style'='green'"] { let config = parse_config_from_env_var_value(env_var_value); assert_eq!(config["delta.plus-style"], "green"); } for env_var_value in &[ r##"'user.name=xxx' 'delta.hunk-header-line-number-style=red "#067a00"'"##, r##"'user.name'='xxx' 'delta.hunk-header-line-number-style'='red "#067a00"'"##, ] { let config = parse_config_from_env_var_value(env_var_value); assert_eq!( config["delta.hunk-header-line-number-style"], r##"red "#067a00""## ); } for env_var_value in &[ r##"'user.name=xxx' 'delta.side-by-side=false'"##, r##"'user.name'='xxx' 'delta.side-by-side'='false'"##, ] { let config = parse_config_from_env_var_value(env_var_value); assert_eq!(config["delta.side-by-side"], "false"); } for env_var_value in &[ r##"'delta.plus-style=green' 'delta.side-by-side=false' 'delta.hunk-header-line-number-style=red "#067a00"'"##, r##"'delta.plus-style'='green' 'delta.side-by-side'='false' 'delta.hunk-header-line-number-style'='red "#067a00"'"##, ] { let config = parse_config_from_env_var_value(env_var_value); assert_eq!(config["delta.plus-style"], "green"); assert_eq!(config["delta.side-by-side"], "false"); assert_eq!( config["delta.hunk-header-line-number-style"], r##"red "#067a00""## ); } } }