cargo-upgrades 3.0.0

Checks if dependencies in Cargo.toml are up to date. Compatible with workspaces and path dependencies.
Documentation
use std::error::Error as _;
use std::io::Write;
use std::process::ExitCode;

use cargo_upgrades::{Error, UpgradesChecker, Workspace};
use yansi::{Color::Red, Condition, Paint, Style};

pub const COLOR: Condition = Condition::from(|| {
    (Condition::stdout_is_tty() || Condition::clicolor()) && Condition::no_color()
});

pub const ERROR: Style = Red.bold();

mod flags {
    pub const HELP: &str = CargoUpgrades::HELP_;

    xflags::xflags! {
        /// https://gitlab.com/kornelski/cargo-upgrades
        cmd cargo-upgrades {
            /// Suggest upgrades from stable to pre-release (alpha, beta) versions
            optional --pre
            /// Use a simple tab-separated text format
            optional --plumbing
            /// Check this Cargo project instead of the current dir (e.g. foo/Cargo.toml)
            optional --manifest-path path: String
        }
    }
}

#[cold]
fn fail(e: Error) -> ExitCode {
    print_error(e, "");
    ExitCode::FAILURE
}

fn run() -> Result<ExitCode, ExitCode> {
    if !COLOR() {
        yansi::disable();
    }

    let args = std::env::args_os().skip(1).skip_while(|arg| arg == "upgrades").collect();
    let flags = flags::CargoUpgrades::from_vec(args).map_err(|e| {
        if e.is_help() {
            println!("{e}");
            ExitCode::SUCCESS
        } else {
            eprint!("{}: {e}\n\n{}", "error".paint(ERROR), flags::HELP);
            ExitCode::FAILURE
        }
    })?;

    if flags.plumbing {
        yansi::disable();
    }

    let u = std::thread::spawn(UpgradesChecker::new);
    let workspace = Workspace::new(flags.manifest_path.as_deref()).map_err(fail)?;
    let u = u.join().unwrap().map_err(fail)?;

    let mut printed_anything = false;
    for (package, deps) in u.outdated_dependencies(&workspace, flags.pre) {
        if flags.plumbing {
            let cwd = std::env::current_dir().unwrap_or_default();
            for (dep, res) in deps {
                printed_anything = true;
                match res {
                    Ok(m) => println!(
                        "{}\t{}@{}\t{}",
                        package.manifest_path.strip_prefix(&cwd).unwrap_or(&package.manifest_path),
                        dep.name,
                        m.matches.map(|v| v.to_string()).as_deref().unwrap_or("*"),
                        m.latest
                    ),
                    Err(e) => {
                        print_error(e, &dep.name);
                    },
                }
            }
            continue;
        }

        if printed_anything {
            println!();
        }
        println!("{}\n{}", package.name.bold(), package.manifest_path.dim());
        let mut stdout = std::io::stdout().lock();
        let mut tw = tabwriter::TabWriter::new(&mut stdout).ansi(yansi::is_enabled());
        let mut header_printed = false;
        for (dep, res) in deps {
            match res {
                Ok(m) => {
                    if !header_printed {
                        let _ = writeln!(&mut tw, "{}\t{}\t{}", "dependency".underline(), "current".underline(), "upgrade".underline());
                        header_printed = true;
                    }
                    let _ = writeln!(&mut tw, "{}\t{}\t{}",
                        dep.rename.as_deref().unwrap_or(&dep.name),
                        m.matches.map(|s| s.to_string()).as_deref().unwrap_or("nothing").yellow(),
                        m.latest.green()
                    );
                },
                Err(e) => {
                    print_error(e, dep.rename.as_deref().unwrap_or(&dep.name));
                },
            }
        }

        let _ = tw.flush();
        printed_anything = true;
    }

    if printed_anything {
        return Ok(ExitCode::from(7));
    }

    if flags.plumbing {
        print!("# ");
    }
    println!("{}", "All dependencies match up-to-date releases.".green());
    if !flags.plumbing {
        println!("Run `cargo update` to update to latest release.");
    }
    Ok(ExitCode::SUCCESS)
}

fn main() -> ExitCode {
    run().unwrap_or_else(|e| e)
}

fn print_error(error: Error, context: &str) {
    eprintln!("{}: {context}{}{error}", "error".paint(ERROR), if !context.is_empty() {": "} else {""});
    let mut source = error.source();
    while let Some(error) = source {
        eprintln!("  {error}");
        source = error.source();
    }
}