paru 1.11.0

Feature packed AUR helper
use crate::config::{Config, LocalRepos, Sign};
use crate::exec;
use crate::fmt::print_indent;
use crate::printtr;
use crate::util::ask;

use std::env::current_exe;
use std::ffi::OsStr;
use std::fs::read_link;
use std::path::{Path, PathBuf};
use std::process::Command;

use alpm::{AlpmListMut, Db};
use ansi_term::Style;
use anyhow::{Context, Result};
use nix::unistd::{self, User};
use tr::tr;
use unicode_width::UnicodeWidthStr;

pub fn add<P: AsRef<Path>, S: AsRef<OsStr>>(
    config: &Config,
    path: P,
    name: &str,
    pkgs: &[S],
) -> Result<()> {
    let db = path.as_ref().join(format!("{}.db", name));
    let name = if db.exists() {
        if pkgs.is_empty() {
            return Ok(());
        }
        read_link(db)?
    } else if !name.contains(".db.") {
        PathBuf::from(format!("{}.db.tar.gz", name))
    } else {
        PathBuf::from(name)
    };

    let path = path.as_ref();
    let file = path.join(&name);

    let user = unistd::getuid();
    let group = unistd::getgid();

    if !path.exists() {
        let mut cmd = Command::new(&config.sudo_bin);
        cmd.arg("install")
            .arg("-dm755")
            .arg("-o")
            .arg(user.to_string())
            .arg("-g")
            .arg(group.to_string())
            .arg(path);
        exec::command(&mut cmd)?;
    }

    let pkgs = pkgs
        .iter()
        .map(|p| path.join(Path::new(p.as_ref()).file_name().unwrap()))
        .collect::<Vec<_>>();

    let mut cmd = Command::new("repo-add");

    if !config.keep_repo_cache {
        cmd.arg("-R");
    }

    cmd.arg(file).args(pkgs);

    if config.sign_db != Sign::No {
        cmd.arg("-s");
        if let Sign::Key(ref k) = config.sign_db {
            cmd.arg("-k");
            cmd.arg(k);
        }
    }

    let err = exec::command(&mut cmd);

    let user = User::from_uid(user).unwrap().unwrap();

    if err.is_err() {
        eprintln!(
            "Could not add packages to repo:
    paru now expects local repos to be writable as your user:
    You should chown/chmod your repos to be writable by you:
    chown -R {}: {}",
            user.name,
            path.display()
        );
    }

    err
}

pub fn remove<P: AsRef<Path>, S: AsRef<OsStr>>(
    config: &Config,
    path: P,
    name: &str,
    pkgs: &[S],
) -> Result<()> {
    let path = path.as_ref();
    let db = path.join(format!("{}.db", name));
    if pkgs.is_empty() || !db.exists() {
        return Ok(());
    }

    let name = read_link(db)?;
    let file = path.join(&name);

    let mut cmd = Command::new("repo-remove");
    cmd.arg(file);

    if config.sign_db != Sign::No {
        cmd.arg("-s");
        if let Sign::Key(ref k) = config.sign_db {
            cmd.arg("-k");
            cmd.arg(k);
        }
    }

    cmd.args(pkgs);
    exec::command(&mut cmd)?;

    Ok(())
}

pub fn init<P: AsRef<Path>>(config: &Config, path: P, name: &str) -> Result<()> {
    let pkgs: &[&str] = &[];
    add(config, path, name, pkgs)
}

fn is_configured_local_db(config: &Config, db: &Db) -> bool {
    match config.repos {
        LocalRepos::None => false,
        LocalRepos::Default => is_local_db(db),
        LocalRepos::Repo(ref r) => is_local_db(db) && r.iter().any(|r| *r == db.name()),
    }
}

pub fn file<'a>(repo: &Db<'a>) -> Option<&'a str> {
    repo.servers()
        .first()
        .map(|s| s.trim_start_matches("file://"))
}

