tbdflow 0.27.0

A CLI to streamline your Git workflow for Trunk-Based Development.
Documentation
use crate::config::Config;
use anyhow::Result;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};

#[derive(Debug, Clone)]
pub struct CommitWizardResult {
    pub r#type: String,
    pub scope: Option<String>,
    pub message: String,
    pub body: Option<String>,
    pub breaking: bool,
    pub breaking_description: Option<String>,
    pub tag: Option<String>,
    pub issue: Option<String>,
}

#[derive(Debug, Clone)]
pub struct BranchWizardResult {
    pub branch_type: String,
    pub name: String,
    pub issue: Option<String>,
    pub from_commit: Option<String>,
}

#[derive(Debug, Clone)]
pub struct CompleteWizardResult {
    pub branch_type: String,
    pub name: String,
}

#[derive(Debug, Clone)]
pub struct ChangeLogWizardResult {
    pub from: Option<String>,
    pub to: Option<String>,
    pub unreleased: bool,
}

pub fn run_commit_wizard(config: &Config) -> Result<CommitWizardResult> {
    let theme = ColorfulTheme::default();

    // Load commit types from config or use defaults
    let allowed_types = config
        .lint
        .as_ref()
        .and_then(|l| l.conventional_commit_type.as_ref())
        .and_then(|cct| cct.allowed_types.as_ref())
        .cloned()
        .unwrap_or_else(|| {
            vec![
                "feat".to_string(),
                "fix".to_string(),
                "chore".to_string(),
                "docs".to_string(),
                "style".to_string(),
                "refactor".to_string(),
                "perf".to_string(),
                "test".to_string(),
                "build".to_string(),
                "ci".to_string(),
                "revert".to_string(),
                "wip".to_string(),
            ]
        });

    let type_selection = Select::with_theme(&theme)
        .with_prompt("Select the type of change")
        .items(&allowed_types)
        .default(0)
        .interact()?;
    let r#type = allowed_types[type_selection].clone();

    // Helper function to convert empty strings from dialoguer to None
    fn to_option(s: String) -> Option<String> {
        if s.is_empty() {
            None
        } else {
            Some(s)
        }
    }

    let scope: Option<String> = to_option(
        Input::<String>::with_theme(&theme)
            .with_prompt("Enter the scope of this change (optional)")
            .allow_empty(true)
            .interact_text()?,
    );

    let message: String = Input::with_theme(&theme)
        .with_prompt("Write a short, imperative tense description of the change")
        .interact_text()?;

    let body: Option<String> = to_option(
        Input::<String>::with_theme(&theme)
            .with_prompt("Provide a longer description of the change (optional)")
            .allow_empty(true)
            .interact_text()?,
    );

    let breaking = Confirm::with_theme(&theme)
        .with_prompt("Is this a breaking change?")
        .default(false)
        .interact()?;

    let breaking_description: Option<String> = if breaking {
        Some(
            Input::<String>::with_theme(&theme)
                .with_prompt("Describe the breaking change")
                .interact_text()?,
        )
    } else {
        None
    };

    let issue: Option<String> = to_option(
        Input::<String>::with_theme(&theme)
            .with_prompt("Enter an issue reference (e.g., PROJ-123) (optional)")
            .allow_empty(true)
            .interact_text()?,
    );

    let tag: Option<String> = to_option(
        Input::<String>::with_theme(&theme)
            .with_prompt("Enter a tag for this commit (optional)")
            .allow_empty(true)
            .interact_text()?,
    );

    Ok(CommitWizardResult {
        r#type,
        scope,
        message,
        body,
        breaking,
        breaking_description,
        tag,
        issue,
    })
}

pub fn run_branch_wizard(config: &Config) -> Result<BranchWizardResult> {
    let theme = ColorfulTheme::default();

    // Load branch types from config
    let mut allowed_types: Vec<String> = config.branch_types.keys().cloned().collect();
    allowed_types.sort(); // Sort for consistent order

    let type_selection = Select::with_theme(&theme)
        .with_prompt("Select the type of branch")
        .items(&allowed_types)
        .default(0)
        .interact()?;
    let branch_type = allowed_types[type_selection].clone();

    let name: String = Input::with_theme(&theme)
        .with_prompt("Enter a short, descriptive name for the branch (use hyphens)")
        .interact_text()?;

    let issue: Option<String> = {
        let input: String = Input::<String>::with_theme(&theme)
            .with_prompt("Enter an issue reference to include in the branch name (optional)")
            .allow_empty(true)
            .interact_text()?;
        if input.is_empty() {
            None
        } else {
            Some(input)
        }
    };

    let from_commit: Option<String> = {
        let input: String = Input::<String>::with_theme(&theme)
            .with_prompt("Enter a commit hash on 'main' to branch from (optional)")
            .allow_empty(true)
            .interact_text()?;
        if input.is_empty() {
            None
        } else {
            Some(input)
        }
    };

    Ok(BranchWizardResult {
        branch_type,
        name,
        issue,
        from_commit,
    })
}

pub fn run_complete_wizard(config: &Config) -> Result<CompleteWizardResult> {
    let theme = ColorfulTheme::default();

    // Load branch types from config
    let mut allowed_types: Vec<String> = config.branch_types.keys().cloned().collect();
    allowed_types.sort(); // Sort for consistent order

    let type_selection = Select::with_theme(&theme)
        .with_prompt("Select the type of branch to complete")
        .items(&allowed_types)
        .default(0)
        .interact()?;
    let branch_type = allowed_types[type_selection].clone();

    let name: String = Input::with_theme(&theme)
        .with_prompt("Enter the name of the branch to complete")
        .interact_text()?;

    Ok(CompleteWizardResult { branch_type, name })
}

pub fn run_changelog_wizard() -> Result<ChangeLogWizardResult> {
    let theme = ColorfulTheme::default();

    let options = &[
        "Generate for unreleased changes (since the latest tag)",
        "Generate for a specific range of tags",
    ];

    let selection = Select::with_theme(&theme)
        .with_prompt("What changelog would you like to generate?")
        .items(options)
        .default(0)
        .interact()?;

    match selection {
        0 => Ok(ChangeLogWizardResult {
            from: None,
            to: None,
            unreleased: true,
        }),
        1 => {
            let from: String = Input::with_theme(&theme)
                .with_prompt("Enter the 'from' tag (e.g., v0.12.0)")
                .interact_text()?;
            let to: String = Input::with_theme(&theme)
                .with_prompt("Enter the 'to' tag (e.g., v0.13.0, optional)")
                .allow_empty(true)
                .interact_text()?;

            Ok(ChangeLogWizardResult {
                from: Some(from),
                to: if to.is_empty() { None } else { Some(to) },
                unreleased: false,
            })
        }
        _ => unreachable!(),
    }
}