git-select-branch 0.2.3

Tiny CLI utility to checkout a recent git branch interactively
use anyhow::{anyhow, Context};
use dialoguer::theme::{ColorfulTheme, Theme};
use std::convert::TryFrom;
use std::sync::Arc;
use std::usize;

#[derive(Clone)]
pub struct Config {
    pub theme: Arc<dyn Theme>,
    pub fuzzy: bool,
    pub show_remote_branches: bool,
    pub limit: Option<usize>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            theme: Arc::new(ColorfulTheme::default()),
            fuzzy: true,
            show_remote_branches: false,
            limit: Some(20usize),
        }
    }
}

macro_rules! extract_config_value {
    ($config:ident, bool, $option_name:expr) => {
        map_git2_not_found_to_none($config.get_bool($option_name))
            .with_context(|| format!("Error parsing boolean value in {}", $option_name))?
    };

    ($config:ident, str, $option_name:expr) => {
        map_git2_not_found_to_none($config.get_str($option_name))
            .with_context(|| format!("Error parsing string value in {}", $option_name))?
    };

    ($config:ident, i64, $option_name:expr) => {
        map_git2_not_found_to_none($config.get_i64($option_name))
            .with_context(|| format!("Error parsing integer value in {}", $option_name))?
    };
}

impl Config {
    pub fn from_git_config(git_config: &git2::Config) -> anyhow::Result<Config> {
        let mut config = Config::default();
        if let Some(value) = extract_config_value!(git_config, bool, "select-branch.fuzzy") {
            config.fuzzy = value;
        }

        if let Some(value) =
            extract_config_value!(git_config, bool, "select-branch.show-remote-branches")
        {
            config.show_remote_branches = value;
        }

        if let Some(value) = extract_config_value!(git_config, str, "select-branch.theme") {
            config.theme = crate::match_theme_config(value)
                .with_context(|| "Could not parse theme configuration")?;
        }

        if let Some("none") = extract_config_value!(git_config, str, "select-branch.limit") {
            config.limit = None
        } else if let Some(limit) = extract_config_value!(git_config, i64, "select-branch.limit") {
            if limit <= 0 {
                return Err(anyhow!(
                    "\"{}\" is not a valid \"select-branch.limit\" value.\n\
                    The value must be either a positive integer, or \"none\". e.g.:\n\
                    > git config --global select-branch.limit none\n\
                    or\n\
                    > git config --global select-branch.limit 20",
                    limit
                ));
            }
            config.limit = Some(
                usize::try_from(limit)
                    .with_context(|| format!("Can't convert {limit:?} to usize"))?,
            )
        }

        Ok(config)
    }
}

fn map_git2_not_found_to_none<E>(
    config_result: anyhow::Result<E, git2::Error>,
) -> Result<Option<E>, git2::Error> {
    config_result
        .map(|value| Some(value))
        .or_else(|err| match err.code() {
            git2::ErrorCode::NotFound => Ok(None),
            _ => Err(err),
        })
}