git-delta 0.10.3

A syntax-highlighting pager for git
/// This module applies rewrite rules to the command line options, in order to
/// 1. Express deprecated usages in the new non-deprecated form
/// 2. Implement options such as --raw which are defined to be equivalent to some set of
///    other options.
use structopt::clap;

use crate::cli;
use crate::config::user_supplied_option;
use crate::fatal;

pub fn apply_rewrite_rules(opt: &mut cli::Opt, arg_matches: &clap::ArgMatches) {
    rewrite_style_strings_to_honor_deprecated_minus_plus_options(opt);
    rewrite_options_to_implement_deprecated_commit_and_file_style_box_option(opt);
    rewrite_options_to_implement_deprecated_hunk_style_option(opt);
    rewrite_options_to_implement_deprecated_theme_option(opt, arg_matches);
}

/// Honor deprecated --theme
fn rewrite_options_to_implement_deprecated_theme_option(
    opt: &mut cli::Opt,
    arg_matches: &clap::ArgMatches,
) {
    if user_supplied_option("deprecated-theme", arg_matches) {
        if let Some(syntax_theme) = opt.deprecated_theme.as_ref() {
            opt.syntax_theme = Some(syntax_theme.to_string());
        }
    }
}

/// Honor deprecated arguments by rewriting the canonical --*-style arguments if appropriate.
// TODO: How to avoid repeating the default values for style options here and in
// the structopt definition?
fn rewrite_style_strings_to_honor_deprecated_minus_plus_options(opt: &mut cli::Opt) {
    // If --highlight-removed was passed then we should set minus and minus emph foreground to
    // "syntax", if they are still at their default values.
    let deprecated_minus_foreground_arg = if opt.deprecated_highlight_minus_lines {
        Some("syntax")
    } else {
        None
    };

    if let Some(rewritten) = _get_rewritten_minus_plus_style_string(
        &opt.minus_style,
        ("normal", "auto"),
        (
            deprecated_minus_foreground_arg,
            opt.deprecated_minus_background_color.as_deref(),
        ),
        "minus",
    ) {
        opt.minus_style = rewritten;
    }
    if let Some(rewritten) = _get_rewritten_minus_plus_style_string(
        &opt.minus_emph_style,
        ("normal", "auto"),
        (
            deprecated_minus_foreground_arg,
            opt.deprecated_minus_emph_background_color.as_deref(),
        ),
        "minus-emph",
    ) {
        opt.minus_emph_style = rewritten;
    }
    if let Some(rewritten) = _get_rewritten_minus_plus_style_string(
        &opt.plus_style,
        ("syntax", "auto"),
        (None, opt.deprecated_plus_background_color.as_deref()),
        "plus",
    ) {
        opt.plus_style = rewritten;
    }
    if let Some(rewritten) = _get_rewritten_minus_plus_style_string(
        &opt.plus_emph_style,
        ("syntax", "auto"),
        (None, opt.deprecated_plus_emph_background_color.as_deref()),
        "plus-emph",
    ) {
        opt.plus_emph_style = rewritten;
    }
}

/// For backwards-compatibility, --{commit,file}-style box means --element-decoration-style 'box ul'.
fn rewrite_options_to_implement_deprecated_commit_and_file_style_box_option(opt: &mut cli::Opt) {
    if &opt.commit_style == "box" {
        opt.commit_decoration_style = format!("box ul {}", opt.commit_decoration_style);
        opt.commit_style.clear();
    }
    if &opt.file_style == "box" {
        opt.file_decoration_style = format!("box ul {}", opt.file_decoration_style);
        opt.file_style.clear();
    }
}

fn rewrite_options_to_implement_deprecated_hunk_style_option(opt: &mut cli::Opt) {
    // Examples of how --hunk-style was originally used are
    // --hunk-style box       => --hunk-header-decoration-style box
    // --hunk-style underline => --hunk-header-decoration-style underline
    // --hunk-style plain     => --hunk-header-decoration-style ''
    if opt.deprecated_hunk_style.is_some() {
        // As in the other cases, we only honor the deprecated option if the replacement option has
        // apparently been left at its default value.
        let hunk_header_decoration_default = "blue box";
        if opt.hunk_header_decoration_style != hunk_header_decoration_default {
            fatal(
                  "Deprecated option --hunk-style cannot be used with --hunk-header-decoration-style. \
                   Use --hunk-header-decoration-style.");
        }
        match opt.deprecated_hunk_style.as_deref().map(str::to_lowercase) {
            Some(attr) if attr == "plain" => opt.hunk_header_decoration_style = "".to_string(),
            Some(attr) if attr.is_empty() => {}
            Some(attr) => opt.hunk_header_decoration_style = attr,
            None => {}
        }
        opt.deprecated_hunk_style = None;
    }
}

