knope 0.22.0

A command line tool for automating common development tasks
use std::path::PathBuf;

use changesets::{UniqueId, Versioning};
use inquire::{
    MultiSelect, Select,
    list_option::ListOption,
    validator::{ErrorMessage, Validation},
};
use itertools::Itertools;
use knope_versioning::changes::{CHANGESET_DIR, ChangeType};
use miette::Diagnostic;
use tracing::info;

use crate::{
    fs, prompt,
    state::{RunType, State},
    step::releases::Package,
};

pub(crate) fn run(state: RunType<State>) -> Result<RunType<State>, Error> {
    let state = match state {
        RunType::DryRun(state) => {
            info!("Would create a new change file");
            return Ok(RunType::DryRun(state));
        }
        RunType::Real(state) => state,
    };

    let packages = if state.packages.len() == 1 {
        state.packages.iter().collect()
    } else {
        MultiSelect::new(
            "Which packages does this change affect?",
            state.packages.iter().collect(),
        )
        .with_validator(|res: &[ListOption<&&Package>]| {
            if res.is_empty() {
                Ok(Validation::Invalid(ErrorMessage::Custom(
                    "At least one package must be selected".to_string(),
                )))
            } else {
                Ok(Validation::Valid)
            }
        })
        .prompt()
        .map_err(prompt::Error::from)?
    };

    let versioning = packages
        .iter()
        .map(|package| {
            let package_name = package.name();
            let change_types = package
                .versioning
                .release_notes
                .sections
                .iter()
                .flat_map(|(_, sources)| sources.iter().filter_map(ChangeType::to_changeset_type))
                .collect_vec();
            let prompt = if let Some(package_name) = package_name.as_custom() {
                format!("What type of change is this for {package_name}?")
            } else {
                "What type of change is this?".to_string()
            };
            Select::new(&prompt, change_types)
                .prompt()
                .map_err(prompt::Error::from)
                .map_err(Error::from)
                .map(|change_type| (package_name.to_string(), change_type))
        })
        .collect::<Result<Versioning, Error>>()?;
    let summary = inquire::Text::new("What is a short summary of this change?")
        .with_help_message("This will be used as a header in the changelog")
        .prompt()
        .map_err(prompt::Error::from)?;
    let unique_id = UniqueId::normalize(&summary);
    let summary = format!("# {summary}");
    let change = changesets::Change {
        unique_id,
        versioning,
        summary,
    };

    let changeset_path = PathBuf::from(CHANGESET_DIR);
    if !changeset_path.exists() {
        fs::create_dir(RunType::Real(&changeset_path))?;
    }
    change
        .write_to_directory(&changeset_path)
        .map_err(|source| {
            let file_name = change.unique_id.to_file_name();
            fs::Error::Write {
                path: changeset_path.join(file_name),
                source,
            }
        })?;
    Ok(RunType::Real(state))
}

#[derive(Debug, Diagnostic, thiserror::Error)]
pub(crate) enum Error {
    #[error(transparent)]
    #[diagnostic(transparent)]
    Fs(#[from] fs::Error),
    #[error(transparent)]
    #[diagnostic(transparent)]
    Prompt(#[from] prompt::Error),
}