gprofile 0.2.0

Quickly switch and manage multiple git user profiles.
use std::{
    collections::BTreeMap, // i want sorted keys
    env,
    error::Error,
    fs,
    io::{self, BufRead, Write},
    path::PathBuf,
    process::{self, Command, Stdio},
};

pub(crate) fn db_path() -> PathBuf {
    #[allow(deprecated)]
    let home_dir = env::home_dir().expect("home directory should be defined");

    home_dir.join(".config").join(env!("CARGO_PKG_NAME"))
}

pub(crate) fn db_file() -> PathBuf {
    db_path().join("db")
}

const DB_HEADER: &str = "# GENERATED BY gprofile, EDIT WITH CAUTION!\n\n";

pub(crate) fn init() -> Result<(), Box<dyn Error>> {
    let git = Command::new("git")
        .arg("--version")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .output()
        .is_ok();

    #[allow(clippy::print_literal)]
    if !git {
        eprintln!(
            "{0}: {1}\n{2}",
            env!("CARGO_PKG_NAME"),
            "Unable to find `git` in PATH, make sure it's installed.",
            "The tool compliments `git` it has no use on it's own."
        );
        process::exit(1);
    }

    let db_path = db_path();

    if !db_path.is_dir() {
        fs::create_dir_all(&db_path)?;
    }

    let db_file = db_file();

    if !db_file.exists() {
        let mut db = fs::File::create(db_file)?;

        db.write_all(DB_HEADER.as_bytes())?;

        db.flush()?;
    }

    Ok(())
}

pub(crate) fn create() -> Result<(), Box<dyn Error>> {
    let profile = prompt("Profile name: ")?;

    let mut db = read_from_db()?;

    if db.contains_key(&profile) {
        loop {
            if let Ok(response) = prompt("Profile already exist, overwrite? [y|n] ") {
                match response.to_lowercase().trim() {
                    "yes" | "y" => break,
                    "no" | "n" => return Ok(()),
                    _ => {}
                }
            }
        }
    }

    let name = prompt("Please input your git username: ")?;

    let email = prompt("Please input your git email: ")?;

    db.insert(
        profile,
        BTreeMap::from([("email".to_string(), email), ("name".to_string(), name)]),
    );

    write_to_db(db)?;

    Ok(())
}

pub(crate) fn list() -> Result<(), Box<dyn Error>> {
    let db = read_from_db()?;

    if db.is_empty() {
        eprintln!("No user profiles has been saved yet.");
        return Ok(());
    }

    println!("Available profiles: ");

    for profile in db.keys() {
        println!("  - {}", profile);
    }

    Ok(())
}

pub(crate) fn delete(profile: String) -> Result<(), Box<dyn Error>> {
    let mut db = read_from_db()?;

    if db.remove(&profile).is_none() {
        eprintln!("Profile '{}' was not found in db.", &profile);
        process::exit(1);
    }

    write_to_db(db)?;

    Ok(())
}

pub(crate) fn edit(profile: String) -> Result<(), Box<dyn Error>> {
    let mut db = read_from_db()?;

    if !db.contains_key(&profile) {
        eprintln!("Profile '{}' was not found in db.", &profile);
        process::exit(1);
    }

    let name = prompt("Please input your git username: ")?;

    let email = prompt("Please input your git email: ")?;

    db.insert(
        profile,
        BTreeMap::from([("email".to_string(), email), ("name".to_string(), name)]),
    );

    write_to_db(db)?;

    Ok(())
}

pub(crate) fn switch(profile: String) -> Result<(), Box<dyn Error>> {
    let db = read_from_db()?;

    if let Some(config) = db.get(&profile) {
        Command::new("git")
            .args(["config", "user.name", config.get("name").unwrap()])
            .output()?;

        Command::new("git")
            .args(["config", "user.email", config.get("email").unwrap()])
            .output()?;
    } else {
        eprintln!("Profile '{}' was not found in db.", &profile);
        process::exit(1);
    }

    Ok(())
}

type Db = BTreeMap<String, BTreeMap<String, String>>;

pub(crate) fn write_to_db(db: Db) -> Result<(), Box<dyn Error>> {
    let contents = db
        .iter()
        .fold(String::from(DB_HEADER), |mut acc, (profile, config)| {
            for (key, value) in config.iter() {
                acc.push_str(format!("{}.{} = {}\n", profile, key, value).as_str());
            }

            acc.push('\n');

            acc
        });

    let mut db = fs::File::create(db_file())?;

    db.write_all(contents.as_bytes())?;

    db.flush()?;

    Ok(())
}

pub(crate) fn read_from_db() -> Result<Db, Box<dyn Error>> {
    let db = fs::File::open(db_file())?;

    let buf = io::BufReader::new(db);

    let parsed = buf
        .lines()
        .filter_map(|line| {
            if let Ok(line) = line {
                if line.is_empty() || line.starts_with('#') {
                    None
                } else {
                    Some(line)
                }
            } else {
                None
            }
        })
        .fold(BTreeMap::new(), |mut acc: Db, line| {
            let profile_and_key_value = &line.split('=').collect::<Vec<_>>();

            if profile_and_key_value.len() != 2 {
                eprintln!("Error in line: \n{}", &line);
                process::exit(1);
            }

            let profile_and_key = profile_and_key_value[0].split('.').collect::<Vec<_>>();

            if profile_and_key.len() != 2 {
                eprintln!("Error in line: \n{:?}", profile_and_key);
                process::exit(1);
            }

            let profile = profile_and_key[0];
            let key = profile_and_key[1];
            let value = profile_and_key_value[1];

            if let Some(config) = acc.get_mut(&profile.to_string()) {
                config.insert(key.trim().to_string(), value.trim().to_string());
            } else {
                let mut config = BTreeMap::new();
                config.insert(key.trim().to_string(), value.trim().to_string());
                acc.insert(profile.trim().to_string(), config);
            }

            acc
        });

    Ok(parsed)
}

pub(crate) fn prompt(msg: &str) -> Result<String, Box<dyn Error>> {
    print!("{}", msg);

    io::stdout().flush()?;

    let mut answer = String::new();

    loop {
        io::stdin().read_line(&mut answer)?;

        if !answer.trim().is_empty() {
            break;
        }
    }

    Ok(answer.trim().to_string())
}