repo_author 0.2.0

add, remove, list project's author.txt
use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};

use colored::Colorize as _;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut args = env::args();
    let program = args.next().unwrap_or_else(|| "author".to_string());
    let command = args.next();
    let repo_root = find_repo_root(env::current_dir()?)?;
    let author_path = repo_root.join("author.txt");

    match command.as_deref() {
        None => list_authors(&author_path, &program)?,
        Some("add") => add_authors(&author_path, args.collect())?,
        Some("remove") => {
            let removals: Vec<String> = args.collect();
            if removals.is_empty() {
                prompt_remove(&author_path)?;
            } else {
                remove_authors(&author_path, removals)?;
            }
        }
        Some(other) => {
            return Err(format!("Unknown command: {other}").into());
        }
    }

    Ok(())
}

fn find_repo_root(start: PathBuf) -> Result<PathBuf, Box<dyn std::error::Error>> {
    let mut current = start.as_path();
    loop {
        if current.join(".git").is_dir() {
            return Ok(current.to_path_buf());
        }
        match current.parent() {
            Some(parent) => current = parent,
            None => return Err("Not inside a git repository".into()),
        }
    }
}

fn list_authors(path: &Path, program: &str) -> Result<(), Box<dyn std::error::Error>> {
    let authors = read_authors(path)?;
    if authors.is_empty() {
        let prefix = "no authors specified, run ".italic();
        let command = format!("{program} add login").bold();
        let suffix = " to add them".italic();
        println!("{prefix}{command}{suffix}");
        return Ok(());
    }
    for author in authors {
        println!("{author}");
    }
    Ok(())
}

fn add_authors(path: &Path, logins: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    if logins.is_empty() {
        return Err("add requires at least one login".into());
    }
    let mut authors = read_authors(path)?;
    for login in logins {
        authors.insert(login);
    }
    write_authors(path, &authors)
}

fn prompt_remove(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
    let authors = read_authors(path)?;
    if authors.is_empty() {
        return Ok(());
    }
    let options: Vec<String> = authors.iter().cloned().collect();
    let selections = inquire::MultiSelect::new("Select authors to remove", options).prompt()?;
    if selections.is_empty() {
        return Ok(());
    }
    let selection_set: BTreeSet<String> = selections.into_iter().collect();
    let remaining: BTreeSet<String> = authors
        .into_iter()
        .filter(|author| !selection_set.contains(author))
        .collect();
    write_authors(path, &remaining)
}

fn remove_authors(path: &Path, removals: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    let mut authors = read_authors(path)?;
    for removal in removals {
        authors.remove(&removal);
    }
    write_authors(path, &authors)
}

fn read_authors(path: &Path) -> Result<BTreeSet<String>, Box<dyn std::error::Error>> {
    if !path.exists() {
        return Ok(BTreeSet::new());
    }
    let contents = fs::read_to_string(path)?;
    Ok(contents.split_whitespace().map(str::to_string).collect())
}

fn write_authors(
    path: &Path,
    authors: &BTreeSet<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let mut file = fs::File::create(path)?;
    if !authors.is_empty() {
        let content = authors.iter().cloned().collect::<Vec<String>>().join(" ");
        writeln!(file, "{content}")?;
    }
    Ok(())
}