use std::process::Command;
use std::str::from_utf8;
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, Default, clap::ValueEnum)]
pub enum VersionLock {
#[default]
None,
Major,
Minor,
}
impl std::fmt::Display for VersionLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionLock::None => {
write!(f, "None")
}
VersionLock::Major => {
write!(f, "Major")
}
VersionLock::Minor => {
write!(f, "Minor")
}
}
}
}
#[derive(Debug, Clone, Default, clap::ValueEnum)]
pub enum PreRelease {
Never,
#[default]
Auto,
Always,
}
impl std::fmt::Display for PreRelease {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PreRelease::Never => {
write!(f, "Never")
}
PreRelease::Auto => {
write!(f, "Auto")
}
PreRelease::Always => {
write!(f, "Always")
}
}
}
}
#[derive(Debug, Default, clap::Parser)]
pub struct DotnetOutdatedOptions {
#[clap(
short = 'i',
long = "include-auto-references",
help = "Include auto-referenced packages"
)]
include_auto_references: bool,
#[clap(
long = "pre-release",
value_name = "VALUE",
default_value = "auto",
help = "Should dotnet-outdated look for pre-release versions of packages",
value_enum
)]
pre_release: PreRelease,
#[clap(
long = "include",
value_name = "PACKAGE_NAME",
number_of_values = 1,
help = "Dependencies that should be included in the consideration"
)]
include: Vec<String>,
#[clap(
long = "exclude",
value_name = "PACKAGE_NAME",
number_of_values = 1,
help = "Dependencies that should be excluded from consideration"
)]
exclude: Vec<String>,
#[clap(
short = 't',
long = "transitive",
help = "Should dotnet-outdated consider transitiv dependencies"
)]
transitive: bool,
#[clap(
long = "transitive-depth",
value_name = "DEPTH",
default_value = "1",
requires = "transitive",
help = "If transitive dependencies are considered, to which depth in the dependency tree"
)]
transitive_depth: u64,
#[clap(
long = "version-lock",
value_name = "LOCK",
default_value = "none",
help = "Should we consider all updates or just minor versions and/or patch levels",
value_enum
)]
version_lock: VersionLock,
#[clap(
long = "input-dir",
value_name = "DIRECTORY",
help = "The input directory to pass to dotnet outdated"
)]
input_dir: Option<std::path::PathBuf>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct DotnetOutdatedData {
pub projects: Vec<Project>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Project {
pub name: String,
pub file_path: String,
pub target_frameworks: Vec<Framework>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Framework {
pub name: String,
pub dependencies: Vec<Dependency>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Dependency {
pub name: String,
pub resolved_version: String,
pub latest_version: String,
pub upgrade_severity: Severity,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Severity {
Major,
Minor,
Patch,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Major => {
write!(f, "Major")
}
Severity::Minor => {
write!(f, "Minor")
}
Severity::Patch => {
write!(f, "Patch")
}
}
}
}
#[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: &DotnetOutdatedOptions,
) -> Result<(IndicatedUpdateRequirement, DotnetOutdatedData), crate::Error> {
let output_dir = tempfile::tempdir()?;
let output_file = output_dir.path().join("outdated.json");
let output_file = output_file
.to_str()
.ok_or(crate::Error::PathConversionError)?;
let mut cmd = Command::new("dotnet");
cmd.args([
"outdated",
"--fail-on-updates",
"--output",
output_file,
"--output-format",
"json",
]);
if options.include_auto_references {
cmd.args(["--include-auto-references"]);
}
cmd.args(["--pre-release", &options.pre_release.to_string()]);
if !options.include.is_empty() {
for i in &options.include {
cmd.args(["--include", i]);
}
}
if !options.exclude.is_empty() {
for e in &options.exclude {
cmd.args(["--exclude", e]);
}
}
if options.transitive {
cmd.args([
"--transitive",
"--transitive-depth",
&options.transitive_depth.to_string(),
]);
}
cmd.args(["--version-lock", &options.version_lock.to_string()]);
if let Some(ref input_dir) = options.input_dir {
cmd.args([&input_dir]);
}
let output = cmd.output()?;
if !output.status.success() {
warn!(
"dotnet 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 output_file_content = std::fs::read_to_string(output_file)?;
trace!("Read output file content:\n{}", output_file_content);
let jd = &mut serde_json::Deserializer::from_str(&output_file_content);
let data: DotnetOutdatedData = serde_path_to_error::deserialize(jd)?;
Ok((update_requirement, data))
}
#[cfg(test)]
mod test {
use super::*;
use crate::Error;
#[test]
fn test_run_dotnet_outdated() -> Result<(), Error> {
outdated(&Default::default())?;
Ok(())
}
}