Repository: dandavison/delta
Pull request #784 (2021-11-20 03:12:33)

Allow styles to be specified as references to other styles


Patch:
diff --git a/src/cli.rs b/src/cli.rs
index aa40ce1..5b0fd6a 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -334,9 +334,9 @@ pub struct Opt {
     /// STYLES section.
     pub minus_emph_style: String,
 
-    #[structopt(long = "minus-non-emph-style", default_value = "auto auto")]
+    #[structopt(long = "minus-non-emph-style", default_value = "minus-style")]
     /// Style (foreground, background, attributes) for non-emphasized sections of removed lines
-    /// that have an emphasized section. Defaults to --minus-style. See STYLES section.
+    /// that have an emphasized section. See STYLES section.
     pub minus_non_emph_style: String,
 
     #[structopt(long = "plus-emph-style", default_value = "syntax auto")]
@@ -344,9 +344,9 @@ pub struct Opt {
     /// STYLES section.
     pub plus_emph_style: String,
 
-    #[structopt(long = "plus-non-emph-style", default_value = "auto auto")]
+    #[structopt(long = "plus-non-emph-style", default_value = "plus-style")]
     /// Style (foreground, background, attributes) for non-emphasized sections of added lines that
-    /// have an emphasized section. Defaults to --plus-style. See STYLES section.
+    /// have an emphasized section. See STYLES section.
     pub plus_non_emph_style: String,
 
     #[structopt(long = "commit-style", default_value = "raw")]
diff --git a/src/config.rs b/src/config.rs
index 4ed7f27..71aa838 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -19,7 +19,8 @@ use crate::features::side_by_side::{self, ansifill, LeftRight};
 use crate::git_config::{GitConfig, GitConfigEntry};
 use crate::minusplus::MinusPlus;
 use crate::paint::BgFillMethod;
-use crate::style::{self, Style};
+use crate::parse_styles;
+use crate::style::Style;
 use crate::tests::TESTING;
 use crate::utils::bat::output::PagingMode;
 use crate::utils::syntect::FromDeltaStyle;
@@ -149,34 +150,7 @@ impl Config {
 
 impl From<cli::Opt> for Config {
     fn from(opt: cli::Opt) -> Self {
-        let (
-            minus_style,
-            minus_emph_style,
-            minus_non_emph_style,
-            minus_empty_line_marker_style,
-            zero_style,
-            plus_style,
-            plus_emph_style,
-            plus_non_emph_style,
-            plus_empty_line_marker_style,
-            whitespace_error_style,
-        ) = make_hunk_styles(&opt);
-
-        let (
-            commit_style,
-            file_style,
-            hunk_header_style,
-            hunk_header_file_style,
-            hunk_header_line_number_style,
-        ) = make_commit_file_hunk_header_styles(&opt);
-
-        let (
-            line_numbers_minus_style,
-            line_numbers_zero_style,
-            line_numbers_plus_style,
-            line_numbers_left_style,
-            line_numbers_right_style,
-        ) = make_line_number_styles(&opt);
+        let styles = parse_styles::parse_styles(&opt);
 
         let max_line_distance_for_naively_paired_lines =
             env::get_env_var("DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES")
@@ -201,22 +175,6 @@ impl From<cli::Opt> for Config {
             ));
         });
 
-        let inline_hint_style = Style::from_str(
-            &opt.inline_hint_style,
-            None,
-            None,
-            opt.computed.true_color,
-            false,
-        );
-        let git_minus_style = match opt.git_config_entries.get("color.diff.old") {
-            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
-            _ => *style::GIT_DEFAULT_MINUS_STYLE,
-        };
-        let git_plus_style = match opt.git_config_entries.get("color.diff.new") {
-            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
-            _ => *style::GIT_DEFAULT_PLUS_STYLE,
-        };
-
         let blame_palette = make_blame_palette(opt.blame_palette, opt.computed.is_light_mode);
 
         let file_added_label = opt.file_added_label;
@@ -267,7 +225,7 @@ impl From<cli::Opt> for Config {
             blame_format: opt.blame_format,
             blame_palette,
             blame_timestamp_format: opt.blame_timestamp_format,
-            commit_style,
+            commit_style: styles["commit-style"],
             color_only: opt.color_only,
             commit_regex,
             cwd_relative_to_repo_root: std::env::var("GIT_PREFIX").ok(),
@@ -282,12 +240,12 @@ impl From<cli::Opt> for Config {
             file_renamed_label,
             right_arrow,
             hunk_label,
-            file_style,
+            file_style: styles["file-style"],
             git_config: opt.git_config,
             git_config_entries: opt.git_config_entries,
-            hunk_header_file_style,
-            hunk_header_line_number_style,
-            hunk_header_style,
+            hunk_header_file_style: styles["hunk-header-file-style"],
+            hunk_header_line_number_style: styles["hunk-header-line-number-style"],
+            hunk_header_style: styles["hunk-header-style"],
             hunk_header_style_include_file_path: opt
                 .hunk_header_style
                 .split(' ')
@@ -300,7 +258,7 @@ impl From<cli::Opt> for Config {
             hyperlinks_commit_link_format: opt.hyperlinks_commit_link_format,
             hyperlinks_file_link_format: opt.hyperlinks_file_link_format,
             inspect_raw_lines: opt.computed.inspect_raw_lines,
-            inline_hint_style,
+            inline_hint_style: styles["inline-hint-style"],
             keep_plus_minus_markers: opt.keep_plus_minus_markers,
             line_fill_method: if !opt.computed.stdout_is_term && !TESTING {
                 // Don't write ANSI sequences (which rely on the width of the
@@ -316,14 +274,14 @@ impl From<cli::Opt> for Config {
                 opt.line_numbers_right_format,
             ),
             line_numbers_style_leftright: LeftRight::new(
-                line_numbers_left_style,
-                line_numbers_right_style,
+                styles["line-numbers-left-style"],
+                styles["line-numbers-right-style"],
             ),
             line_numbers_style_minusplus: MinusPlus::new(
-                line_numbers_minus_style,
-                line_numbers_plus_style,
+                styles["line-numbers-minus-style"],
+                styles["line-numbers-plus-style"],
             ),
-            line_numbers_zero_style,
+            line_numbers_zero_style: styles["line-numbers-zero-style"],
             line_buffer_size: opt.line_buffer_size,
             max_line_distance: opt.max_line_distance,
             max_line_distance_for_naively_paired_lines,
@@ -344,24 +302,24 @@ impl From<cli::Opt> for Config {
                     )
                 }
             },
-            minus_emph_style,
-            minus_empty_line_marker_style,
+            minus_emph_style: styles["minus-emph-style"],
+            minus_empty_line_marker_style: styles["minus-empty-line-marker-style"],
             minus_file: opt.minus_file,
-            minus_non_emph_style,
-            minus_style,
+            minus_non_emph_style: styles["minus-non-emph-style"],
+            minus_style: styles["minus-style"],
             navigate: opt.navigate,
             navigate_regexp,
             null_style: Style::new(),
             null_syntect_style: SyntectStyle::default(),
             pager: opt.pager,
             paging_mode: opt.computed.paging_mode,
-            plus_emph_style,
-            plus_empty_line_marker_style,
+            plus_emph_style: styles["plus-emph-style"],
+            plus_empty_line_marker_style: styles["plus-empty-line-marker-style"],
             plus_file: opt.plus_file,
-            plus_non_emph_style,
-            plus_style,
-            git_minus_style,
-            git_plus_style,
+            plus_non_emph_style: styles["plus-non-emph-style"],
+            plus_style: styles["plus-style"],
+            git_minus_style: styles["git-minus-style"],
+            git_plus_style: styles["git-plus-style"],
             relative_paths: opt.relative_paths,
             show_themes: opt.show_themes,
             side_by_side: opt.side_by_side,
@@ -397,223 +355,16 @@ impl From<cli::Opt> for Config {
                     }
                 },
                 max_lines: wrap_max_lines_plus1,
-                inline_hint_syntect_style: SyntectStyle::from_delta_style(inline_hint_style),
+                inline_hint_syntect_style: SyntectStyle::from_delta_style(
+                    styles["inline-hint-style"],
+                ),
             },
-            whitespace_error_style,
-            zero_style,
+            whitespace_error_style: styles["whitespace-error-style"],
+            zero_style: styles["zero-style"],
         }
     }
 }
 
