mol 0.3.0

Package versioning with ease
Documentation
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Context;
use async_trait::async_trait;
use clap::Parser;
use tokio::fs;

use mol_core::prelude::*;

use super::{ExecutableCommand, ExecutableContext};

#[derive(Parser, Debug)]
pub struct Version {
  #[clap(long)]
  pub no_build: bool,
  #[clap(long)]
  pub build_args: Vec<String>,
}

impl Version {
  async fn consume_changesets<V: VersionEditor>(
    changesets: &Changesets,
    package_graph: &PackageGraph<'_, V>,
  ) -> anyhow::Result<(Vec<PathBuf>, Bump<V>)> {
    let mut bump = Bump::default();
    let mut changeset_files_paths = Vec::new();

    let mut changeset_files = fs::read_dir(&changesets.directory).await.with_context(|| {
      format!(
        "Unable to read the changesets directory at {:?}",
        changesets.directory
      )
    })?;

    while let Some(changeset) = changeset_files.next_entry().await? {
      let changeset_path = changeset.path();

      if let Some(ext) = changeset_path.extension() {
        if ext == "md" {
          if Some(OsStr::new("README.md")) == changeset_path.file_name() {
            continue;
          }

          let raw_changeset = fs::read_to_string(&changeset_path)
            .await
            .with_context(|| format!("Unable to read the changeset at {:?}", changeset_path))?;

          bump.add(
            Changeset::<V>::parse(&raw_changeset)
              .with_context(|| format!("Unable to parse changeset at {:?}", changeset_path))?,
            package_graph,
          );

          changeset_files_paths.push(changeset_path);
        }
      }
    }

    Ok((changeset_files_paths, bump))
  }
}

#[async_trait]
impl<T, V> ExecutableCommand<T, V> for Version
where
  T: PackageManager + Send + Sync,
  V: VersionEditor + Send + Sync + 'static,
{
  async fn execute(
    &self,
    context: &ExecutableContext<T, V>,
    plugins: Arc<PluginManager>,
  ) -> anyhow::Result<()> {
    plugins.pre_command("version", &context.as_plugin())?;

    let package_graph = context.packages.as_package_graph();
    let (changeset_paths, bump) =
      Self::consume_changesets::<V>(&context.changesets, &package_graph).await?;

    if bump.is_empty() {
      println!(
        "Sorry but no changesets found in {:?}",
        context.changesets.directory
      );

      return Ok(());
    }

    let mut updated = HashMap::new();

    for package in package_graph.update_order() {
      if let Some(update) = bump.package(&package.name).version() {
        let next_version = update
          .apply(&package.version.value)
          .with_context(|| format!("Failed updating package {}", package.name))?;

        updated.insert(package.name.as_str(), next_version.clone());

        if context.dry_run {
          println!(
            "dry_run - version bump: {} -> {}",
            package.version.value, next_version
          );
        } else {
          context
            .package_manager
            .apply_version(&package.path, &next_version)
            .await?;
        }

        for (name, version, updated_version) in package
          .dependencies
          .iter()
          .filter(|(name, _)| updated.contains_key(name.as_str()))
          .filter(|(name, version)| !V::r#match(version, &updated[name.as_str()]))
          .map(|(name, version)| (name, version, &updated[name.as_str()]))
        {
          if context.dry_run {
            println!(
              "dry_run - dependecy version bump: {} {} -> {}",
              name,
              version,
              V::mask(version, updated_version)
            );
          } else {
            context
              .package_manager
              .apply_dependency_version(&package.path, name, V::mask(version, updated_version))
              .await?;
          }
        }

        if let Some(root_path) = package.path.parent() {
          let changelog_path = {
            let mut root_path = root_path.to_path_buf();
            root_path.push("CHANGELOG.md");
            root_path
          };

          Changelog::update_changelog(
            &changelog_path,
            next_version.into(),
            &bump.package(&package.name),
            context.dry_run,
          )
          .await
          .with_context(|| {
            format!(
              "Could not update the changelog for {} at {:?}",
              package.name, changelog_path
            )
          })?;
        }
      }
    }

    if !context.dry_run && !self.no_build {
      context
        .package_manager
        .run_build(&context.root_dir, self.build_args.clone())
        .await?;
    }

    for changeset_path in changeset_paths {
      if context.dry_run {
        println!("dry_run - delete: {:?}", changeset_path);
      } else {
        fs::remove_file(&changeset_path)
          .await
          .with_context(|| format!("Unable to remove the changeset at {:?}", changeset_path))?;
      }
    }

    plugins.post_command("version", &context.as_plugin())?;

    Ok(())
  }
}