pub fn all_files(config: &Config) -> Vec<String> {
    config
        .alpm
        .syncdbs()
        .iter()
        .flat_map(|db| db.servers())
        .filter(|f| f.starts_with("file://"))
        .map(|s| s.trim_start_matches("file://").to_string())
        .collect()
}

fn is_local_db(db: &alpm::Db) -> bool {
    !db.servers().is_empty() && db.servers().iter().all(|s| s.starts_with("file://"))
}

pub fn repo_aur_dbs(config: &Config) -> (AlpmListMut<Db>, AlpmListMut<Db>) {
    let dbs = config.alpm.syncdbs();
    let mut aur = dbs.to_list_mut();
    let mut repo = dbs.to_list_mut();
    aur.retain(|db| is_configured_local_db(config, db));
    repo.retain(|db| !is_configured_local_db(config, db));
    (repo, aur)
}

pub fn refresh<S: AsRef<OsStr>>(config: &mut Config, repos: &[S]) -> Result<i32> {
    let exe = current_exe().context(tr!("failed to get current exe"))?;
    let c = config.color;
    if !nix::unistd::getuid().is_root() {
        let mut cmd = Command::new(&config.sudo_bin);

        cmd.arg(exe);

        if let Some(ref conf) = config.pacman_conf {
            cmd.arg("--config").arg(conf);
        }

        cmd.arg("--dbpath")
            .arg(config.alpm.dbpath())
            .arg("-Ly")
            .args(repos);

        let status = cmd.spawn()?.wait()?;

        return Ok(status.code().unwrap_or(1));
    }

    let mut dbs = config.alpm.syncdbs_mut().to_list_mut();
    dbs.retain(|db| is_local_db(db));

    if !repos.is_empty() {
        dbs.retain(|db| repos.iter().any(|r| r.as_ref() == db.name()));
    }

    println!(
        "{} {}",
        c.action.paint("::"),
        c.bold.paint(tr!("syncing local databases..."))
    );

    if !dbs.is_empty() {
        dbs.update(false)?;
    } else {
        printtr!("  nothing to do");
    }

    Ok(0)
}

pub fn clean(config: &mut Config) -> Result<i32> {
    let c = config.color;
    let (_, repos) = repo_aur_dbs(config);
    let repo_names = repos
        .iter()
        .map(|r| r.name().to_string())
        .collect::<Vec<_>>();
    drop(repos);
    refresh(config, &repo_names)?;
    let (_, repos) = repo_aur_dbs(config);
    let db = config.alpm.localdb();

    let mut rem = repos
        .iter()
        .map(|repo| {
            repo.pkgs()
                .iter()
                .filter(|pkg| db.pkg(pkg.name()).is_err())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    rem.retain(|r| !r.is_empty());
    drop(repos);

    if rem.is_empty() {
        printtr!("there is nothing to do");
        return Ok(0);
    }

    println!();
    let count = rem.iter().fold(0, |acc, r| acc + r.len());
    let fmt = format!("{} ({}) ", tr!("Packages"), count);
    let start = fmt.width();
    print!("{}", c.bold.paint(fmt));
    print_indent(
        Style::new(),
        start,
        4,
        config.cols,
        "  ",
        rem.iter().flat_map(|r| r).map(|p| p.name()),
    );

    println!();
    if !ask(config, &tr!("Proceed with removal?"), true) {
        return Ok(1);
    }

    for pkgs in rem {
        let repo = pkgs[0].db().unwrap();
        let path = file(&repo).unwrap();
        let pkgs = pkgs.iter().map(|p| p.name()).collect::<Vec<_>>();
        remove(config, path, repo.name(), &pkgs)?;
    }

    let (_, repos) = repo_aur_dbs(config);
    let repo_names = repos
        .iter()
        .map(|r| r.name().to_string())
        .collect::<Vec<_>>();
    drop(repos);
    refresh(config, &repo_names)?;

    Ok(0)
}