tracel-xtask 4.17.0

Reusable and Extensible xtask commands to manage repositories.
Documentation
use anyhow::{Ok, Result};
use strum::IntoEnumIterator;
use tracel_xtask_utils::{
    cargo::ensure_cargo_crate_is_installed,
    endgroup,
    environment::Environment,
    group,
    process::{run_process, run_process_for_package, run_process_for_workspace},
    prompt::ask_once,
    workspace::{WorkspaceMemberType, get_workspace_members},
};

use crate::{
    commands::WARN_IGNORED_EXCLUDE_AND_ONLY_ARGS, context::Context, versions::TYPOS_VERSION,
};

use super::Target;

#[tracel_xtask_macros::declare_command_args(Target, FixSubCommand)]
pub struct FixCmdArgs {}

pub fn handle_command(
    args: FixCmdArgs,
    _env: Environment,
    _ctx: Context,
    mut answer: Option<bool>,
) -> anyhow::Result<()> {
    answer = warning_prompt(answer, &args);
    if answer.unwrap() {
        match args.get_command() {
            FixSubCommand::Audit => run_audit(),
            FixSubCommand::Format => run_format(&args.target, &args.exclude, &args.only),
            FixSubCommand::Lint => run_lint(
                &args.target,
                &args.exclude,
                &args.only,
                &args.features,
                args.no_default_features,
            ),
            FixSubCommand::Typos => run_typos(),
            FixSubCommand::All => FixSubCommand::iter()
                .filter(|c| *c != FixSubCommand::All)
                .try_for_each(|c| {
                    handle_command(
                        FixCmdArgs {
                            command: Some(c),
                            target: args.target.clone(),
                            exclude: args.exclude.clone(),
                            only: args.only.clone(),
                            features: args.features.clone(),
                            no_default_features: args.no_default_features,
                            yes: args.yes,
                        },
                        _env.clone(),
                        _ctx.clone(),
                        answer,
                    )
                }),
        }
    } else {
        Ok(())
    }
}

pub fn warning_prompt(answer: Option<bool>, args: &FixCmdArgs) -> Option<bool> {
    if args.yes {
        return Some(true);
    }
    if answer.is_none() {
        if args.target == Target::Workspace && (!args.exclude.is_empty() || !args.only.is_empty()) {
            log::warn!("{WARN_IGNORED_EXCLUDE_AND_ONLY_ARGS}");
        }
        return Some(ask_once(
            "This will run the check with autofix mode enabled.",
        ));
    };
    answer
}

pub(crate) fn run_audit() -> anyhow::Result<()> {
    ensure_cargo_crate_is_installed("cargo-audit", Some("fix"), None, false)?;
    group!("Audit Rust Dependencies");
    run_process(
        "cargo",
        &["audit", "-q", "--color", "always", "fix"],
        None,
        None,
        "Audit check execution failed",
    )?;
    endgroup!();
    Ok(())
}

fn run_format(target: &Target, excluded: &Vec<String>, only: &Vec<String>) -> Result<()> {
    match target {
        Target::Workspace => {
            group!("Format Workspace");
            run_process_for_workspace(
                "cargo",
                &["fmt"],
                None,
                &[],
                None,
                None,
                "Workspace compilation failed",
                None,
                None,
            )?;
            endgroup!();
        }
        Target::Crates | Target::Examples => {
            let members = match target {
                Target::Crates => get_workspace_members(WorkspaceMemberType::Crate),
                Target::Examples => get_workspace_members(WorkspaceMemberType::Example),
                _ => unreachable!(),
            };
            for member in members {
                group!("Format: {}", member.name);
                run_process_for_package(
                    "cargo",
                    &member.name,
                    &["fmt", "-p", &member.name],
                    None,
                    excluded,
                    only,
                    &format!("Format check execution failed for {}", &member.name),
                    None,
                    None,
                )?;
                endgroup!();
            }
        }
        Target::AllPackages => {
            Target::iter()
                .filter(|t| *t != Target::AllPackages && *t != Target::Workspace)
                .try_for_each(|t| run_format(&t, excluded, only))?;
        }
    }
    Ok(())
}

fn run_lint(
    target: &Target,
    excluded: &Vec<String>,
    only: &Vec<String>,
    features: &[String],
    no_default_features: bool,
) -> anyhow::Result<()> {
    match target {
        Target::Workspace => {
            group!("Lint Workspace");

            let mut cmd_args = vec![
                "clippy",
                "--no-deps",
                "--fix",
                "--allow-dirty",
                "--allow-staged",
                "--allow-no-vcs",
                "--color=always",
            ];

            if no_default_features {
                cmd_args.push("--no-default-features");
            }

            let features_str = features.join(",");
            if !features.is_empty() {
                cmd_args.push("--features");
                cmd_args.push(&features_str);
            }

            cmd_args.extend(&["--", "--deny", "warnings"]);

            run_process_for_workspace(
                "cargo",
                &cmd_args,
                None,
                &[],
                None,
                None,
                "Workspace lint failed",
                None,
                None,
            )?;
            endgroup!();
        }
        Target::Crates | Target::Examples => {
            let members = match target {
                Target::Crates => get_workspace_members(WorkspaceMemberType::Crate),
                Target::Examples => get_workspace_members(WorkspaceMemberType::Example),
                _ => unreachable!(),
            };
            for member in members {
                group!("Lint: {}", member.name);

                let mut cmd_args = vec![
                    "clippy",
                    "--no-deps",
                    "--fix",
                    "--allow-dirty",
                    "--allow-staged",
                    "--color=always",
                    "-p",
                    &member.name,
                ];

                if no_default_features {
                    cmd_args.push("--no-default-features");
                }

                let features_str = features.join(",");
                if !features.is_empty() {
                    cmd_args.push("--features");
                    cmd_args.push(&features_str);
                }

                cmd_args.extend(&["--", "--deny", "warnings"]);

                run_process_for_package(
                    "cargo",
                    &member.name,
                    &cmd_args,
                    None,
                    excluded,
                    only,
                    &format!("Lint fix execution failed for {}", &member.name),
                    None,
                    None,
                )?;
                endgroup!();
            }
        }
        Target::AllPackages => {
            Target::iter()
                .filter(|t| *t != Target::AllPackages && *t != Target::Workspace)
                .try_for_each(|t| run_lint(&t, excluded, only, features, no_default_features))?;
        }
    }
    Ok(())
}

pub(crate) fn run_typos() -> anyhow::Result<()> {
    ensure_cargo_crate_is_installed("typos-cli", None, Some(TYPOS_VERSION), false)?;
    group!("Typos");
    run_process(
        "typos",
        &["--write-changes", "--color", "always"],
        None,
        None,
        "Some typos have been found and cannot be fixed.",
    )?;
    endgroup!();
    Ok(())
}