Skip to main content

mars_agents/cli/
outdated.rs

1//! `mars outdated` — show available updates without applying.
2
3use serde::Serialize;
4
5use crate::error::MarsError;
6
7use super::output;
8
9/// Arguments for `mars outdated`.
10#[derive(Debug, clap::Args)]
11pub struct OutdatedArgs {}
12
13/// One row in the outdated report.
14#[derive(Debug, Serialize)]
15struct OutdatedEntry {
16    source: String,
17    locked: String,
18    constraint: String,
19    updateable: String,
20    latest: String,
21}
22
23/// Run `mars outdated`.
24pub fn run(_args: &OutdatedArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
25    let lock = crate::lock::load(&ctx.managed_root)?;
26    let config = crate::config::load(&ctx.managed_root)?;
27    let cache = crate::source::GlobalCache::new()?;
28
29    let mut entries = Vec::new();
30
31    for (name, source_entry) in &config.sources {
32        // Only check git sources with versions
33        let url = match &source_entry.url {
34            Some(u) => u,
35            None => continue, // local path sources have no version
36        };
37
38        let locked_version = lock
39            .sources
40            .get(name)
41            .and_then(|s| s.version.clone())
42            .unwrap_or_else(|| "-".to_string());
43
44        let constraint = source_entry
45            .version
46            .clone()
47            .unwrap_or_else(|| "latest".to_string());
48
49        // Try to list versions (may fail for non-git sources)
50        let versions = match crate::source::list_versions(url, &cache) {
51            Ok(v) => v,
52            Err(_) => continue,
53        };
54
55        if versions.is_empty() {
56            // Untagged repo — compare locked commit vs current HEAD
57            let current_head = crate::source::git::ls_remote_head(url.as_ref())
58                .map(|sha| {
59                    if sha.len() >= 12 {
60                        sha[..12].to_string()
61                    } else {
62                        sha
63                    }
64                })
65                .unwrap_or_else(|_| "-".to_string());
66            let locked_commit = lock
67                .sources
68                .get(name)
69                .and_then(|s| s.commit.as_ref().map(|c| c.to_string()))
70                .unwrap_or_else(|| "-".to_string());
71            let locked_short = if locked_commit.len() >= 12 {
72                locked_commit[..12].to_string()
73            } else {
74                locked_commit
75            };
76            entries.push(OutdatedEntry {
77                source: name.to_string(),
78                locked: locked_short,
79                constraint: "HEAD".to_string(),
80                updateable: current_head.clone(),
81                latest: current_head,
82            });
83            continue;
84        }
85
86        // Find latest version overall
87        let latest = versions
88            .iter()
89            .max_by(|a, b| a.version.cmp(&b.version))
90            .map(|v| v.tag.clone())
91            .unwrap_or_else(|| "-".to_string());
92
93        // Find latest version matching current constraint
94        let parsed_constraint =
95            crate::resolve::parse_version_constraint(source_entry.version.as_deref());
96        let updateable = match &parsed_constraint {
97            crate::resolve::VersionConstraint::Semver(req) => versions
98                .iter()
99                .filter(|v| req.matches(&v.version))
100                .max_by(|a, b| a.version.cmp(&b.version))
101                .map(|v| v.tag.clone())
102                .unwrap_or_else(|| locked_version.clone()),
103            crate::resolve::VersionConstraint::Latest => latest.clone(),
104            crate::resolve::VersionConstraint::RefPin(_) => locked_version.clone(),
105        };
106
107        entries.push(OutdatedEntry {
108            source: name.to_string(),
109            locked: locked_version,
110            constraint,
111            updateable,
112            latest,
113        });
114    }
115
116    if json {
117        output::print_json(&entries);
118    } else {
119        print_outdated_table(&entries);
120    }
121
122    Ok(0)
123}
124
125fn print_outdated_table(entries: &[OutdatedEntry]) {
126    if entries.is_empty() {
127        output::print_info("no git sources to check");
128        return;
129    }
130
131    let name_w = entries
132        .iter()
133        .map(|e| e.source.len())
134        .max()
135        .unwrap_or(6)
136        .max(6);
137    let locked_w = entries
138        .iter()
139        .map(|e| e.locked.len())
140        .max()
141        .unwrap_or(6)
142        .max(6);
143    let constraint_w = entries
144        .iter()
145        .map(|e| e.constraint.len())
146        .max()
147        .unwrap_or(10)
148        .max(10);
149    let update_w = entries
150        .iter()
151        .map(|e| e.updateable.len())
152        .max()
153        .unwrap_or(10)
154        .max(10);
155
156    println!(
157        "{:<name_w$}  {:<locked_w$}  {:<constraint_w$}  {:<update_w$}  LATEST",
158        "SOURCE", "LOCKED", "CONSTRAINT", "UPDATEABLE"
159    );
160
161    for entry in entries {
162        println!(
163            "{:<name_w$}  {:<locked_w$}  {:<constraint_w$}  {:<update_w$}  {}",
164            entry.source, entry.locked, entry.constraint, entry.updateable, entry.latest
165        );
166    }
167}