mod remote;
pub use remote::GitRemoteRepo;
use crate::env::DeltaEnv;
use regex::Regex;
use std::cell::OnceCell;
use std::collections::HashMap;
use std::path::Path;
use lazy_static::lazy_static;
pub struct GitConfig {
config: git2::Config,
config_from_env_var: HashMap<String, String>,
pub enabled: bool,
repo: Option<git2::Repository>,
remote_url: OnceCell<Option<GitRemoteRepo>>,
#[cfg(test)]
path: std::path::PathBuf,
}
#[cfg(test)]
impl Clone for GitConfig {
fn clone(&self) -> Self {
assert!(self.repo.is_none());
GitConfig {
config: git2::Config::open(&self.path).unwrap(),
config_from_env_var: self.config_from_env_var.clone(),
enabled: self.enabled,
repo: None,
remote_url: OnceCell::new(),
path: self.path.clone(),
}
}
}
impl GitConfig {
#[cfg(not(test))]
pub fn try_create(env: &DeltaEnv) -> Option<Self> {
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,
remote_url: OnceCell::new(),
})
}
None => None,
}
}
#[cfg(test)]
pub fn try_create(_env: &DeltaEnv) -> Option<Self> {
None
}
#[cfg(test)]
pub fn for_testing() -> Option<Self> {
Some(GitConfig {
config: git2::Config::new().unwrap(),
config_from_env_var: HashMap::new(),
enabled: true,
repo: None,
remote_url: OnceCell::new(),
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,
remote_url: OnceCell::new(),
#[cfg(test)]
path: path.into(),
}
}
Err(e) => {
fatal(format!("Failed to read git config: {}", e.message()));
}
}
}
pub fn get<T>(&self, key: &str) -> Option<T>
where
T: GitConfigGet,
{
if self.enabled {
T::git_config_get(key, self)
} else {
None
}
}
#[cfg(test)]
fn get_remote_url_impl(&self) -> Option<GitRemoteRepo> {
GitRemoteRepo::for_testing()
}
#[cfg(not(test))]
fn get_remote_url_impl(&self) -> Option<GitRemoteRepo> {
use std::str::FromStr;
self.repo
.as_ref()?
.find_remote("origin")
.ok()?
.url()
.and_then(|url| GitRemoteRepo::from_str(url).ok())
}
pub fn get_remote_url(&self) -> &Option<GitRemoteRepo> {
self.remote_url.get_or_init(|| self.get_remote_url_impl())
}
pub fn for_each<F>(&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<String, String> {
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<String, String> {
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<Self>
where
Self: Sized;
}
impl GitConfigGet for String {
fn git_config_get(key: &str, git_config: &GitConfig) -> Option<Self> {
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<String> {
fn git_config_get(key: &str, git_config: &GitConfig) -> Option<Self> {
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<Self> {
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<Self> {
if let Some(s) = git_config.config_from_env_var.get(key) {
if let Ok(n) = s.parse::<usize>() {
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<Self> {
if let Some(s) = git_config.config_from_env_var.get(key) {
if let Ok(n) = s.parse::<f64>() {
return Some(n);
}
}
match git_config.config.get_string(key) {
Ok(value) => value.parse::<f64>().ok(),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::parse_config_from_env_var_value;
#[test]
fn test_parse_config_from_env_var_value() {
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""##
);
}
}
}