1use std::process::Command;
3use std::str::from_utf8;
4use tracing::{debug, trace, warn};
5
6#[derive(Debug, Clone, Default, clap::ValueEnum)]
8pub enum VersionLock {
9 #[default]
11 None,
12 Major,
14 Minor,
16}
17
18impl std::fmt::Display for VersionLock {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match self {
21 VersionLock::None => {
22 write!(f, "None")
23 }
24 VersionLock::Major => {
25 write!(f, "Major")
26 }
27 VersionLock::Minor => {
28 write!(f, "Minor")
29 }
30 }
31 }
32}
33
34#[derive(Debug, Clone, Default, clap::ValueEnum)]
36pub enum PreRelease {
37 Never,
39 #[default]
41 Auto,
42 Always,
44}
45
46impl std::fmt::Display for PreRelease {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 PreRelease::Never => {
50 write!(f, "Never")
51 }
52 PreRelease::Auto => {
53 write!(f, "Auto")
54 }
55 PreRelease::Always => {
56 write!(f, "Always")
57 }
58 }
59 }
60}
61
62#[derive(Debug, Default, clap::Parser)]
64pub struct DotnetOutdatedOptions {
65 #[clap(
67 short = 'i',
68 long = "include-auto-references",
69 help = "Include auto-referenced packages"
70 )]
71 include_auto_references: bool,
72 #[clap(
74 long = "pre-release",
75 value_name = "VALUE",
76 default_value = "auto",
77 help = "Should dotnet-outdated look for pre-release versions of packages",
78 value_enum
79 )]
80 pre_release: PreRelease,
81 #[clap(
83 long = "include",
84 value_name = "PACKAGE_NAME",
85 number_of_values = 1,
86 help = "Dependencies that should be included in the consideration"
87 )]
88 include: Vec<String>,
89 #[clap(
91 long = "exclude",
92 value_name = "PACKAGE_NAME",
93 number_of_values = 1,
94 help = "Dependencies that should be excluded from consideration"
95 )]
96 exclude: Vec<String>,
97 #[clap(
99 short = 't',
100 long = "transitive",
101 help = "Should dotnet-outdated consider transitiv dependencies"
102 )]
103 transitive: bool,
104 #[clap(
106 long = "transitive-depth",
107 value_name = "DEPTH",
108 default_value = "1",
109 requires = "transitive",
110 help = "If transitive dependencies are considered, to which depth in the dependency tree"
111 )]
112 transitive_depth: u64,
113 #[clap(
115 long = "version-lock",
116 value_name = "LOCK",
117 default_value = "none",
118 help = "Should we consider all updates or just minor versions and/or patch levels",
119 value_enum
120 )]
121 version_lock: VersionLock,
122 #[clap(
124 long = "input-dir",
125 value_name = "DIRECTORY",
126 help = "The input directory to pass to dotnet outdated"
127 )]
128 input_dir: Option<std::path::PathBuf>,
129}
130
131#[derive(Debug, serde::Serialize, serde::Deserialize)]
133#[serde(rename_all = "PascalCase")]
134pub struct DotnetOutdatedData {
135 pub projects: Vec<Project>,
137}
138
139#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
141#[serde(rename_all = "PascalCase")]
142pub struct Project {
143 pub name: String,
145 pub file_path: String,
147 pub target_frameworks: Vec<Framework>,
149}
150
151#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
153#[serde(rename_all = "PascalCase")]
154pub struct Framework {
155 pub name: String,
157 pub dependencies: Vec<Dependency>,
159}
160
161#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
163#[serde(rename_all = "PascalCase")]
164pub struct Dependency {
165 pub name: String,
167 pub resolved_version: String,
169 pub latest_version: String,
171 pub upgrade_severity: Severity,
173}
174
175#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
177pub enum Severity {
178 Major,
180 Minor,
182 Patch,
184}
185
186impl std::fmt::Display for Severity {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self {
189 Severity::Major => {
190 write!(f, "Major")
191 }
192 Severity::Minor => {
193 write!(f, "Minor")
194 }
195 Severity::Patch => {
196 write!(f, "Patch")
197 }
198 }
199 }
200}
201
202#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
204pub enum IndicatedUpdateRequirement {
205 UpToDate,
207 UpdateRequired,
209}
210
211impl std::fmt::Display for IndicatedUpdateRequirement {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 match self {
214 IndicatedUpdateRequirement::UpToDate => {
215 write!(f, "up-to-date")
216 }
217 IndicatedUpdateRequirement::UpdateRequired => {
218 write!(f, "update-required")
219 }
220 }
221 }
222}
223
224pub fn outdated(
226 options: &DotnetOutdatedOptions,
227) -> Result<(IndicatedUpdateRequirement, DotnetOutdatedData), crate::Error> {
228 let output_dir = tempfile::tempdir()?;
229 let output_file = output_dir.path().join("outdated.json");
230 let output_file = output_file
231 .to_str()
232 .ok_or(crate::Error::PathConversionError)?;
233
234 let mut cmd = Command::new("dotnet");
235
236 cmd.args([
237 "outdated",
238 "--fail-on-updates",
239 "--output",
240 output_file,
241 "--output-format",
242 "json",
243 ]);
244
245 if options.include_auto_references {
246 cmd.args(["--include-auto-references"]);
247 }
248
249 cmd.args(["--pre-release", &options.pre_release.to_string()]);
250
251 if !options.include.is_empty() {
252 for i in &options.include {
253 cmd.args(["--include", i]);
254 }
255 }
256
257 if !options.exclude.is_empty() {
258 for e in &options.exclude {
259 cmd.args(["--exclude", e]);
260 }
261 }
262
263 if options.transitive {
264 cmd.args([
265 "--transitive",
266 "--transitive-depth",
267 &options.transitive_depth.to_string(),
268 ]);
269 }
270
271 cmd.args(["--version-lock", &options.version_lock.to_string()]);
272
273 if let Some(ref input_dir) = options.input_dir {
274 cmd.args([&input_dir]);
275 }
276
277 let output = cmd.output()?;
278
279 if !output.status.success() {
280 warn!(
281 "dotnet outdated did not return with a successful exit code: {}",
282 output.status
283 );
284 debug!("stdout:\n{}", from_utf8(&output.stdout)?);
285 if !output.stderr.is_empty() {
286 warn!("stderr:\n{}", from_utf8(&output.stderr)?);
287 }
288 }
289
290 let update_requirement = if output.status.success() {
291 IndicatedUpdateRequirement::UpToDate
292 } else {
293 IndicatedUpdateRequirement::UpdateRequired
294 };
295
296 let output_file_content = std::fs::read_to_string(output_file)?;
297
298 trace!("Read output file content:\n{}", output_file_content);
299
300 let jd = &mut serde_json::Deserializer::from_str(&output_file_content);
301 let data: DotnetOutdatedData = serde_path_to_error::deserialize(jd)?;
302 Ok((update_requirement, data))
303}
304
305#[cfg(test)]
306mod test {
307 use super::*;
308 use crate::Error;
309
310 #[test]
313 fn test_run_dotnet_outdated() -> Result<(), Error> {
314 outdated(&Default::default())?;
315 Ok(())
316 }
317}