1use crate::{
6 indexed::IndexedMetadata,
7 toml_edit::{MutableTomlFile, TomlPathLookup},
8};
9use color_eyre::{Result, eyre::eyre};
10use crates_io_api::SyncClient;
11use itertools::Itertools;
12use semver::{Version, VersionReq};
13use std::{borrow::Borrow, collections::BTreeMap, fs, iter, path::PathBuf};
14use tinyvec::{ArrayVec, array_vec};
15
16pub fn is_major_update_for(requirement: &VersionReq, version: &Version) -> bool {
24 if requirement.matches(version) {
25 return false;
26 }
27
28 if !version.pre.is_empty() {
30 return false;
31 }
32
33 let stripped_version = Version {
34 build: semver::BuildMetadata::EMPTY,
35 pre: semver::Prerelease::EMPTY,
36 ..*version
37 };
38
39 for i in &requirement.comparators {
40 let i_version = Version {
41 major: i.major,
42 minor: i.minor.unwrap_or(version.minor),
43 patch: i.patch.unwrap_or(version.patch),
44 pre: semver::Prerelease::EMPTY,
45 build: semver::BuildMetadata::EMPTY,
46 };
47
48 match i.op {
49 semver::Op::Less | semver::Op::LessEq => {
50 if i_version == stripped_version {
51 return false;
53 }
54 }
55 semver::Op::Exact
56 | semver::Op::Greater
57 | semver::Op::GreaterEq
58 | semver::Op::Tilde
59 | semver::Op::Caret => {
60 if i_version >= stripped_version {
61 return false;
62 }
63 }
64 semver::Op::Wildcard => unreachable!("Should've matched this version already"),
65 op => panic!("Unknown semver operation: {op:?}"),
66 }
67 }
68
69 true
70}
71
72pub fn fetch_versions_for(
74 client: &SyncClient,
75 package: &str,
76) -> Result<Option<impl Iterator<Item = Version>>> {
77 let info = match client.get_crate(package) {
78 Ok(info) => info,
79 Err(crates_io_api::Error::NotFound(_)) => return Ok(None),
80 Err(err) => return Err(err.into()),
81 };
82 let versions = info
83 .versions
84 .into_iter()
85 .filter(|version| !version.yanked)
86 .map(|version| {
87 version
88 .num
89 .parse::<Version>()
90 .expect("Published crate version should be a valid `semver` version")
91 });
92 Ok(Some(versions))
93}
94
95pub fn fetch_major_updates_for(
98 client: &SyncClient,
99 package: &str,
100 reqs: impl Iterator<Item: Borrow<VersionReq>> + Clone,
101) -> Result<Option<impl Iterator<Item = Version>>> {
102 let Some(versions) = fetch_versions_for(client, package)? else {
103 return Ok(None);
104 };
105 let versions = versions.filter(move |version| {
106 reqs.clone()
107 .any(|version_req| is_major_update_for(version_req.borrow(), version))
108 });
109 Ok(Some(versions))
110}
111
112pub enum LatestVersion {
114 CrateNotFound,
115 NoMajorUpdates,
116 NewestUpdate(Version),
117}
118
119pub fn fetch_latest_major_update_for(
122 client: &SyncClient,
123 package: &str,
124 reqs: impl Iterator<Item: Borrow<VersionReq>> + Clone,
125) -> Result<LatestVersion> {
126 let Some(versions) = fetch_major_updates_for(client, package, reqs)? else {
127 return Ok(LatestVersion::CrateNotFound);
128 };
129 let newest = versions.max();
130 Ok(newest.map_or(LatestVersion::NoMajorUpdates, LatestVersion::NewestUpdate))
131}
132
133pub struct DependencyMention {
135 manifest_idx: usize,
136 toml_path: Vec<String>,
138 version: VersionReq,
139}
140
141impl DependencyMention {
142 pub fn toml_path(&self) -> &[String] {
143 &self.toml_path
144 }
145
146 pub fn version(&self) -> &VersionReq {
147 &self.version
148 }
149}
150
151pub struct ManifestDependencySet {
154 pub manifests: ManifestSet,
155 pub dependencies: BTreeMap<String, Vec<DependencyMention>>,
157}
158
159impl ManifestDependencySet {
160 fn dependency_toml_paths(
162 manifest: &MutableTomlFile,
163 ) -> Result<impl Iterator<Item = ArrayVec<[&str; 3]>>> {
164 let targets = manifest
165 .document()
166 .as_table()
167 .get("target")
168 .map(|target| {
169 target.as_table_like().ok_or_else(|| {
170 eyre!("Invalid target table in {:?} at `target`", manifest.path())
171 })
172 })
173 .transpose()?
174 .into_iter()
175 .flat_map(|target| target.iter().map(|(key, _)| key));
176
177 let dep_paths = iter::once(None)
178 .chain(targets.map(Some))
179 .cartesian_product(["dependencies", "build-dependencies", "dev-dependencies"])
180 .map(|(target, dep_kind)| {
181 target.map_or(
182 array_vec!(_ => dep_kind),
183 |target| array_vec!(_ => "target", target, dep_kind),
184 )
185 });
186
187 Ok(dep_paths)
188 }
189
190 fn read_version(manifest: &MutableTomlFile, path: &[String]) -> Result<VersionReq> {
192 let version = manifest
193 .path_lookup(path)
194 .expect("Version path lookup failed (maybe the `MutableTomlFile` changed?)")
195 .as_str()
196 .ok_or_else(|| {
197 eyre!(
198 "Invalid `version`/immediate value in {path:?} at {:?}",
199 manifest.path()
200 )
201 })?
202 .parse::<VersionReq>()?;
203 Ok(version)
204 }
205
206 fn collect_dependencies(
208 manifest_idx: usize,
209 manifest: &MutableTomlFile,
210 direct_dependencies: &mut BTreeMap<String, Vec<DependencyMention>>,
211 ) -> Result<()> {
212 for dep_path in Self::dependency_toml_paths(manifest)? {
213 let Some(dependencies) = manifest.path_lookup(dep_path) else {
214 continue;
215 };
216
217 let dependencies = dependencies.as_table_like().ok_or_else(|| {
218 eyre!(
219 "Invalid dependency table in {:?} at {dep_path}",
220 manifest.path()
221 )
222 })?;
223
224 for (name, dependency) in dependencies.iter() {
225 let (package, version_path_segment) =
226 if let Some(dependency) = dependency.as_table_like() {
227 let package = match dependency.get("package") {
228 None => name,
229 Some(package) => package.as_str().ok_or_else(|| {
230 eyre!(
231 "Invalid `package` value in {:?} at {dep_path}.{name:?}",
232 manifest.path()
233 )
234 })?,
235 };
236
237 if dependency.contains_key("registry")
238 || !dependency.contains_key("version")
239 || dependency.contains_key("git")
240 {
241 continue;
242 }
243
244 (package, Some("version"))
245 } else {
246 (name, None)
247 };
248
249 let version_path = dep_path
250 .into_iter()
251 .chain(iter::once(name))
252 .chain(version_path_segment)
253 .map(|s| s.to_owned())
254 .collect::<Vec<_>>();
255
256 let version = Self::read_version(manifest, &version_path)?;
257
258 direct_dependencies
259 .entry(package.to_owned())
260 .or_default()
261 .push(DependencyMention {
262 manifest_idx,
263 toml_path: version_path,
264 version,
265 })
266 }
267 }
268
269 Ok(())
270 }
271
272 pub fn collect(metadata: &IndexedMetadata) -> Result<Self> {
275 let manifests = ManifestSet::collect(metadata)?;
276
277 let mut dependencies = BTreeMap::new();
278 for (idx, manifest) in manifests.manifests.iter().enumerate() {
279 Self::collect_dependencies(idx, manifest, &mut dependencies)?;
280 }
281
282 Ok(ManifestDependencySet {
283 manifests,
284 dependencies,
285 })
286 }
287
288 pub fn commit(&mut self) -> Result<()> {
290 self.manifests.write_back()?;
291 self.manifests.commit_lock_contents()?;
292
293 for manifest in &mut self.manifests.manifests {
296 manifest.commit()?;
298 }
299
300 Ok(())
301 }
302
303 pub fn roll_back(&mut self) -> Result<()> {
306 let mut errors = Vec::new();
307
308 if let Err(error) = self.manifests.roll_back_lock_contents() {
309 errors.push(error);
310 }
311
312 for manifest in &mut self.manifests.manifests {
313 if let Err(error) = manifest.roll_back() {
314 errors.push(error);
315 }
316 }
317
318 for mention in self.dependencies.values_mut().flatten() {
319 mention.version = Self::read_version(
320 &self.manifests.manifests[mention.manifest_idx],
321 &mention.toml_path,
322 )?;
323 }
324
325 if errors.is_empty() {
326 Ok(())
327 } else {
328 Err(eyre!("Failed to roll back:\n{errors:?}"))
329 }
330 }
331}
332
333pub struct ManifestSet {
335 manifests: Vec<MutableTomlFile>,
336 lock_path: PathBuf,
337 last_lock_contents: String,
338}
339
340impl ManifestSet {
341 pub fn collect(metadata: &IndexedMetadata) -> Result<Self> {
343 let workspace_manifest = metadata.workspace_root.join("Cargo.toml");
344 let lock_path = workspace_manifest.with_extension("lock").into();
345
346 let mut member_manifests = metadata
347 .packages
348 .iter()
349 .filter(|(pkg_id, _)| metadata.workspace_members.contains(pkg_id))
350 .map(|(_, pkg)| &pkg.manifest_path)
351 .collect::<Vec<_>>();
352
353 let isnt_workspace = matches!(*member_manifests, [single] if *single == workspace_manifest);
354
355 if isnt_workspace {
356 member_manifests.clear();
357 }
358
359 let manifests = iter::once(&workspace_manifest)
360 .chain(member_manifests)
361 .map(MutableTomlFile::open)
362 .collect::<Result<Vec<_>>>()?;
363
364 let last_lock_contents = fs::read_to_string(&lock_path)?;
365
366 Ok(ManifestSet {
367 manifests,
368 lock_path,
369 last_lock_contents,
370 })
371 }
372
373 pub fn as_slice(&self) -> &[MutableTomlFile] {
374 &self.manifests
375 }
376
377 pub fn as_slice_mut(&mut self) -> &mut [MutableTomlFile] {
378 &mut self.manifests
379 }
380
381 pub fn write_back(&mut self) -> Result<()> {
383 for manifest in &mut self.manifests {
384 manifest.write_back()?;
385 }
386
387 Ok(())
388 }
389
390 pub fn manifest_for(&self, mention: &DependencyMention) -> &MutableTomlFile {
393 &self.manifests[mention.manifest_idx]
394 }
395
396 pub fn manifest_mut_for(&mut self, mention: &DependencyMention) -> &mut MutableTomlFile {
399 &mut self.manifests[mention.manifest_idx]
400 }
401
402 pub fn write_back_for(&mut self, mention: &DependencyMention) -> Result<()> {
405 self.manifest_mut_for(mention).write_back()?;
406 Ok(())
407 }
408
409 pub fn write_back_for_all(&mut self, mentions: &[DependencyMention]) -> Result<()> {
412 for mention in mentions {
413 self.write_back_for(mention)?;
414 }
415 Ok(())
416 }
417
418 pub fn write_version_to_memory(
421 &mut self,
422 mention: &mut DependencyMention,
423 version: VersionReq,
424 ) {
425 let Some(toml_edit::Value::String(toml_version)) = self
426 .manifest_mut_for(mention)
427 .path_lookup_mut(&mention.toml_path)
428 .and_then(toml_edit::Item::as_value_mut)
429 else {
430 panic!("Version path lookup failed (maybe the `MutableTomlFile` changed?)");
431 };
432 let decor = toml_version.decor().clone();
433
434 let as_string = match *version.comparators {
435 [ref single] if single.op == semver::Op::Caret => {
436 let mut out = version.to_string();
437 if out.starts_with('^') {
438 out.remove(0); }
440 out
441 }
442 _ => version.to_string(),
443 };
444
445 *toml_version = toml_edit::Formatted::new(as_string);
446 *toml_version.decor_mut() = decor;
447
448 mention.version = version;
449 }
450
451 pub fn write_versions_to_memory(
454 &mut self,
455 mentions: &mut [DependencyMention],
456 version: &VersionReq,
457 ) {
458 for mention in mentions {
459 self.write_version_to_memory(mention, version.clone());
460 }
461 }
462
463 pub fn write_version_to_file(
465 &mut self,
466 mention: &mut DependencyMention,
467 version: VersionReq,
468 ) -> Result<()> {
469 self.write_version_to_memory(mention, version);
470 self.write_back_for(mention)?;
471 Ok(())
472 }
473
474 pub fn write_versions_to_file(
476 &mut self,
477 mentions: &mut [DependencyMention],
478 version: &VersionReq,
479 ) -> Result<()> {
480 self.write_versions_to_memory(mentions, version);
481 self.write_back_for_all(mentions)?;
482 Ok(())
483 }
484
485 pub fn update_version_in_memory(&mut self, mention: &mut DependencyMention, version: &Version) {
487 if is_major_update_for(&mention.version, version) {
488 self.write_version_to_memory(
489 mention,
490 VersionReq {
491 comparators: vec![semver::Comparator {
492 op: semver::Op::Caret,
493 major: version.major,
494 minor: Some(version.minor),
495 patch: Some(version.patch),
496 pre: version.pre.clone(),
497 }],
498 },
499 );
500 }
501 }
502
503 pub fn update_versions_in_memory(
506 &mut self,
507 mentions: &mut [DependencyMention],
508 version: &Version,
509 ) {
510 for mention in mentions {
511 self.update_version_in_memory(mention, version);
512 }
513 }
514
515 pub fn update_version_in_file(
517 &mut self,
518 mention: &mut DependencyMention,
519 version: &Version,
520 ) -> Result<()> {
521 self.update_version_in_memory(mention, version);
522 self.write_back_for(mention)?;
523 Ok(())
524 }
525
526 pub fn update_versions_in_file(
528 &mut self,
529 mentions: &mut [DependencyMention],
530 version: &Version,
531 ) -> Result<()> {
532 self.update_versions_in_memory(mentions, version);
533 self.write_back_for_all(mentions)?;
534 Ok(())
535 }
536
537 pub fn commit_lock_contents(&mut self) -> Result<()> {
538 self.last_lock_contents = fs::read_to_string(&self.lock_path)?;
539 Ok(())
540 }
541
542 pub fn roll_back_lock_contents(&mut self) -> Result<()> {
543 fs::write(&self.lock_path, &self.last_lock_contents)?;
544 Ok(())
545 }
546}