Skip to main content

mars_agents/cli/
outdated.rs

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