1#![deny(unknown_lints)]
2#![deny(renamed_and_removed_lints)]
3#![forbid(unsafe_code)]
4#![deny(deprecated)]
5#![forbid(private_interfaces)]
6#![forbid(private_bounds)]
7#![forbid(non_fmt_panics)]
8#![deny(unreachable_code)]
9#![deny(unreachable_patterns)]
10#![forbid(unused_doc_comments)]
11#![forbid(unused_must_use)]
12#![deny(while_true)]
13#![deny(unused_parens)]
14#![deny(redundant_semicolons)]
15#![deny(non_ascii_idents)]
16#![deny(confusable_idents)]
17#![warn(missing_docs)]
18#![warn(clippy::missing_docs_in_private_items)]
19#![warn(clippy::cargo_common_metadata)]
20#![warn(rustdoc::missing_crate_level_docs)]
21#![deny(rustdoc::broken_intra_doc_links)]
22#![warn(missing_debug_implementations)]
23#![doc = include_str!("../README.md")]
24
25use thiserror::Error;
26
27use std::process::Command;
28use std::str::from_utf8;
29use tracing::{debug, warn};
30
31#[derive(Debug, Error)]
33pub enum Error {
34 #[error("Error parsing JSON: {0}")]
37 SerdeJsonError(#[from] serde_json::Error),
38 #[error("Error interpreting program output as UTF-8: {0}")]
41 Utf8Error(#[from] std::str::Utf8Error),
42 #[error("I/O Error: {0}")]
44 StdIoError(#[from] std::io::Error),
45}
46
47#[derive(Debug, clap::Parser)]
49pub struct ComposerOutdatedOptions {
50 #[clap(
52 short = 'i',
53 long = "ignore",
54 value_name = "PACKAGE_NAME",
55 number_of_values = 1,
56 help = "Dependencies that should be ignored"
57 )]
58 ignored_packages: Vec<String>,
59}
60
61#[derive(Debug, serde::Serialize, serde::Deserialize)]
63pub struct ComposerOutdatedData {
64 pub locked: Vec<PackageStatus>,
67}
68
69#[derive(Debug, serde::Serialize, serde::Deserialize)]
71pub struct PackageStatus {
72 pub name: String,
74 pub version: String,
76 pub latest: String,
78 #[serde(rename = "latest-status")]
80 pub latest_status: UpdateRequirement,
81 pub description: String,
83 pub warning: Option<String>,
85}
86
87#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
89#[serde(rename_all = "kebab-case")]
90pub enum UpdateRequirement {
91 UpToDate,
93 SemverSafeUpdate,
95 UpdatePossible,
97}
98
99impl std::fmt::Display for UpdateRequirement {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 UpdateRequirement::UpToDate => {
103 write!(f, "up-to-date")
104 }
105 UpdateRequirement::SemverSafeUpdate => {
106 write!(f, "semver-safe-update")
107 }
108 UpdateRequirement::UpdatePossible => {
109 write!(f, "update-possible")
110 }
111 }
112 }
113}
114
115#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
117pub enum IndicatedUpdateRequirement {
118 UpToDate,
120 UpdateRequired,
122}
123
124impl std::fmt::Display for IndicatedUpdateRequirement {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 match self {
127 IndicatedUpdateRequirement::UpToDate => {
128 write!(f, "up-to-date")
129 }
130 IndicatedUpdateRequirement::UpdateRequired => {
131 write!(f, "update-required")
132 }
133 }
134 }
135}
136
137pub fn outdated(
139 options: &ComposerOutdatedOptions,
140) -> Result<(IndicatedUpdateRequirement, ComposerOutdatedData), Error> {
141 let mut cmd = Command::new("composer");
142
143 cmd.args([
144 "outdated",
145 "-f",
146 "json",
147 "--no-plugins",
148 "--strict",
149 "--locked",
150 "-m",
151 ]);
152
153 for package_name in &options.ignored_packages {
154 cmd.args(["--ignore", package_name]);
155 }
156
157 let output = cmd.output()?;
158
159 if !output.status.success() {
160 warn!(
161 "composer outdated did not return with a successful exit code: {}",
162 output.status
163 );
164 debug!("stdout:\n{}", from_utf8(&output.stdout)?);
165 if !output.stderr.is_empty() {
166 warn!("stderr:\n{}", from_utf8(&output.stderr)?);
167 }
168 }
169
170 let update_requirement = if output.status.success() {
171 IndicatedUpdateRequirement::UpToDate
172 } else {
173 IndicatedUpdateRequirement::UpdateRequired
174 };
175
176 let json_str = from_utf8(&output.stdout)?;
177 let data: ComposerOutdatedData = serde_json::from_str(json_str)?;
178 Ok((update_requirement, data))
179}
180
181#[cfg(test)]
182mod test {
183 use super::*;
184 #[test]
189 fn test_run_composer_outdated() -> Result<(), Error> {
190 outdated(&ComposerOutdatedOptions {
191 ignored_packages: vec![],
192 })?;
193 Ok(())
194 }
195}