cargo-changeset 0.0.2

A cargo subcommand for managing changesets
use std::path::Path;

use changeset_manifest::InitConfig;
use changeset_operations::operations::{
    InitInput, InitOperation, InitPlan, build_config_from_input,
};
use changeset_operations::providers::{FileSystemManifestWriter, FileSystemProjectProvider};
use changeset_operations::traits::{
    ChangelogSettingsInput, GitSettingsInput, ProjectContext, ProjectProvider, VersionSettingsInput,
};
use changeset_project::ProjectKind;

use crate::commands::InitArgs;
use crate::error::Result;
use crate::interaction::{
    TerminalInitInteractionProvider, confirm_proceed, is_terminal_interactive,
};

pub(crate) fn run(args: InitArgs, start_path: &Path) -> Result<()> {
    let project_provider = FileSystemProjectProvider::new();
    let manifest_writer = FileSystemManifestWriter::new();
    let interaction_provider = TerminalInitInteractionProvider::new();

    let project = project_provider.discover_project(start_path)?;
    let (root_config, _) = project_provider.load_configs(&project)?;

    let context = ProjectContext {
        is_single_package: project.kind == ProjectKind::SinglePackage,
    };

    let is_interactive = !args.no_interactive && is_terminal_interactive();

    let input = if args.defaults {
        build_init_input(&args, context)
    } else if is_interactive {
        build_init_input_interactive(&args, &interaction_provider, context)?
    } else {
        build_init_input(&args, context)
    };

    let config = build_config_from_input(&input, context);

    let changeset_dir_path = root_config.changeset_dir();
    let full_changeset_dir = project.root.join(changeset_dir_path);
    let dir_exists = full_changeset_dir.exists();
    let gitkeep_exists = full_changeset_dir.join(".gitkeep").exists();

    let metadata_section = match project.kind {
        ProjectKind::VirtualWorkspace | ProjectKind::WorkspaceWithRoot => {
            changeset_manifest::MetadataSection::Workspace
        }
        ProjectKind::SinglePackage => changeset_manifest::MetadataSection::Package,
    };

    let plan = InitPlan {
        changeset_dir: full_changeset_dir,
        dir_exists,
        gitkeep_exists,
        metadata_section,
        config,
    };

    print_summary(&plan);

    let skip_confirmation = args.defaults || args.no_interactive || !is_terminal_interactive();
    if !skip_confirmation && !confirm_proceed("Proceed with initialization?")? {
        println!("Aborted.");
        return Ok(());
    }

    let operation = InitOperation::new(project_provider)
        .with_manifest_writer(manifest_writer)
        .with_interaction_provider(interaction_provider);

    let output = operation.execute_plan(start_path, &plan)?;

    println!();
    if output.created_dir {
        println!(
            "Created changeset directory at '{}'",
            output.changeset_dir.display()
        );
    } else {
        println!(
            "Changeset directory already exists at '{}'",
            output.changeset_dir.display()
        );
    }

    if output.created_gitkeep {
        println!("Created .gitkeep file");
    }

    if output.wrote_config {
        if let Some(section) = output.config_location {
            println!("Wrote configuration to {section} in Cargo.toml");
        }
    }

    Ok(())
}

fn has_any_git_args(args: &InitArgs) -> bool {
    args.commit.is_some()
        || args.tags.is_some()
        || args.keep_changesets.is_some()
        || args.tag_format.is_some()
}

fn has_any_changelog_args(args: &InitArgs) -> bool {
    args.changelog.is_some() || args.comparison_links.is_some()
}

fn has_any_version_args(args: &InitArgs) -> bool {
    args.zero_version_behavior.is_some()
}

