gitquick 0.2.0

Command line tool that simplifies common git workflows.
use std::{
    fs,
    io::{self, Write},
    path::PathBuf,
};

use directories::ProjectDirs;
use serde::{Deserialize, Serialize};

use crate::ConfigArgs;

#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(default)]
pub struct Config {
    pub commit: Commit,
    pub branch: Branch,
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(default)]
pub struct Commit {
    pub conventional: bool,
    pub ticket: bool,
    pub types: Vec<String>,
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(default)]
pub struct Branch {
    pub conventional: bool,
    pub types: Vec<String>,
}

impl Default for Commit {
    fn default() -> Self {
        Self {
            conventional: false,
            ticket: false,
            types: vec![
                "build".into(),
                "ci".into(),
                "docs".into(),
                "feat".into(),
                "fix".into(),
                "perf".into(),
                "refactor".into(),
                "style".into(),
                "test".into(),
                "revert".into(),
            ],
        }
    }
}

impl Default for Branch {
    fn default() -> Self {
        Self {
            conventional: false,
            types: vec![
                "feature".into(),
                "bugfix".into(),
                "hotfix".into(),
                "release".into(),
                "chore".into(),
            ],
        }
    }
}

pub fn load_config() -> Config {
    let config_path = get_config_path();

    if let Ok(config_content) = fs::read_to_string(&config_path) {
        toml::from_str(&config_content).expect("Failed to parse config")
    } else {
        let config = Config::default();
        let _ = save_config(&config, &config_path);
        config
    }
}

pub fn run_config(args: &ConfigArgs) -> Result<(), String> {
    let mut config = load_config();
    let config_path = get_config_path();
    let setting: Vec<&str> = args.key.split('.').collect();
    if setting.len() != 2 {
        return Err("Key format must be <section>.<field>".into());
    }
    let (section, field) = (setting[0], setting[1]);

    match section {
        "commit" => match field {
            "conventional" => set_bool(&mut config.commit.conventional, &args.value)?,
            "ticket" => set_bool(&mut config.commit.ticket, &args.value)?,
            "types" => set_vec(&mut config.commit.types, &args.value)?,
            _ => return Err(format!("Unknown commit setting '{}'", field)),
        },

        "branch" => match field {
            "conventional" => set_bool(&mut config.branch.conventional, &args.value)?,
            "types" => set_vec(&mut config.branch.types, &args.value)?,
            _ => return Err(format!("Unknown branch setting '{}'", field)),
        },

        _ => return Err(format!("Unknown section '{}'", section)),
    }
    save_config(&config, &config_path).map_err(|e| format!("Failed to save config: {}", e))?;
    println!("✅ Config created successfuly!");
    Ok(())
}

fn set_bool(target: &mut bool, value: &str) -> Result<(), String> {
    match value.to_lowercase().as_str() {
        "1" | "true" => *target = true,
        "0" | "false" => *target = false,
        _ => return Err("Value must be boolean".into()),
    }
    Ok(())
}

fn set_vec(target: &mut Vec<String>, value: &str) -> Result<(), String> {
    *target = value.split(',').map(|s| s.trim().to_string()).collect();
    Ok(())
}

fn get_config_path() -> PathBuf {
    let proj_dirs = ProjectDirs::from("", "", "gitquick").expect("Failed to get project directory");

    let directory = proj_dirs.config_dir();
    directory.join("config.toml")
}

fn save_config(config: &Config, config_path: &PathBuf) -> io::Result<()> {
    if let Some(directory) = config_path.parent() {
        fs::create_dir_all(directory)?
    };
    let content = toml::to_string(config).unwrap();
    let mut file = fs::File::create(config_path)?;
    file.write_all(content.as_bytes())?;
    Ok(())
}