huber 1.0.11

Huber, simplify GitHub package management
Documentation
use std::cmp::Ordering;
use std::collections::HashMap;
use std::sync::Arc;

use anyhow::anyhow;
use async_trait::async_trait;
use clap::{Args, ValueHint};
use log::{error, info, warn};
use maplit::hashmap;
use semver::{Version, VersionReq};
use simpledi_rs::di::{DIContainer, DIContainerTrait};
use tokio::task::JoinHandle;

use crate::cmd::get_default_stdlib;
use crate::cmd::{CommandTrait, PlatformStdLib};
use crate::error::HuberError::{PackageNotInstalled, PackageUnableToUpdate};
use crate::lock_huber_ops;
use crate::model::config::Config;
use crate::model::package::Package;
use crate::model::release::Release;
use crate::service::package::PackageService;
use crate::service::release::ReleaseService;
use crate::service::ItemOperationTrait;

#[derive(Args)]
pub struct UpdateArgs {
    #[arg(help = "Package name", num_args = 1, value_hint = ValueHint::Unknown)]
    name: Vec<String>,

    #[cfg(any(target_os = "linux", target_os = "windows"))]
    #[arg(
        help = "Prefer standard library (only for Linux or Windows)",
        long,
        num_args = 1,
        default_value_t = get_default_stdlib(),
        value_enum
    )]
    prefer_stdlib: PlatformStdLib,

    #[cfg(target_os = "macos")]
    #[arg(
        help = "Prefer standard library (only for Linux or Windows)",
        long,
        hide = true,
        num_args = 1,
        default_value_t = get_default_stdlib(),
        value_enum
    )]
    prefer_stdlib: PlatformStdLib,

    #[arg(
        help = "Dry run to show available updates",
        long,
        num_args = 0,
        value_hint = ValueHint::Unknown
    )]
    dryrun: bool,
}

#[async_trait]
impl CommandTrait for UpdateArgs {
    async fn run(&self, config: &Config, container: &DIContainer) -> anyhow::Result<()> {
        lock_huber_ops!(config);

        let release_service = Arc::new(container.get::<ReleaseService>().unwrap().clone());
        let pkg_service = Arc::new(container.get::<PackageService>().unwrap().clone());
        let config = Arc::new(config.clone());

        for name in self.name.iter() {
            if !release_service.has(name)? {
                return Err(anyhow!(PackageUnableToUpdate(anyhow!(
                    PackageNotInstalled(name.clone())
                ))));
            }
        }

        let installed_latest_pkg_releases = if self.name.is_empty() {
            get_installed_latest_pkg_releases(&release_service)?
        } else {
            get_installed_latest_pkg_releases(&release_service)?
                .into_iter()
                .filter(|(name, _)| self.name.contains(name))
                .collect()
        };

        let mut join_handles: Vec<JoinHandle<anyhow::Result<()>>> = vec![];
        for (name, installed_release) in installed_latest_pkg_releases {
            let release_service = release_service.clone();
            let pkg_service = pkg_service.clone();
            let config = config.clone();
            let dryrun = self.dryrun;
            let prefer_stdlib = self.prefer_stdlib;

            let handle: JoinHandle<_> = tokio::spawn(async move {
                info!(
                    "Checking for updates for {}. The latest installed version is {}",
                    name, installed_release.version
                );

                let pkg = pkg_service.get(&name)?;
                let new_release = release_service.get_latest(&pkg).await.inspect_err(|_| {
                    error!("Failed to get the latest release of package {}", name);
                })?;

                info!(
                    "Found the latest version of {}: {}",
                    name, new_release.version
                );
                if is_pkg_locked_for_release(&config, &pkg, &new_release.version) {
                    warn!(
                        "Package {} is locked to version {}. Skipping updating to {}",
                        pkg.name,
                        config.lock_pkg_versions.get(&pkg.name).unwrap(),
                        new_release.version
                    );
                    return Ok(());
                }

                if new_release.compare(&installed_release)? == Ordering::Greater {
                    info!(
                        "Updating package {} from {} to {}",
                        name, installed_release.version, new_release.version
                    );
                    update(
                        release_service,
                        dryrun,
                        &new_release,
                        &installed_release,
                        &prefer_stdlib,
                    )
                    .await?;
                    info!(
                        "Package {} updated to {} successfully",
                        name, new_release.version
                    );
                } else {
                    info!(
                    "Nothing to update, as the currently installed version ({}) is equal to or \
                higher than the found version ({})",
                    installed_release.version, new_release.version
                );
                }

                Ok(())
            });
            join_handles.push(handle);
        }

        for handle in join_handles.into_iter() {
            handle.await??;
        }

        Ok(())
    }
}

fn get_installed_latest_pkg_releases(
    release_service: &ReleaseService,
) -> anyhow::Result<HashMap<String, Release>> {
    let mut installed_latest_pkg_releases: HashMap<String, Release> = hashmap! {};

    for release in release_service.list()? {
        if let Some(existing_release) = installed_latest_pkg_releases.get(&release.name) {
            if release.compare(existing_release)? == Ordering::Greater {
                installed_latest_pkg_releases.insert(release.name.clone(), release);
            }
        } else {
            installed_latest_pkg_releases.insert(release.name.clone(), release);
        }
    }
    Ok(installed_latest_pkg_releases)
}

pub fn is_pkg_locked_for_release(
    config: &Config,
    pkg: &Package,
    new_release_version: &str,
) -> bool {
    if let Some(lock_version) = config.lock_pkg_versions.get(&pkg.name) {
        let lock_version = lock_version.trim_start_matches("v");
        let lock_version = VersionReq::parse(lock_version).unwrap();

        let r = Version::parse(new_release_version.trim_start_matches("v"));
        if r.is_err() {
            warn!(
                "Failed to parse the new release version {}. Skip locking check",
                new_release_version
            );
            return false;
        }

        return !lock_version.matches(&r.unwrap());
    }

    false
}

async fn update(
    release_service: Arc<ReleaseService>,
    dryrun: bool,
    new_release: &Release,
    installed_release: &Release,
    prefer_stdlib: &PlatformStdLib,
) -> anyhow::Result<()> {
    if dryrun {
        info!("Available update {} to {}", installed_release, new_release);
    } else {
        info!("Updating {} to {}", installed_release, new_release);
        release_service
            .update(&new_release.package, prefer_stdlib, true)
            .await?;
    }

    Ok(())
}