fn _get_rewritten_commit_file_hunk_header_style_string(
    style_default_pair: (&str, Option<&str>),
    deprecated_args_style_pair: (Option<&str>, Option<&str>),
) -> Option<String> {
    let format_style = |pair: (&str, Option<&str>)| {
        format!(
            "{}{}",
            pair.0,
            match pair.1 {
                Some(s) => format!(" {}", s),
                None => "".to_string(),
            }
        )
    };
    match deprecated_args_style_pair {
        (None, None) => None,
        deprecated_args_style_pair => Some(format_style((
            deprecated_args_style_pair.0.unwrap_or(style_default_pair.0),
            match deprecated_args_style_pair.1 {
                Some(s) => Some(s),
                None => style_default_pair.1,
            },
        ))),
    }
}

fn _get_rewritten_minus_plus_style_string(
    style: &str,
    style_default_pair: (&str, &str),
    deprecated_args_style_pair: (Option<&str>, Option<&str>),
    element_name: &str,
) -> Option<String> {
    let format_style = |pair: (&str, &str)| format!("{} {}", pair.0, pair.1);
    match (style, deprecated_args_style_pair) {
        (_, (None, None)) => None, // no rewrite
        (style, deprecated_args_style_pair) if style == format_style(style_default_pair) => {
            // TODO: We allow the deprecated argument values to have effect if
            // the style argument value is equal to its default value. This is
            // non-ideal, because the user may have explicitly supplied the
            // style argument (i.e. it might just happen to equal the default).
            Some(format_style((
                deprecated_args_style_pair.0.unwrap_or(style_default_pair.0),
                deprecated_args_style_pair.1.unwrap_or(style_default_pair.1),
            )))
        }
        (_, (_, Some(_))) => {
            fatal(format!(
                "--{name}-color cannot be used with --{name}-style. \
                 Use --{name}-style=\"fg bg attr1 attr2 ...\" to set \
                 foreground color, background color, and style attributes. \
                 --{name}-color can only be used to set the background color. \
                 (It is still available for backwards-compatibility.)",
                name = element_name,
            ));
        }
        (_, (Some(_), None)) => {
            fatal(format!(
                "Deprecated option --highlight-removed cannot be used with \
                 --{name}-style. Use --{name}-style=\"fg bg attr1 attr2 ...\" \
                 to set foreground color, background color, and style \
                 attributes.",
                name = element_name,
            ));
        }
    }
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;

    use structopt::{clap, StructOpt};

    use crate::cli;
    use crate::options::rewrite::apply_rewrite_rules;

    /// Since --hunk-header-decoration-style is at its default value of "box",
    /// the deprecated option is allowed to overwrite it.
    #[test]
    fn test_deprecated_hunk_style_is_rewritten() {
        let mut opt = cli::Opt::from_iter(Vec::<OsString>::new());
        opt.deprecated_hunk_style = Some("underline".to_string());
        let default = "blue box";
        assert_eq!(opt.hunk_header_decoration_style, default);
        apply_rewrite_rules(&mut opt, &clap::ArgMatches::new());
        assert_eq!(opt.deprecated_hunk_style, None);
        assert_eq!(opt.hunk_header_decoration_style, "underline");
    }

    #[test]
    fn test_deprecated_hunk_style_is_not_rewritten() {
        let mut opt = cli::Opt::from_iter(Vec::<OsString>::new());
        opt.deprecated_hunk_style = Some("".to_string());
        let default = "blue box";
        assert_eq!(opt.hunk_header_decoration_style, default);
        apply_rewrite_rules(&mut opt, &clap::ArgMatches::new());
        assert_eq!(opt.deprecated_hunk_style, None);
        assert_eq!(opt.hunk_header_decoration_style, default);
    }
}