1use crate::lockfile::LockFile;
2use crate::manifest::{DepSourceKind, DependencySpec, Manifest};
3
4#[derive(Debug, Clone)]
6pub struct OutdatedInfo {
7 pub name: String,
8 pub current: String,
9 pub latest_matching: Option<String>,
11 pub latest_available: Option<String>,
13 pub source_kind: DepSourceKind,
15}
16
17impl OutdatedInfo {
18 pub fn is_up_to_date(&self) -> bool {
19 match (&self.latest_matching, &self.latest_available) {
20 (Some(matching), _) => matching == &self.current,
21 _ => true,
22 }
23 }
24}
25
26pub fn check_outdated_with_versions(
31 manifest: &Manifest,
32 lock: &LockFile,
33 version_lookup: &dyn Fn(&str) -> Result<Vec<String>, String>,
34) -> Result<Vec<OutdatedInfo>, String> {
35 let mut results = Vec::new();
36
37 for (name, spec) in &manifest.dependencies {
38 let source_kind = spec.source_kind();
39
40 let current = match lock.find(name) {
42 Some(locked) => locked.version.clone(),
43 None => continue, };
45
46 if source_kind != DepSourceKind::Registry {
47 results.push(OutdatedInfo {
49 name: name.clone(),
50 current,
51 latest_matching: None,
52 latest_available: None,
53 source_kind,
54 });
55 continue;
56 }
57
58 let version_req_str = match spec {
60 DependencySpec::Simple(req) => req.clone(),
61 DependencySpec::Detailed(d) => d.version.clone().unwrap_or_else(|| "*".into()),
62 };
63
64 match version_lookup(name) {
66 Ok(versions) => {
67 let req = crate::version::VersionReq::parse(&version_req_str)?;
68
69 let mut matching_versions: Vec<crate::version::Version> = versions
71 .iter()
72 .filter_map(|v| crate::version::Version::parse(v).ok())
73 .filter(|v| req.matches(v))
74 .collect();
75 matching_versions.sort();
76 let latest_matching = matching_versions.last().map(|v| v.to_string());
77
78 let mut all_versions: Vec<crate::version::Version> = versions
80 .iter()
81 .filter_map(|v| crate::version::Version::parse(v).ok())
82 .collect();
83 all_versions.sort();
84 let latest_available = all_versions.last().map(|v| v.to_string());
85
86 results.push(OutdatedInfo {
87 name: name.clone(),
88 current,
89 latest_matching,
90 latest_available,
91 source_kind,
92 });
93 }
94 Err(_) => {
95 results.push(OutdatedInfo {
97 name: name.clone(),
98 current,
99 latest_matching: None,
100 latest_available: None,
101 source_kind,
102 });
103 }
104 }
105 }
106
107 Ok(results)
108}
109
110#[cfg(feature = "registry")]
112pub fn check_outdated(manifest: &Manifest, lock: &LockFile) -> Result<Vec<OutdatedInfo>, String> {
113 check_outdated_with_versions(manifest, lock, &|name| {
114 let info = crate::registry_client::get_package_info(name)?;
115 Ok(info.versions.iter().map(|v| v.version.clone()).collect())
116 })
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::lockfile::LockedPackage;
123 use crate::manifest::{DetailedDep, ProjectConfig};
124 use std::collections::BTreeMap;
125
126 fn test_manifest(deps: Vec<(&str, DependencySpec)>) -> Manifest {
127 let mut dependencies = BTreeMap::new();
128 for (name, spec) in deps {
129 dependencies.insert(name.to_string(), spec);
130 }
131 Manifest {
132 project: ProjectConfig {
133 name: "test".into(),
134 version: "0.1.0".into(),
135 edition: None,
136 authors: None,
137 description: None,
138 entry: None,
139 },
140 dependencies,
141 }
142 }
143
144 #[test]
145 fn test_outdated_newer_available() {
146 let manifest = test_manifest(vec![("utils", DependencySpec::Simple("^1.0".into()))]);
147 let lock = LockFile {
148 packages: vec![LockedPackage::new(
149 "utils",
150 "1.0.0",
151 "registry+http://localhost@1.0.0".into(),
152 )],
153 };
154
155 let results = check_outdated_with_versions(&manifest, &lock, &|_name| {
156 Ok(vec!["1.0.0".into(), "1.3.0".into(), "2.0.0".into()])
157 })
158 .unwrap();
159
160 assert_eq!(results.len(), 1);
161 assert_eq!(results[0].name, "utils");
162 assert_eq!(results[0].current, "1.0.0");
163 assert_eq!(results[0].latest_matching, Some("1.3.0".into()));
164 assert_eq!(results[0].latest_available, Some("2.0.0".into()));
165 assert!(!results[0].is_up_to_date());
166 }
167
168 #[test]
169 fn test_outdated_up_to_date() {
170 let manifest = test_manifest(vec![("helpers", DependencySpec::Simple("^2.0".into()))]);
171 let lock = LockFile {
172 packages: vec![LockedPackage::new(
173 "helpers",
174 "2.1.0",
175 "registry+http://localhost@2.1.0".into(),
176 )],
177 };
178
179 let results = check_outdated_with_versions(&manifest, &lock, &|_name| {
180 Ok(vec!["2.0.0".into(), "2.1.0".into()])
181 })
182 .unwrap();
183
184 assert_eq!(results.len(), 1);
185 assert!(results[0].is_up_to_date());
186 }
187
188 #[test]
189 fn test_outdated_git_dep() {
190 let manifest = test_manifest(vec![(
191 "mylib",
192 DependencySpec::Detailed(DetailedDep {
193 version: None,
194 git: Some("https://github.com/user/mylib.git".into()),
195 branch: None,
196 tag: None,
197 rev: None,
198 path: None,
199 }),
200 )]);
201 let lock = LockFile {
202 packages: vec![LockedPackage::new(
203 "mylib",
204 "1.0.0",
205 LockedPackage::git_source("https://github.com/user/mylib.git", "abc123"),
206 )],
207 };
208
209 let results = check_outdated_with_versions(&manifest, &lock, &|_| {
210 panic!("should not query registry for git deps");
211 })
212 .unwrap();
213
214 assert_eq!(results.len(), 1);
215 assert_eq!(results[0].source_kind, DepSourceKind::Git);
216 assert!(results[0].is_up_to_date()); }
218}