homeboy 0.76.0

CLI for multi-component deployment and development workflow automation
Documentation
use crate::component::{CommandScopeConfig, Component};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeCommand {
    Audit,
    Lint,
    Test,
    Refactor,
    Deploy,
    Release,
    Fleet,
}

#[derive(Debug, Clone, Default)]
pub struct EffectiveScope {
    pub include: Vec<String>,
    pub exclude: Vec<String>,
}

pub fn resolve_component_scope(component: &Component, command: ScopeCommand) -> EffectiveScope {
    let mut effective = builtin_scope_defaults(component, command);

    let Some(scopes) = component.scopes.as_ref() else {
        dedupe(&mut effective.include);
        dedupe(&mut effective.exclude);
        return effective;
    };

    if let Some(defaults) = scopes.defaults.as_ref() {
        merge_scope(&mut effective, defaults);
    }

    let command_scope = match command {
        ScopeCommand::Audit => scopes.audit.as_ref(),
        ScopeCommand::Lint => scopes.lint.as_ref(),
        ScopeCommand::Test => scopes.test.as_ref(),
        ScopeCommand::Refactor => scopes.refactor.as_ref(),
        ScopeCommand::Deploy => scopes.deploy.as_ref(),
        ScopeCommand::Release => scopes.release.as_ref(),
        ScopeCommand::Fleet => scopes.fleet.as_ref(),
    };

    if let Some(scope) = command_scope {
        merge_scope(&mut effective, scope);
    }

    dedupe(&mut effective.include);
    dedupe(&mut effective.exclude);
    effective
}

fn builtin_scope_defaults(component: &Component, command: ScopeCommand) -> EffectiveScope {
    let mut effective = EffectiveScope::default();

    if matches!(command, ScopeCommand::Audit) {
        if let Some(target) = component.changelog_target.as_ref() {
            effective.exclude.push(target.clone());
        }
    }

    effective
}
fn merge_scope(target: &mut EffectiveScope, scope: &CommandScopeConfig) {
    target.include.extend(scope.include.iter().cloned());
    target.exclude.extend(scope.exclude.iter().cloned());
}

fn dedupe(items: &mut Vec<String>) {
    items.sort();
    items.dedup();
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::component::{Component, ScopeConfig};

    #[test]
    fn merges_default_and_command_specific_excludes() {
        let mut component = Component::new(
            "homeboy".to_string(),
            "/tmp/homeboy".to_string(),
            "".to_string(),
            None,
        );
        component.scopes = Some(ScopeConfig {
            defaults: Some(CommandScopeConfig {
                include: vec![],
                exclude: vec!["tmp/**".to_string()],
            }),
            audit: Some(CommandScopeConfig {
                include: vec![],
                exclude: vec!["docs/CHANGES.md".to_string()],
            }),
            ..Default::default()
        });
        component.changelog_target = Some("docs/CHANGELOG.md".to_string());

        let resolved = resolve_component_scope(&component, ScopeCommand::Audit);
        assert_eq!(
            resolved.exclude,
            vec!["docs/CHANGELOG.md", "docs/CHANGES.md", "tmp/**"]
        );
    }

    #[test]
    fn audit_scope_has_no_builtin_changelog_without_component_target() {
        let component = Component::new(
            "generic".to_string(),
            "/tmp/generic".to_string(),
            "".to_string(),
            None,
        );

        let resolved = resolve_component_scope(&component, ScopeCommand::Audit);
        assert!(resolved.exclude.is_empty());
    }

    #[test]
    fn audit_scope_includes_component_changelog_target() {
        let mut component = Component::new(
            "generic".to_string(),
            "/tmp/generic".to_string(),
            "".to_string(),
            None,
        );
        component.changelog_target = Some("docs/CHANGES.md".to_string());

        let resolved = resolve_component_scope(&component, ScopeCommand::Audit);
        assert_eq!(resolved.exclude, vec!["docs/CHANGES.md"]);
    }
}