Skip to main content

ephemeris_release/adapters/cargo/
version_groups.rs

1//! Cargo packages that share one version source and so release together.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use super::workspace::{VersionSource, WorkspaceError};
6use crate::version::Version;
7
8/// Package names grouped by the manifest entry that sets their version. The
9/// only constructor files every package under exactly one source, in name
10/// order.
11#[derive(Clone, Debug, Default)]
12pub(crate) struct VersionGroups(BTreeMap<VersionSource, Vec<String>>);
13
14impl VersionGroups {
15    pub(crate) fn new(packages: impl Iterator<Item = (VersionSource, String)>) -> Self {
16        let mut groups = BTreeMap::<VersionSource, Vec<String>>::new();
17        for (source, name) in packages {
18            groups.entry(source).or_default().push(name);
19        }
20        for members in groups.values_mut() {
21            members.sort();
22            members.dedup();
23        }
24        Self(groups)
25    }
26
27    pub(crate) fn iter(&self) -> impl Iterator<Item = (&VersionSource, &[String])> {
28        self.0
29            .iter()
30            .map(|(source, members)| (source, members.as_slice()))
31    }
32
33    /// Reject a selection that covers only part of a group.
34    pub(crate) fn validate_selection(
35        &self,
36        selected: &BTreeSet<String>,
37    ) -> Result<(), WorkspaceError> {
38        for (source, members) in self.iter() {
39            if members.iter().any(|name| selected.contains(name)) {
40                let missing: Vec<_> = members
41                    .iter()
42                    .filter(|name| !selected.contains(*name))
43                    .cloned()
44                    .collect();
45                if !missing.is_empty() {
46                    return Err(WorkspaceError::IncompleteVersionGroup {
47                        version_source: source.clone(),
48                        missing,
49                    });
50                }
51            }
52        }
53        Ok(())
54    }
55
56    /// An explicit version for one member sets its whole group; conflicting
57    /// targets within a group are rejected.
58    pub(crate) fn complete_explicit(
59        &self,
60        proposals: &BTreeMap<String, Version>,
61    ) -> Result<BTreeMap<String, Version>, WorkspaceError> {
62        let mut completed = proposals.clone();
63        for (source, members) in self.iter() {
64            let targets = distinct(members.iter().filter_map(|name| proposals.get(name)));
65            let target = match targets.as_slice() {
66                [] => continue,
67                [target] => (*target).clone(),
68                [_, _, ..] => {
69                    return Err(WorkspaceError::ConflictingVersionGroup {
70                        version_source: source.clone(),
71                        members: members.to_vec(),
72                    });
73                }
74            };
75            completed.extend(
76                members
77                    .iter()
78                    .map(|member| (member.clone(), target.clone())),
79            );
80        }
81        Ok(completed)
82    }
83
84    /// Automatic proposals fan out to their group at the highest target.
85    pub(crate) fn complete_automatic(
86        &self,
87        proposals: &BTreeMap<String, Version>,
88    ) -> BTreeMap<String, Version> {
89        let mut completed = proposals.clone();
90        for (_, members) in self.iter() {
91            let Some(target) = members
92                .iter()
93                .filter_map(|name| proposals.get(name))
94                .max_by(|left, right| left.precedence_cmp(right))
95            else {
96                continue;
97            };
98            for member in members {
99                completed.insert(member.clone(), target.clone());
100            }
101        }
102        completed
103    }
104}
105
106/// Distinct values in first-seen order.
107pub(crate) fn distinct<'a, V: PartialEq>(values: impl Iterator<Item = &'a V>) -> Vec<&'a V> {
108    let mut distinct = Vec::new();
109    for value in values {
110        if !distinct.contains(&value) {
111            distinct.push(value);
112        }
113    }
114    distinct
115}