fn build_init_input_interactive(
    args: &InitArgs,
    provider: &TerminalInitInteractionProvider,
    context: ProjectContext,
) -> Result<InitInput> {
    use changeset_operations::traits::InitInteractionProvider;

    let git_config = if has_any_git_args(args) {
        Some(GitSettingsInput {
            commit: args.commit.unwrap_or(true),
            tags: args.tags.unwrap_or(true),
            keep_changesets: args.keep_changesets.unwrap_or(false),
            tag_format: args.tag_format.map(Into::into).unwrap_or_else(|| {
                if context.is_single_package {
                    changeset_manifest::TagFormat::VersionOnly
                } else {
                    changeset_manifest::TagFormat::CratePrefixed
                }
            }),
        })
    } else {
        provider.configure_git_settings(context)?
    };

    let changelog_config = if has_any_changelog_args(args) {
        Some(ChangelogSettingsInput {
            changelog: args.changelog.map(Into::into).unwrap_or_default(),
            comparison_links: args.comparison_links.map(Into::into).unwrap_or_default(),
        })
    } else {
        provider.configure_changelog_settings(context)?
    };

    let version_config = if has_any_version_args(args) {
        Some(VersionSettingsInput {
            zero_version_behavior: args
                .zero_version_behavior
                .map(Into::into)
                .unwrap_or_default(),
        })
    } else {
        provider.configure_version_settings()?
    };

    Ok(InitInput {
        defaults: false,
        git_config,
        changelog_config,
        version_config,
    })
}

fn print_summary(plan: &InitPlan) {
    println!();
    println!("=== Initialization Summary ===");
    println!();

    if plan.dir_exists {
        println!(
            "Directory: {} (already exists)",
            plan.changeset_dir.display()
        );
    } else {
        println!(
            "Directory: {} (will be created)",
            plan.changeset_dir.display()
        );
    }

    if !plan.gitkeep_exists {
        println!("  - .gitkeep file will be created");
    }

    if !plan.config.is_empty() {
        println!();
        println!("Configuration to be written to {}:", plan.metadata_section);
        print_config_summary(&plan.config);
    } else {
        println!();
        println!("No configuration will be written (using defaults).");
    }

    println!();
}

fn print_config_summary(config: &InitConfig) {
    if let Some(commit) = config.commit {
        println!("  commit = {commit}");
    }
    if let Some(tags) = config.tags {
        println!("  tags = {tags}");
    }
    if let Some(keep_changesets) = config.keep_changesets {
        println!("  keep_changesets = {keep_changesets}");
    }
    if let Some(ref tag_format) = config.tag_format {
        println!("  tag_format = \"{}\"", tag_format.as_str());
    }
    if let Some(ref changelog) = config.changelog {
        println!("  changelog = \"{}\"", changelog.as_str());
    }
    if let Some(ref comparison_links) = config.comparison_links {
        println!("  comparison_links = \"{}\"", comparison_links.as_str());
    }
    if let Some(ref zero_version_behavior) = config.zero_version_behavior {
        println!(
            "  zero_version_behavior = \"{}\"",
            zero_version_behavior.as_str()
        );
    }
}

fn build_init_input(args: &InitArgs, context: ProjectContext) -> InitInput {
    let git_config = if has_any_git_args(args) {
        Some(GitSettingsInput {
            commit: args.commit.unwrap_or(true),
            tags: args.tags.unwrap_or(true),
            keep_changesets: args.keep_changesets.unwrap_or(false),
            tag_format: args.tag_format.map(Into::into).unwrap_or_else(|| {
                if context.is_single_package {
                    changeset_manifest::TagFormat::VersionOnly
                } else {
                    changeset_manifest::TagFormat::CratePrefixed
                }
            }),
        })
    } else {
        None
    };

    let changelog_config = if has_any_changelog_args(args) {
        Some(ChangelogSettingsInput {
            changelog: args.changelog.map(Into::into).unwrap_or_default(),
            comparison_links: args.comparison_links.map(Into::into).unwrap_or_default(),
        })
    } else {
        None
    };

    let version_config = if has_any_version_args(args) {
        Some(VersionSettingsInput {
            zero_version_behavior: args
                .zero_version_behavior
                .map(Into::into)
                .unwrap_or_default(),
        })
    } else {
        None
    };

    InitInput {
        defaults: args.defaults,
        git_config,
        changelog_config,
        version_config,
    }
}