-fn make_hunk_styles(
-    opt: &cli::Opt,
-) -> (
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-    Style,
-) {
-    let is_light_mode = opt.computed.is_light_mode;
-    let true_color = opt.computed.true_color;
-    let minus_style = Style::from_str(
-        &opt.minus_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_minus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let minus_emph_style = Style::from_str(
-        &opt.minus_emph_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_minus_emph_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        true,
-    );
-
-    let minus_non_emph_style = Style::from_str(
-        &opt.minus_non_emph_style,
-        Some(minus_style),
-        None,
-        true_color,
-        false,
-    );
-
-    // The style used to highlight a removed empty line when otherwise it would be invisible due to
-    // lack of background color in minus-style.
-    let minus_empty_line_marker_style = Style::from_str(
-        &opt.minus_empty_line_marker_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_minus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let zero_style = Style::from_str(&opt.zero_style, None, None, true_color, false);
-
-    let plus_style = Style::from_str(
-        &opt.plus_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_plus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let plus_emph_style = Style::from_str(
-        &opt.plus_emph_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_plus_emph_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        true,
-    );
-
-    let plus_non_emph_style = Style::from_str(
-        &opt.plus_non_emph_style,
-        Some(plus_style),
-        None,
-        true_color,
-        false,
-    );
-
-    // The style used to highlight an added empty line when otherwise it would be invisible due to
-    // lack of background color in plus-style.
-    let plus_empty_line_marker_style = Style::from_str(
-        &opt.plus_empty_line_marker_style,
-        Some(Style::from_colors(
-            None,
-            Some(color::get_plus_background_color_default(
-                is_light_mode,
-                true_color,
-            )),
-        )),
-        None,
-        true_color,
-        false,
-    );
-
-    let whitespace_error_style =
-        Style::from_str(&opt.whitespace_error_style, None, None, true_color, false);
-
-    (
-        minus_style,
-        minus_emph_style,
-        minus_non_emph_style,
-        minus_empty_line_marker_style,
-        zero_style,
-        plus_style,
-        plus_emph_style,
-        plus_non_emph_style,
-        plus_empty_line_marker_style,
-        whitespace_error_style,
-    )
-}
-
-fn make_line_number_styles(opt: &cli::Opt) -> (Style, Style, Style, Style, Style) {
-    let true_color = opt.computed.true_color;
-    let line_numbers_left_style =
-        Style::from_str(&opt.line_numbers_left_style, None, None, true_color, false);
-
-    let line_numbers_minus_style =
-        Style::from_str(&opt.line_numbers_minus_style, None, None, true_color, false);
-
-    let line_numbers_zero_style =
-        Style::from_str(&opt.line_numbers_zero_style, None, None, true_color, false);
-
-    let line_numbers_plus_style =
-        Style::from_str(&opt.line_numbers_plus_style, None, None, true_color, false);
-
-    let line_numbers_right_style =
-        Style::from_str(&opt.line_numbers_right_style, None, None, true_color, false);
-
-    (
-        line_numbers_minus_style,
-        line_numbers_zero_style,
-        line_numbers_plus_style,
-        line_numbers_left_style,
-        line_numbers_right_style,
-    )
-}
-
-fn make_commit_file_hunk_header_styles(opt: &cli::Opt) -> (Style, Style, Style, Style, Style) {
-    let true_color = opt.computed.true_color;
-    (
-        Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-            &opt.commit_style,
-            None,
-            Some(&opt.commit_decoration_style),
-            opt.deprecated_commit_color.as_deref(),
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-            &opt.file_style,
-            None,
-            Some(&opt.file_decoration_style),
-            opt.deprecated_file_color.as_deref(),
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-            &opt.hunk_header_style,
-            None,
-            Some(&opt.hunk_header_decoration_style),
-            opt.deprecated_hunk_color.as_deref(),
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes(
-            &opt.hunk_header_file_style,
-            None,
-            None,
-            true_color,
-            false,
-        ),
-        Style::from_str_with_handling_of_special_decoration_attributes(
-            &opt.hunk_header_line_number_style,
-            None,
-            None,
-            true_color,
-            false,
-        ),
-    )
-}
-
 fn make_blame_palette(blame_palette: Option<String>, is_light_mode: bool) -> Vec<String> {
     match (blame_palette, is_light_mode) {
         (Some(string), _) => string
diff --git a/src/main.rs b/src/main.rs
index 6a6e3c7..95bd397 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,6 +19,7 @@ mod minusplus;
 mod options;
 mod paint;
 mod parse_style;
+mod parse_styles;
 mod style;
 mod utils;
 mod wrapping;
diff --git a/src/paint.rs b/src/paint.rs
index ecf76e2..32a5978 100644
--- a/src/paint.rs
+++ b/src/paint.rs
@@ -691,14 +691,15 @@ impl<'p> Painter<'p> {
         } else {
             None
         };
-        Self::update_styles(&mut diff_sections.0, None, minus_non_emph_style);
+        let mut lines_style_sections = MinusPlus::new(&mut diff_sections.0, &mut diff_sections.1);
+        Self::update_styles(lines_style_sections[Minus], None, minus_non_emph_style);
         let plus_non_emph_style = if config.plus_non_emph_style != config.plus_emph_style {
             Some(config.plus_non_emph_style)
         } else {
             None
         };
         Self::update_styles(
-            &mut diff_sections.1,
+            lines_style_sections[Plus],
             Some(config.whitespace_error_style),
             plus_non_emph_style,
         );
@@ -714,29 +715,27 @@ impl<'p> Painter<'p> {
     /// 2. If the line constitutes a whitespace error, then the whitespace error style
     ///    should be applied to the added material.
     fn update_styles(
-        style_sections: &mut Vec<LineSegments<'_, Style>>,
+        lines_style_sections: &mut Vec<LineSegments<'_, Style>>,
         whitespace_error_style: Option<Style>,
         non_emph_style: Option<Style>,
     ) {
-        for line_sections in style_sections {
+        for style_sections in lines_style_sections {
             let line_has_emph_and_non_emph_sections =
-                style_sections_contain_more_than_one_style(line_sections);
+                style_sections_contain_more_than_one_style(style_sections);
             let should_update_non_emph_styles =
                 non_emph_style.is_some() && line_has_emph_and_non_emph_sections;
             let is_whitespace_error =
-                whitespace_error_style.is_some() && is_whitespace_error(line_sections);
-            for section in line_sections.iter_mut() {
+                whitespace_error_style.is_some() && is_whitespace_error(style_sections);
+            for (style, _) in style_sections.iter_mut() {
                 // If the line as a whole constitutes a whitespace error then highlight this
                 // section if either (a) it is an emph section, or (b) the line lacks any
                 // emph/non-emph distinction.
-                if is_whitespace_error
-                    && (section.0.is_emph || !line_has_emph_and_non_emph_sections)
-                {
-                    *section = (whitespace_error_style.unwrap(), section.1);
+                if is_whitespace_error && (style.is_emph || !line_has_emph_and_non_emph_sections) {
+                    *style = whitespace_error_style.unwrap();
                 }
                 // Otherwise, update the style if this is a non-emph section that needs updating.
-                else if should_update_non_emph_styles && !section.0.is_emph {
-                    *section = (non_emph_style.unwrap(), section.1);
+                else if should_update_non_emph_styles && !style.is_emph {
+                    *style = non_emph_style.unwrap();
                 }
             }
         }
diff --git a/src/parse_style.rs b/src/parse_style.rs
index 820cc03..6cdefe8 100644
--- a/src/parse_style.rs
+++ b/src/parse_style.rs
@@ -15,7 +15,6 @@ impl Style {
         default: Option<Self>,
         decoration_style_string: Option<&str>,
         true_color: bool,
-        is_emph: bool,
     ) -> Self {
         let (ansi_term_style, is_omitted, is_raw, is_syntax_highlighted) =
             parse_ansi_term_style(style_string, default, true_color);
@@ -23,7 +22,7 @@ impl Style {
             DecorationStyle::from_str(decoration_style_string.unwrap_or(""), true_color);
         Self {
             ansi_term_style,
-            is_emph,
+            is_emph: false,
             is_omitted,
             is_raw,
             is_syntax_highlighted,
@@ -32,7 +31,7 @@ impl Style {
     }
 
     pub fn from_git_str(git_style_string: &str) -> Self {
-        Self::from_str(git_style_string, None, None, true, false)
+        Self::from_str(git_style_string, None, None, true)
     }
 
     /// Construct Style but interpreting 'ul', 'box', etc as applying to the decoration style.
@@ -41,17 +40,11 @@ impl Style {
         default: Option<Self>,
         decoration_style_string: Option<&str>,
         true_color: bool,
-        is_emph: bool,
     ) -> Self {
         let (special_attributes_from_style_string, style_string) =
             extract_special_decoration_attributes_from_non_decoration_style_string(style_string);
-        let mut style = Style::from_str(
-            &style_string,
-            default,
-            decoration_style_string,
-            true_color,
-            is_emph,
-        );
+        let mut style =
+            Style::from_str(&style_string, default, decoration_style_string, true_color);
         // TODO: box in this context resulted in box-with-underline for commit and file
         style.decoration_style = DecorationStyle::apply_special_decoration_attributes(
             &mut style,
@@ -68,14 +61,12 @@ impl Style {
         decoration_style_string: Option<&str>,
         deprecated_foreground_color_arg: Option<&str>,
         true_color: bool,
-        is_emph: bool,
     ) -> Self {
         let mut style = Self::from_str_with_handling_of_special_decoration_attributes(
             style_string,
             default,
             decoration_style_string,
             true_color,
-            is_emph,
         );
         if let Some(s) = deprecated_foreground_color_arg {
             // The deprecated --{commit,file,hunk}-color args functioned to set the decoration
@@ -530,7 +521,6 @@ mod tests {
             None,
             Some("ol red box bold green ul"),
             true,
-            false,
         );
         let red_green_bold = ansi_term::Style {
             foreground: Some(ansi_term::Color::Red),
@@ -550,7 +540,7 @@ mod tests {
 
     #[test]
     fn test_style_from_str_raw_with_box() {
-        let actual_style = Style::from_str("raw", None, Some("box"), true, false);
+        let actual_style = Style::from_str("raw", None, Some("box"), true);
         let empty_ansi_term_style = ansi_term::Style::new();
         assert_eq!(
             actual_style,
@@ -565,7 +555,7 @@ mod tests {
 
     #[test]
     fn test_style_from_str_decoration_style_only() {
-        let actual_style = Style::from_str("", None, Some("ol red box bold green ul"), true, false);
+        let actual_style = Style::from_str("", None, Some("ol red box bold green ul"), true);
         let red_green_bold = ansi_term::Style {
             foreground: Some(ansi_term::Color::Red),
             background: Some(ansi_term::Color::Green),
@@ -588,7 +578,6 @@ mod tests {
             None,
             Some("ol red box bold green ul"),
             true,
-            false,
         );
         let expected_decoration_style = DecorationStyle::BoxWithUnderOverline(ansi_term::Style {
             foreground: Some(ansi_term::Color::Red),
@@ -612,7 +601,6 @@ mod tests {
             None,
             Some("box"),
             true,
-            false,
         );
         let empty_ansi_term_style = ansi_term::Style::new();
         assert_eq!(
@@ -636,7 +624,7 @@ mod tests {
             ..ansi_term::Style::new()
         });
         let actual_style = Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
-                "", None, Some("ol red box bold green ul"), None, true, false
+                "", None, Some("ol red box bold green ul"), None, true
             );
         assert_eq!(
             actual_style,
@@ -656,7 +644,6 @@ mod tests {
             Some("box"),
             None,
             true,
-            false,
         );
         let empty_ansi_term_style = ansi_term::Style::new();
         assert_eq!(
diff --git a/src/parse_styles.rs b/src/parse_styles.rs
new file mode 100644
index 0000000..197b269
--- /dev/null
+++ b/src/parse_styles.rs
@@ -0,0 +1,430 @@
+use std::collections::{HashMap, HashSet};
+
+use crate::cli;
+use crate::color;
+use crate::fatal;
+use crate::git_config::GitConfigEntry;
+use crate::style::{self, Style};
+
+#[derive(Debug, Clone)]
+enum StyleReference {
+    Style(Style),
+    Reference(String),
+}
+
+fn is_style_reference(style_string: &str) -> bool {
+    style_string.ends_with("-style") && !style_string.chars().any(|c| c == ' ')
+}
+
+pub fn parse_styles(opt: &cli::Opt) -> HashMap<String, Style> {
+    let mut styles: HashMap<&str, StyleReference> = HashMap::new();
+
+    make_hunk_styles(opt, &mut styles);
+    make_commit_file_hunk_header_styles(opt, &mut styles);
+    make_line_number_styles(opt, &mut styles);
+    styles.insert(
+        "inline-hint-style",
+        style_from_str(&opt.inline_hint_style, None, None, opt.computed.true_color),
+    );
+    styles.insert(
+        "git-minus-style",
+        StyleReference::Style(match opt.git_config_entries.get("color.diff.old") {
+            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
+            _ => *style::GIT_DEFAULT_MINUS_STYLE,
+        }),
+    );
+    styles.insert(
+        "git-plus-style",
+        StyleReference::Style(match opt.git_config_entries.get("color.diff.new") {
+            Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
+            _ => *style::GIT_DEFAULT_PLUS_STYLE,
+        }),
+    );
+    let mut resolved_styles = resolve_style_references(styles, opt);
+    resolved_styles.get_mut("minus-emph-style").unwrap().is_emph = true;
+    resolved_styles.get_mut("plus-emph-style").unwrap().is_emph = true;
+    resolved_styles
+}
+
+fn resolve_style_references(
+    edges: HashMap<&str, StyleReference>,
+    opt: &cli::Opt,
+) -> HashMap<String, Style> {
+    let mut resolved_styles = HashMap::new();
+
+    for starting_node in edges.keys() {
+        if resolved_styles.contains_key(*starting_node) {
+            continue;
+        }
+        let mut visited = HashSet::new();
+        let mut node = *starting_node;
+        loop {
+            if !visited.insert(node) {
+                #[cfg(not(test))]
+                fatal(format!("Your delta styles form a cycle! {:?}", visited));
+                #[cfg(test)]
+                return [("__cycle__", Style::default())]
+                    .iter()
+                    .map(|(a, b)| (a.to_string(), *b))
+                    .collect();
+            }
+            match &edges.get(&node) {
+                Some(StyleReference::Reference(child_node)) => node = child_node,
+                Some(StyleReference::Style(style)) => {
+                    resolved_styles.extend(visited.iter().map(|node| (node.to_string(), *style)));
+                    break;
+                }
+                None => {
+                    if let Some(git_config) = &opt.git_config {
+                        match git_config.get::<String>(&format!("delta.{}", node)) {
+                            Some(s) => {
+                                let style = Style::from_git_str(&s);
+                                resolved_styles
+                                    .extend(visited.iter().map(|node| (node.to_string(), style)));
+                                break;
+                            }
+                            _ => fatal(format!("Style not found: {}", node)),
+                        }
+                    } else {
+                        fatal(format!("Style not found: {}", node));
+                    }
+                }
+            }
+        }
+    }
+    resolved_styles
+}
+
+fn make_hunk_styles<'a>(opt: &'a cli::Opt, styles: &'a mut HashMap<&str, StyleReference>) {
+    let is_light_mode = opt.computed.is_light_mode;
+    let true_color = opt.computed.true_color;
+    let minus_style = style_from_str(
+        &opt.minus_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_minus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+    );
+
+    let minus_emph_style = style_from_str(
+        &opt.minus_emph_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_minus_emph_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+    );
+
+    let minus_non_emph_style = style_from_str(&opt.minus_non_emph_style, None, None, true_color);
+
+    // The style used to highlight a removed empty line when otherwise it would be invisible due to
+    // lack of background color in minus-style.
+    let minus_empty_line_marker_style = style_from_str(
+        &opt.minus_empty_line_marker_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_minus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+    );
+
+    let zero_style = style_from_str(&opt.zero_style, None, None, true_color);
+
+    let plus_style = style_from_str(
+        &opt.plus_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_plus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+    );
+
+    let plus_emph_style = style_from_str(
+        &opt.plus_emph_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_plus_emph_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+    );
+
+    let plus_non_emph_style = style_from_str(&opt.plus_non_emph_style, None, None, true_color);
+
+    // The style used to highlight an added empty line when otherwise it would be invisible due to
+    // lack of background color in plus-style.
+    let plus_empty_line_marker_style = style_from_str(
+        &opt.plus_empty_line_marker_style,
+        Some(Style::from_colors(
+            None,
+            Some(color::get_plus_background_color_default(
+                is_light_mode,
+                true_color,
+            )),
+        )),
+        None,
+        true_color,
+    );
+
+    let whitespace_error_style =
+        style_from_str(&opt.whitespace_error_style, None, None, true_color);
+
+    styles.extend([
+        ("minus-style", minus_style),
+        ("minus-emph-style", minus_emph_style),
+        ("minus-non-emph-style", minus_non_emph_style),
+        (
+            "minus-empty-line-marker-style",
+            minus_empty_line_marker_style,
+        ),
+        ("zero-style", zero_style),
+        ("plus-style", plus_style),
+        ("plus-emph-style", plus_emph_style),
+        ("plus-non-emph-style", plus_non_emph_style),
+        ("plus-empty-line-marker-style", plus_empty_line_marker_style),
+        ("whitespace-error-style", whitespace_error_style),
+    ])
+}
+
+fn make_line_number_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) {
+    let true_color = opt.computed.true_color;
+    let line_numbers_left_style =
+        style_from_str(&opt.line_numbers_left_style, None, None, true_color);
+
+    let line_numbers_minus_style =
+        style_from_str(&opt.line_numbers_minus_style, None, None, true_color);
+
+    let line_numbers_zero_style =
+        style_from_str(&opt.line_numbers_zero_style, None, None, true_color);
+
+    let line_numbers_plus_style =
+        style_from_str(&opt.line_numbers_plus_style, None, None, true_color);
+
+    let line_numbers_right_style =
+        style_from_str(&opt.line_numbers_right_style, None, None, true_color);
+
+    styles.extend([
+        ("line-numbers-minus-style", line_numbers_minus_style),
+        ("line-numbers-zero-style", line_numbers_zero_style),
+        ("line-numbers-plus-style", line_numbers_plus_style),
+        ("line-numbers-left-style", line_numbers_left_style),
+        ("line-numbers-right-style", line_numbers_right_style),
+    ])
+}
+
+fn make_commit_file_hunk_header_styles(opt: &cli::Opt, styles: &mut HashMap<&str, StyleReference>) {
+    let true_color = opt.computed.true_color;
+    styles.extend([
+        ("commit-style",
+            style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+                &opt.commit_style,
+                None,
+                Some(&opt.commit_decoration_style),
+                opt.deprecated_commit_color.as_deref(),
+                true_color,
+            )
+        ),
+        ("file-style",
+            style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+                &opt.file_style,
+                None,
+                Some(&opt.file_decoration_style),
+                opt.deprecated_file_color.as_deref(),
+                true_color,
+            )
+        ),
+        ("hunk-header-style",
+            style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+                &opt.hunk_header_style,
+                None,
+                Some(&opt.hunk_header_decoration_style),
+                opt.deprecated_hunk_color.as_deref(),
+                true_color,
+            )
+        ),
+        ("hunk-header-file-style",
+            style_from_str_with_handling_of_special_decoration_attributes(
+                &opt.hunk_header_file_style,
+                None,
+                None,
+                true_color,
+            )
+        ),
+        ("hunk-header-line-number-style",
+            style_from_str_with_handling_of_special_decoration_attributes(
+                &opt.hunk_header_line_number_style,
+                None,
+                None,
+                true_color,
+            )
+        ),
+    ]);
+}
+
+fn style_from_str(
+    style_string: &str,
+    default: Option<Style>,
+    decoration_style_string: Option<&str>,
+    true_color: bool,
+) -> StyleReference {
+    if is_style_reference(style_string) {
+        StyleReference::Reference(style_string.to_owned())
+    } else {
+        StyleReference::Style(Style::from_str(
+            style_string,
+            default,
+            decoration_style_string,
+            true_color,
+        ))
+    }
+}
+
+fn style_from_str_with_handling_of_special_decoration_attributes(
+    style_string: &str,
+    default: Option<Style>,
+    decoration_style_string: Option<&str>,
+    true_color: bool,
+) -> StyleReference {
+    if is_style_reference(style_string) {
+        StyleReference::Reference(style_string.to_owned())
+    } else {
+        StyleReference::Style(
+            Style::from_str_with_handling_of_special_decoration_attributes(
+                style_string,
+                default,
+                decoration_style_string,
+                true_color,
+            ),
+        )
+    }
+}
+
+fn style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+    style_string: &str,
+    default: Option<Style>,
+    decoration_style_string: Option<&str>,
+    deprecated_foreground_color_arg: Option<&str>,
+    true_color: bool,
+) -> StyleReference {
+    if is_style_reference(style_string) {
+        StyleReference::Reference(style_string.to_owned())
+    } else {
+        StyleReference::Style(Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg(
+            style_string,
+            default,
+            decoration_style_string,
+            deprecated_foreground_color_arg,
+            true_color,
+        ))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::tests::integration_test_utils;
+
+    fn resolve_style_references(edges: HashMap<&str, StyleReference>) -> HashMap<String, Style> {
+        let opt = integration_test_utils::make_options_from_args(&[]);
+        super::resolve_style_references(edges, &opt)
+    }
+
+    #[test]
+    fn test_resolve_style_references_1() {
+        let style_1 = Style::default();
+        let mut style_2 = Style::default();
+        style_2.is_syntax_highlighted = !style_1.is_syntax_highlighted;
+
+        let edges: HashMap<&str, StyleReference> = [
+            ("a", StyleReference::Style(style_1)),
+            ("b", StyleReference::Reference("c".to_string())),
+            ("c", StyleReference::Style(style_2)),
+        ]
+        .iter()
+        .map(|(a, b)| (*a, b.clone()))
+        .collect();
+
+        let expected = [("a", style_1), ("b", style_2), ("c", style_2)]
+            .iter()
+            .map(|(a, b)| (a.to_string(), *b))
+            .collect();
+
+        assert_eq!(resolve_style_references(edges), expected);
+    }
+
+    #[test]
+    fn test_resolve_style_references_2() {
+        let style_1 = Style::default();
+        let mut style_2 = Style::default();
+        style_2.is_syntax_highlighted = !style_1.is_syntax_highlighted;
+
+        let edges: HashMap<&str, StyleReference> = [
+            ("a", StyleReference::Reference("b".to_string())),
+            ("b", StyleReference::Reference("c".to_string())),
+            ("c", StyleReference::Style(style_1)),
+            ("d", StyleReference::Reference("b".to_string())),
+            ("e", StyleReference::Reference("a".to_string())),
+            ("f", StyleReference::Style(style_2)),
+            ("g", StyleReference::Reference("f".to_string())),
+            ("h", StyleReference::Reference("g".to_string())),
+            ("i", StyleReference::Reference("g".to_string())),
+        ]
+        .iter()
+        .map(|(a, b)| (*a, b.clone()))
+        .collect();
+
+        let expected = [
+            ("a", style_1),
+            ("b", style_1),
+            ("c", style_1),
+            ("d", style_1),
+            ("e", style_1),
+            ("f", style_2),
+            ("g", style_2),
+            ("h", style_2),
+            ("i", style_2),
+        ]
+        .iter()
+        .map(|(a, b)| (a.to_string(), *b))
+        .collect();
+
+        assert_eq!(resolve_style_references(edges), expected);
+    }
+
+    #[test]
+    fn test_resolve_style_references_cycle() {
+        let edges: HashMap<&str, StyleReference> = [
+            ("a", StyleReference::Reference("b".to_string())),
+            ("b", StyleReference::Reference("c".to_string())),
+            ("c", StyleReference::Reference("a".to_string())),
+        ]
+        .iter()
+        .map(|(a, b)| (*a, b.clone()))
+        .collect();
+
+        assert_eq!(
+            resolve_style_references(edges).keys().next().unwrap(),
+            "__cycle__"
+        );
+    }
+}
diff --git a/themes.gitconfig b/themes.gitconfig
index 707e71c..28764ee 100644
--- a/themes.gitconfig
+++ b/themes.gitconfig
@@ -75,6 +75,10 @@
 
 [delta "hoopoe"]
     # author: https://github.com/dandavison
+    dark-pink-style = normal "#ffc0c0"
+    pink-style = normal "#ffe0e0"
+    green-style = syntax "#d0ffd0"
+    dark-green-style = syntax "#a0efa0"
     commit-decoration-style = blue ol
     commit-style = raw
     file-style = omit
@@ -83,14 +87,14 @@
     hunk-header-line-number-style = "#067a00"
     hunk-header-style = file line-number syntax
     light = true
-    minus-emph-style = normal "#ffc0c0"
-    minus-empty-line-marker-style = normal "#ffe0e0"
-    minus-non-emph-style = normal "#ffe0e0"
-    minus-style = normal "#ffe0e0"
-    plus-emph-style = syntax "#a0efa0"
-    plus-empty-line-marker-style = normal "#d0ffd0"
-    plus-non-emph-style = syntax "#d0ffd0"
-    plus-style = syntax "#d0ffd0"
+    minus-emph-style = dark-pink-style
+    minus-empty-line-marker-style = minus-style
+    minus-non-emph-style = minus-style
+    minus-style = pink-style
+    plus-emph-style = dark-green-style
+    plus-empty-line-marker-style = plus-style
+    plus-non-emph-style = plus-style
+    plus-style = green-style
     syntax-theme = GitHub
     zero-style = syntax
 


Test patch:
diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs
index e63f0ea..259c43b 100644
--- a/src/tests/ansi_test_utils.rs
+++ b/src/tests/ansi_test_utils.rs
@@ -167,7 +167,7 @@ pub mod ansi_test_utils {
     ) -> bool {
         let line = output.lines().nth(line_number).unwrap();
         assert!(ansi::strip_ansi_codes(line).starts_with(expected_prefix));
-        let mut style = Style::from_str(expected_style, None, None, config.true_color, false);
+        let mut style = Style::from_str(expected_style, None, None, config.true_color);
         if _4_bit_color {
             style.ansi_term_style.foreground = style
                 .ansi_term_style
diff --git a/src/tests/test_example_diffs.rs b/src/tests/test_example_diffs.rs
index a6d5f49..9b92af4 100644
--- a/src/tests/test_example_diffs.rs
+++ b/src/tests/test_example_diffs.rs
@@ -1471,7 +1471,7 @@ src/align.rs:71: impl<'a> Alignment<'a> { │
         let output = integration_test_utils::run_delta(example_diff, &config);
         let line = output.lines().nth(8).unwrap();
         if base_style_has_background_color {
-            let style = style::Style::from_str(base_style, None, None, true, false);
+            let style = style::Style::from_str(base_style, None, None, true);
             assert_eq!(
                 line,
                 &style