depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Project module.

use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};

use crate::actions::DefinedAction;
use crate::cmd::{CatProjectArgs, CleanArgs};
use crate::entities::info::ShortName;
use crate::entities::runs::Runs;
use crate::entities::variables::Variable;
use crate::globals::DeployerGlobalConfig;
use crate::pipelines::DescribedPipeline;
use crate::{ARTIFACTS_DIR, CACHE_DIR, bmap};

const CURRENT_PROJECT_CONF_VERSION: u8 = 8;
/// Returns current project configuration version.
pub fn get_default_project_conf_version() -> u8 {
  CURRENT_PROJECT_CONF_VERSION
}

/// Project configuration.
#[derive(Deserialize, Serialize, PartialEq, Clone)]
pub struct DeployerProjectOptions {
  /// Project name.
  pub project_name: String,

  /// Configuration version
  #[serde(default = "get_default_project_conf_version")]
  pub version: u8,

  /// Ignore files and folders' relative paths.
  ///
  /// Files that never will be used in any circumstances (e.g., `.git` folder etc.).
  #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
  pub ignore_files: BTreeSet<PathBuf>,

  /// Cache files and folders' relative paths.
  ///
  /// Cache files are ignored by default when creating new build foler;
  /// but you can copy or symlink any cache files from project's folder
  /// by build options (see `crate::build::build` function).
  #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
  pub cache_files: BTreeSet<PathBuf>,

  /// Project variables.
  ///
  /// This is how you can change your shell commands on the fly.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub variables: BTreeMap<ShortName, Variable>,

  /// Project Actions.
  #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
  pub actions: BTreeSet<DefinedAction>,

  /// Project Pipelines.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub pipelines: Vec<DescribedPipeline>,
}

impl Default for DeployerProjectOptions {
  fn default() -> Self {
    Self {
      project_name: String::new(),
      ignore_files: Default::default(),
      cache_files: Default::default(),
      actions: Default::default(),
      pipelines: Default::default(),
      variables: Default::default(),
      version: CURRENT_PROJECT_CONF_VERSION,
    }
  }
}

impl DeployerProjectOptions {
  /// Collects variables for used action from project configuration.
  pub fn variables_for(&self, with: &BTreeMap<String, ShortName>) -> anyhow::Result<BTreeMap<String, Variable>> {
    let vars = {
      let mut replaced: BTreeMap<String, Variable> = bmap!();
      for (placeholder, short_name) in with.iter() {
        let variable = self
          .variables
          .iter()
          .find(|(k, _)| k.as_str().eq(short_name.as_str()))
          .ok_or(anyhow::anyhow!(
            "Can't find requested variable `{}`!",
            short_name.as_str()
          ))?
          .1;
        replaced.insert(placeholder.to_string(), variable.to_owned());
      }
      replaced
    };
    Ok(vars)
  }
}

/// Inits the project.
pub fn init_project(globals: &mut DeployerGlobalConfig, config: &mut DeployerProjectOptions) -> anyhow::Result<()> {
  let curr_dir = std::env::current_dir()
    .expect("Can't get current dir!")
    .to_str()
    .expect("Can't convert current dir's path to string!")
    .to_owned();
  if !globals.projects.contains(&curr_dir) {
    globals.projects.push(curr_dir.to_owned());
  }

  config.init_from_prompt(curr_dir)?;

  if !std::fs::exists(".depl")? && *config != Default::default() {
    std::fs::create_dir(".depl")?;
  }

  println!("Setup is completed. Don't forget to assign at least one pipeline to the project to run!");

  Ok(())
}

/// Prints project pipelines' list.
///
/// If you specify `full` option (`depl cat . -f`),
/// Deployer will print full project configuration in YAML format.
///
/// If you specify `cat_all_shell_commands` option (`depl cat . -n`),
/// Deployer will print all shell commands of all project pipelines.
pub fn cat_project(config: &DeployerProjectOptions, args: CatProjectArgs) -> anyhow::Result<()> {
  if args.full {
    let project_ser = serde_pretty_yaml::to_string_pretty(&config)?;
    println!("{project_ser}");
  } else if args.cat_all_shell_commands {
    for pipeline in config.pipelines.as_slice() {
      let cmds = pipeline.return_all_cmds(&config.actions)?;
      println!("Pipeline `{}`:", pipeline.title.blue().italic());
      for cmd in cmds {
        println!(">>> {}", cmd.green());
      }
    }
  } else {
    for pipeline in config.pipelines.as_slice() {
      println!("{}", pipeline.title);
    }
  }

  Ok(())
}

/// Edits the project.
pub async fn edit_project(
  globals: &mut DeployerGlobalConfig,
  config: &mut DeployerProjectOptions,
) -> anyhow::Result<()> {
  if *config == Default::default() {
    panic!("Config is invalid! Reinit the project.");
  }

  config.edit_from_prompt(globals).await?;

  if !std::fs::exists(".depl")? && *config != Default::default() {
    std::fs::create_dir(".depl")?;
  }

  Ok(())
}

/// Cleans all project runs.
///
/// You can also specify `include_artifacts` option (`deployer clean -i`)
/// to cleanup `artifacts` folder.
pub fn clean_runs(
  config: &DeployerProjectOptions,
  runs: &mut Runs,
  cache_dir: &Path,
  args: &CleanArgs,
) -> anyhow::Result<()> {
  use fs_extra::dir::get_size;

  let mut path = PathBuf::new();
  path.push(cache_dir);
  path.push(CACHE_DIR);

  let mut total: u64 = 0;

  if let Some(project_builds) = runs
    .projects
    .iter_mut()
    .find(|p| p.name.as_str().eq(config.project_name.as_str()))
  {
    if !args.preserve_least {
      for folder in project_builds.runs.iter().map(|b| b.folder.clone()) {
        total += get_size(&folder)?;
        let _ = std::fs::remove_dir_all(folder);
      }
      project_builds.runs.clear();
    } else {
      let mut hm = bmap!();
      for folder in project_builds.runs.iter().cloned() {
        hm.insert(folder.exclusive_tag.clone().unwrap_or(String::new()), folder);
      }
      for folder in project_builds
        .runs
        .iter()
        .filter(|b| !hm.contains_key(&b.exclusive_tag.clone().unwrap_or_default()))
        .map(|b| b.folder.clone())
      {
        total += get_size(&folder)?;
        let _ = std::fs::remove_dir_all(folder);
      }
      project_builds.runs.clear();
      for folder in hm.values() {
        project_builds.runs.push(folder.clone());
      }
    }
  }

  if args.include_artifacts {
    let curr_dir = std::env::current_dir()?;
    let artifacts_dir = curr_dir.join(ARTIFACTS_DIR);
    if artifacts_dir.as_path().exists() {
      total += get_size(&artifacts_dir)?;
      let _ = std::fs::remove_dir_all(artifacts_dir);
    }
  }

  println!("Cleaned: {}", format_size(total));

  Ok(())
}

/// Formats `u64` as file size (bytes).
fn format_size(size: u64) -> String {
  const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
  let mut size = size as f64;
  let mut unit_index = 0;

  while size >= 1024.0 && unit_index < UNITS.len() - 1 {
    size /= 1024.0;
    unit_index += 1;
  }

  if unit_index == 0 {
    format!("{} {}", size as u64, UNITS[unit_index])
  } else {
    format!("{:.1} {}", size, UNITS[unit_index])
  }
}