#![deny(unknown_lints)]
#![deny(renamed_and_removed_lints)]
#![forbid(unsafe_code)]
#![deny(deprecated)]
#![forbid(private_interfaces)]
#![forbid(private_bounds)]
#![forbid(non_fmt_panics)]
#![deny(unreachable_code)]
#![deny(unreachable_patterns)]
#![forbid(unused_doc_comments)]
#![forbid(unused_must_use)]
#![deny(while_true)]
#![deny(unused_parens)]
#![deny(redundant_semicolons)]
#![deny(non_ascii_idents)]
#![deny(confusable_idents)]
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]
#![warn(clippy::cargo_common_metadata)]
#![warn(rustdoc::missing_crate_level_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![warn(missing_debug_implementations)]
#![doc = include_str!("../README.md")]
use thiserror::Error;
use std::process::Command;
use std::str::from_utf8;
use tracing::{debug, warn};
#[derive(Debug, Error)]
pub enum Error {
#[error("Error parsing JSON: {0}")]
SerdeJsonError(#[from] serde_json::Error),
#[error("Error interpreting program output as UTF-8: {0}")]
Utf8Error(#[from] std::str::Utf8Error),
#[error("I/O Error: {0}")]
StdIoError(#[from] std::io::Error),
}
#[derive(Debug, clap::Parser)]
pub struct ComposerOutdatedOptions {
#[clap(
short = 'i',
long = "ignore",
value_name = "PACKAGE_NAME",
number_of_values = 1,
help = "Dependencies that should be ignored"
)]
ignored_packages: Vec<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ComposerOutdatedData {
pub locked: Vec<PackageStatus>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct PackageStatus {
pub name: String,
pub version: String,
pub latest: String,
#[serde(rename = "latest-status")]
pub latest_status: UpdateRequirement,
pub description: String,
pub warning: Option<String>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum UpdateRequirement {
UpToDate,
SemverSafeUpdate,
UpdatePossible,
}
impl std::fmt::Display for UpdateRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UpdateRequirement::UpToDate => {
write!(f, "up-to-date")
}
UpdateRequirement::SemverSafeUpdate => {
write!(f, "semver-safe-update")
}
UpdateRequirement::UpdatePossible => {
write!(f, "update-possible")
}
}
}
}
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum IndicatedUpdateRequirement {
UpToDate,
UpdateRequired,
}
impl std::fmt::Display for IndicatedUpdateRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndicatedUpdateRequirement::UpToDate => {
write!(f, "up-to-date")
}
IndicatedUpdateRequirement::UpdateRequired => {
write!(f, "update-required")
}
}
}
}
pub fn outdated(
options: &ComposerOutdatedOptions,
) -> Result<(IndicatedUpdateRequirement, ComposerOutdatedData), Error> {
let mut cmd = Command::new("composer");
cmd.args([
"outdated",
"-f",
"json",
"--no-plugins",
"--strict",
"--locked",
"-m",
]);
for package_name in &options.ignored_packages {
cmd.args(["--ignore", package_name]);
}
let output = cmd.output()?;
if !output.status.success() {
warn!(
"composer outdated did not return with a successful exit code: {}",
output.status
);
debug!("stdout:\n{}", from_utf8(&output.stdout)?);
if !output.stderr.is_empty() {
warn!("stderr:\n{}", from_utf8(&output.stderr)?);
}
}
let update_requirement = if output.status.success() {
IndicatedUpdateRequirement::UpToDate
} else {
IndicatedUpdateRequirement::UpdateRequired
};
let json_str = from_utf8(&output.stdout)?;
let data: ComposerOutdatedData = serde_json::from_str(json_str)?;
Ok((update_requirement, data))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_run_composer_outdated() -> Result<(), Error> {
outdated(&ComposerOutdatedOptions {
ignored_packages: vec![],
})?;
Ok(())
}
}