paru 1.11.0

Feature packed AUR helper
use crate::config::{Config, Mode};

use crate::exec;
use crate::print_error;
use crate::printtr;
use crate::util::ask;

use std::fs::{read_dir, remove_dir_all, remove_file, set_permissions, DirEntry};

use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::Command;

use alpm_utils::DbListExt;
use anyhow::{bail, Context, Result};
use srcinfo::Srcinfo;
use tr::tr;

pub fn clean(config: &Config) -> Result<()> {
    if config.mode != Mode::Aur {
        exec::pacman(config, &config.args)?;
    }

    if config.mode != Mode::Repo {
        let rm = config.delete >= 1;
        let remove_all = config.clean >= 2;
        let clean_method = &config.pacman.clean_method;
        let keep_installed = clean_method.iter().any(|a| a == "KeepInstalled");
        let keep_current = clean_method.iter().any(|a| a == "KeepCurrent");

        let question = if remove_all {
            tr!("Do you want to clean ALL AUR packages from cache?")
        } else {
            tr!("Do you want to clean all other AUR packages from cache?")
        };

        if config.mode == Mode::Any {
            println!();
        }

        printtr!("Clone Directory: {}", config.fetch.clone_dir.display());

        if ask(config, &question, !remove_all) {
            clean_aur(config, keep_installed, keep_current, remove_all, rm)?;
        }

        printtr!("\nDiff Directory: {}", config.fetch.diff_dir.display());

        let question = tr!("Do you want to remove all saved diffs?");
        if ask(config, &question, true) {
            clean_diff(config)?;
        }
    }

    Ok(())
}

fn clean_diff(config: &Config) -> Result<()> {
    if !config.fetch.diff_dir.exists() {
        return Ok(());
    }

    let diffs = read_dir(&config.fetch.diff_dir).with_context(|| {
        tr!(
            "can't open diff dir: {}",
            config.fetch.diff_dir.display().to_string()
        )
    })?;

    for diff in diffs {
        let diff = diff?;

        if !diff.file_type()?.is_dir() && diff.path().extension().map(|s| s == "diff") == Some(true)
        {
            remove_file(diff.path())
                .with_context(|| tr!("could not remove '{}'", diff.path().display().to_string()))?;
        }
    }

    Ok(())
}

fn clean_aur(
    config: &Config,
    keep_installed: bool,
    keep_current: bool,
    remove_all: bool,
    rm: bool,
) -> Result<()> {
    if !config.fetch.clone_dir.exists() {
        return Ok(());
    }

    let cached_pkgs = read_dir(&config.fetch.clone_dir)
        .with_context(|| tr!("can't open clone dir: {}", config.fetch.clone_dir.display()))?;

    for file in cached_pkgs {
        if let Err(err) = clean_aur_pkg(config, file, remove_all, keep_installed, keep_current, rm)
        {
            print_error(config.color.error, err);
            continue;
        }
    }

    Ok(())
}

fn fix_perms(file: &Path) -> Result<()> {
    let pkg = file.join("pkg");
    let mut perms = pkg.metadata()?.permissions();
    perms.set_mode(0o755);
    set_permissions(pkg, perms)?;
    Ok(())
}

fn clean_aur_pkg(
    config: &Config,
    file: std::io::Result<DirEntry>,
    remove_all: bool,
    keep_installed: bool,
    keep_current: bool,
    rm: bool,
) -> Result<()> {
    let file = file?;

    if !file.file_type()?.is_dir()
        || !file.path().join(".git").exists()
        || !file.path().join(".SRCINFO").exists()
    {
        return Ok(());
    }

    let _ = fix_perms(&file.path());

    if remove_all {
        return do_remove(config, &file.path(), rm);
    }

    let srcinfo = Srcinfo::parse_file(file.path().join(".SRCINFO"))?;

    if config.clean == 1 {
        if keep_installed {
            let local_db = config.alpm.localdb();
            for pkg in &srcinfo.pkgs {
                if let Ok(pkg) = local_db.pkg(&*pkg.pkgname) {
                    if pkg.version().as_str() == srcinfo.version() {
                        return Ok(());
                    }
                }
            }
        }

        if keep_current {
            for pkg in &srcinfo.pkgs {
                let sync_dbs = config.alpm.syncdbs();
                if let Ok(pkg) = sync_dbs.pkg(&*pkg.pkgname) {
                    if pkg.version().as_str() == srcinfo.version() {
                        return Ok(());
                    }
                }
            }
        }
    }

    do_remove(
        config,
        &config.fetch.clone_dir.join(srcinfo.base.pkgbase),
        rm,
    )
}

fn do_remove(config: &Config, path: &Path, rm: bool) -> Result<()> {
    if rm {
        remove_dir_all(path)
            .with_context(|| tr!("could not remove '{}'", path.display().to_string()))
    } else {
        clean_untracked(config, path)
    }
}

pub fn clean_untracked(config: &Config, path: &Path) -> Result<()> {
    let output = Command::new(&config.git_bin)
        .args(&config.git_flags)
        .current_dir(path)
        .args(&["reset", "--hard", "HEAD"])
        .output()
        .with_context(|| {
            format!(
                "{} {} reset --hard HEAD",
                config.git_bin,
                config.git_flags.join(" "),
            )
        })?;

    if !output.status.success() {
        bail!(
            "{} {} reset --hard HEAD: {}",
            config.git_bin,
            config.git_flags.join(" "),
            String::from_utf8_lossy(&output.stderr)
        )
    }

    let output = Command::new(&config.git_bin)
        .args(&config.git_flags)
        .current_dir(path)
        .arg("clean")
        .arg("-fx")
        .output()
        .with_context(|| {
            format!(
                "{} {} clean -fx",
                config.git_bin,
                config.git_flags.join(" "),
            )
        })?;

    if !output.status.success() {
        bail!(
            "{} {} clean -fx: {}",
            config.git_bin,
            config.git_flags.join(" "),
            String::from_utf8_lossy(&output.stderr)
        )
    }

    Ok(())
}