commit-emoji 0.2.4

A git hook for adorning conventional commits with emoji
//
// Partially borrowed from https://gitlab.com/ogarcia/lazycc/-/blob/master/src/config.rs
// SPDX: GPL-3.0-only
//
use {
    super::repo,
    anyhow::{anyhow, Context, Result},
    once_cell::sync::Lazy,
    resolve_path::PathResolveExt,
    serde::Deserialize,
    std::{collections::HashMap, fs::read_to_string, path::PathBuf},
};

const CONFIG_FILENAME: &str = ".commit-emoji.toml"; // for git hooks this is in the GIT_DIR/repo root
const GLOBAL_CONFIG: &str = "~/.config/commit-emoji/config.toml";

#[derive(Clone)]
pub struct Config {
    pub breaking: bool,
    pub commit_types: HashMap<String, String>, // Ignore descriptions
    pub replacements: HashMap<String, String>,
}

fn map_file_config_to_config(from_file: &FileConfig, from_default: &Config) -> Config {
    let mut out_config = from_default.clone();
    if let Some(breaking) = from_file.breaking {
        out_config.breaking = breaking;
    }
    if let Some(ref commit_types) = from_file.commit_types {
        for t in commit_types {
            out_config
                .commit_types
                .entry(t.name.clone())
                .and_modify(|e| *e = t.emoji.clone())
                .or_insert(t.emoji.clone());
        }
    }
    if let Some(ref replacements) = from_file.replacements {
        for r in replacements {
            out_config
                .replacements
                .entry(r.from.clone())
                .and_modify(|e| *e = r.to.clone())
                .or_insert(r.to.clone());
        }
    }
    out_config
}

#[derive(Deserialize)]
struct FileConfig {
    pub breaking: Option<bool>,
    pub commit_types: Option<Vec<EmojiMap>>,
    pub replacements: Option<Vec<ReplacementMap>>,
}

#[derive(Deserialize)]
struct EmojiMap {
    pub name: String,
    pub emoji: String,
    #[allow(dead_code)]
    pub description: Option<String>,
}

/// If entire commit message matches text `from` replace it entirely with the text `to`
#[derive(Deserialize)]
struct ReplacementMap {
    pub from: String,
    pub to: String,
}

fn config_from_file(path: &PathBuf) -> Result<FileConfig> {
    if path.exists() {
        let config_str =
            read_to_string(&path).context(format!("Failed to read config file {:?}", &path))?;
        let config: FileConfig = toml::from_str(&config_str)
            .context(format!("Failed to parse config file {:?}", &path))?;
        return Ok(config);
    }
    return Err(anyhow!("Failed to read config from {}", path.display()));
}

pub fn config() -> Result<Config> {
    let mut path = repo::get_absolute_path()?;
    path.pop();
    path.push(CONFIG_FILENAME);

    let file_config = config_from_file(&path)
        .or_else(|_| config_from_file(&PathBuf::from(GLOBAL_CONFIG.resolve())))
        .or_else(|_| {
            Ok::<FileConfig, anyhow::Error>(FileConfig {
                breaking: None,
                commit_types: None,
                replacements: None,
            })
        })
        .unwrap();

    Ok(map_file_config_to_config(&file_config, &*DEFAULT_CONFIG))
}

// A default config of emoji based on https://gist.github.com/berkus/5ce2cdf5dd74909bcd4faf6cb7d0ae18
// Changes to it can be made via a config file (~/.config/commit-emoji/config.toml or .commit-emoji.toml in repo dir)

static DEFAULT_CONFIG: Lazy<Config> = Lazy::new(|| Config {
    breaking: true,
    commit_types: HashMap::from([
        ("feat".into(), "โœจ".into()),
        ("fix".into(), "๐Ÿ›".into()),
        ("docs".into(), "๐Ÿ“š".into()),
        ("style".into(), "๐Ÿ’Ž".into()),
        ("refactor".into(), "๐Ÿ“ฆ".into()),
        ("perf".into(), "๐Ÿš€".into()),
        ("test".into(), "๐Ÿšจ".into()),
        ("build".into(), "๐Ÿ› ".into()),
        ("ci".into(), "โš™๏ธ".into()),
        ("chore".into(), "๐Ÿงน".into()),
        ("revert".into(), "๐Ÿ—‘".into()),
        ("deprecated".into(), "โ›”๏ธ".into()),
        ("removed".into(), "โŒ".into()),
        ("security".into(), "๐Ÿ”".into()),
        ("merge".into(), "โ›“๏ธ".into()),
        ("breaking".into(), "๐Ÿ’”".into()),
    ]),
    replacements: HashMap::from([
        ("initial".into(), "feat: ๐ŸŽ‰ Initial commit".into()),
        ("deps".into(), "fix: โซ Update dependencies".into()),
        ("peerdeps".into(), "fix: โฌ†๏ธ Update peer dependencies".into()),
        (
            "devdeps".into(),
            "chore: ๐Ÿ”ผ Update development dependencies".into(),
        ),
        (
            "metadata".into(),
            "fix: ๐Ÿ“ฆ Update metadata (Cargo.toml)".into(),
        ),
        ("typos".into(), "chore: โ™ป๏ธ Fix typos".into()),
    ]),
});