use crate::utils::file_utils::read_file_with_context_sync;
use anstyle::Style;
use anyhow::Result;
use once_cell::sync::Lazy;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct GitColorConfig {
pub diff_new: Style,
pub diff_old: Style,
pub diff_context: Style,
pub diff_header: Style,
pub diff_meta: Style,
pub diff_frag: Style,
pub status_added: Style,
pub status_modified: Style,
pub status_deleted: Style,
pub status_untracked: Style,
pub branch_current: Style,
pub branch_local: Style,
pub branch_remote: Style,
}
impl Default for GitColorConfig {
fn default() -> Self {
Self::with_defaults()
}
}
impl GitColorConfig {
pub fn with_defaults() -> Self {
Self {
diff_new: crate::utils::style_helpers::style_from_color_name("green"),
diff_old: crate::utils::style_helpers::style_from_color_name("red"),
diff_context: Style::new(),
diff_header: Style::new(),
diff_meta: Style::new(),
diff_frag: crate::utils::style_helpers::style_from_color_name("cyan"),
status_added: crate::utils::style_helpers::style_from_color_name("green:dimmed"),
status_modified: crate::utils::style_helpers::style_from_color_name("red:dimmed"),
status_deleted: crate::utils::style_helpers::style_from_color_name("red:dimmed"),
status_untracked: Style::new(),
branch_current: Style::new(),
branch_local: Style::new(),
branch_remote: Style::new(),
}
}
pub fn from_git_config(config_path: &Path) -> Result<Self> {
let content = read_file_with_context_sync(config_path, "Git config")?;
let mut config = Self::with_defaults();
if let Some(diff_new) = Self::extract_git_color(&content, "diff", "new") {
config.diff_new = diff_new;
}
if let Some(diff_old) = Self::extract_git_color(&content, "diff", "old") {
config.diff_old = diff_old;
}
if let Some(diff_context) = Self::extract_git_color(&content, "diff", "context") {
config.diff_context = diff_context;
}
if let Some(diff_header) = Self::extract_git_color(&content, "diff", "header") {
config.diff_header = diff_header;
}
if let Some(diff_meta) = Self::extract_git_color(&content, "diff", "meta") {
config.diff_meta = diff_meta;
}
if let Some(diff_frag) = Self::extract_git_color(&content, "diff", "frag") {
config.diff_frag = diff_frag;
}
if let Some(status_added) = Self::extract_git_color(&content, "status", "added") {
config.status_added = status_added;
}
if let Some(status_modified) = Self::extract_git_color(&content, "status", "modified") {
config.status_modified = status_modified;
}
if let Some(status_deleted) = Self::extract_git_color(&content, "status", "deleted") {
config.status_deleted = status_deleted;
}
if let Some(status_untracked) = Self::extract_git_color(&content, "status", "untracked") {
config.status_untracked = status_untracked;
}
if let Some(branch_current) = Self::extract_git_color(&content, "branch", "current") {
config.branch_current = branch_current;
}
if let Some(branch_local) = Self::extract_git_color(&content, "branch", "local") {
config.branch_local = branch_local;
}
if let Some(branch_remote) = Self::extract_git_color(&content, "branch", "remote") {
config.branch_remote = branch_remote;
}
Ok(config)
}
fn extract_git_color(content: &str, section: &str, key: &str) -> Option<Style> {
let section_pattern = format!(r#"\[color "{}"\]"#, regex::escape(section));
let section_re = regex::Regex::new(§ion_pattern).ok()?;
let section_start = section_re.find(content)?.end();
static OPEN_BRACKET_RE: Lazy<regex::Regex> = Lazy::new(|| match regex::Regex::new(r"\[") {
Ok(regex) => regex,
Err(error) => panic!("open bracket regex must compile: {error}"),
});
let section_end =
if let Some(next_section) = OPEN_BRACKET_RE.find(&content[section_start..]) {
section_start + next_section.start()
} else {
content.len()
};
let section_content = &content[section_start..section_end];
let key_pattern = format!(r"{}\s*=\s*(.+?)(?:\r?\n|$)", regex::escape(key));
let key_re = regex::Regex::new(&key_pattern).ok()?;
let value = key_re.captures(section_content)?.get(1)?.as_str().trim();
let color_name = value.split_whitespace().find(|word| {
matches!(
word.to_lowercase().as_str(),
"normal"
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| "magenta"
| "purple"
| "cyan"
| "white"
)
})?;
Some(crate::utils::style_helpers::style_from_color_name(
color_name,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_git_color_config_defaults() {
let config = GitColorConfig::default();
assert_ne!(config.diff_new, Style::new());
assert_ne!(config.diff_old, Style::new());
}
fn create_test_git_config(content: &str) -> Result<GitColorConfig> {
let mut temp = tempfile::NamedTempFile::new()?;
temp.write_all(content.as_bytes())?;
temp.flush()?;
GitColorConfig::from_git_config(temp.path())
}
#[test]
fn test_parse_git_config_diff_section() {
let config_text = r#"
[core]
bare = false
[color "diff"]
new = green
old = red
context = cyan
"#;
let config = create_test_git_config(config_text).expect("Failed to parse test config");
assert_ne!(config.diff_new, Style::new());
assert_ne!(config.diff_old, Style::new());
}
#[test]
fn test_parse_git_config_status_section() {
let config_text = r#"
[color "status"]
added = green bold
modified = cyan
deleted = red bold
untracked = cyan
"#;
let config = create_test_git_config(config_text).expect("Failed to parse test config");
assert_ne!(config.status_added, Style::new());
assert_ne!(config.status_modified, Style::new());
}
#[test]
fn test_parse_git_config_hex_colors() {
let config_text = r#"
[color "diff"]
new = #00ff00
old = #ff0000
"#;
let config = create_test_git_config(config_text).expect("Failed to parse test config");
assert_ne!(config.diff_new, Style::new());
assert_ne!(config.diff_old, Style::new());
}
#[test]
fn test_parse_git_config_missing_file() {
let result = GitColorConfig::from_git_config(Path::new("/nonexistent/.git/config"));
assert!(result.is_err());
}
#[test]
fn test_parse_git_config_empty_file() {
let config_text = "";
let config = create_test_git_config(config_text).expect("Failed to parse empty config");
assert_ne!(config.diff_new, Style::new());
assert_ne!(config.diff_old, Style::new());
}
#[test]
fn test_parse_git_config_branch_section() {
let config_text = r#"
[color "branch"]
current = cyan bold
local = cyan
remote = cyan
"#;
let config = create_test_git_config(config_text).expect("Failed to parse test config");
assert_ne!(config.branch_current, Style::new());
}
#[test]
fn test_parse_git_config_all_sections() {
let config_text = r#"
[color "diff"]
new = green
old = red
[color "status"]
added = green
modified = cyan
[color "branch"]
current = cyan bold
"#;
let config = create_test_git_config(config_text).expect("Failed to parse test config");
assert_ne!(config.diff_new, Style::new());
assert_ne!(config.status_added, Style::new());
assert_ne!(config.branch_current, Style::new());
}
}