Skip to main content

uv_resolver/
exclude_newer.rs

1use std::{
2    ops::{Deref, DerefMut},
3    str::FromStr,
4};
5
6use jiff::Timestamp;
7use rustc_hash::FxHashMap;
8use serde::ser::SerializeMap;
9use uv_distribution_types::{ExcludeNewerOverride, ExcludeNewerSpan, ExcludeNewerValue};
10use uv_normalize::PackageName;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ExcludeNewerValueChange {
14    /// A relative span changed to a new value
15    SpanChanged(ExcludeNewerSpan, ExcludeNewerSpan),
16    /// A relative span was added
17    SpanAdded(ExcludeNewerSpan),
18    /// A relative span was removed
19    SpanRemoved,
20    /// A relative span is present and the timestamp changed
21    RelativeTimestampChanged(Timestamp, Timestamp, ExcludeNewerSpan),
22    /// The timestamp changed and a relative span is not present
23    AbsoluteTimestampChanged(Timestamp, Timestamp),
24}
25
26impl ExcludeNewerValueChange {
27    pub fn is_relative_timestamp_change(&self) -> bool {
28        matches!(self, Self::RelativeTimestampChanged(_, _, _))
29    }
30}
31
32impl std::fmt::Display for ExcludeNewerValueChange {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::SpanChanged(old, new) => {
36                write!(f, "change of exclude newer span from `{old}` to `{new}`")
37            }
38            Self::SpanAdded(span) => {
39                write!(f, "addition of exclude newer span `{span}`")
40            }
41            Self::SpanRemoved => {
42                write!(f, "removal of exclude newer span")
43            }
44            Self::RelativeTimestampChanged(old, new, span) => {
45                write!(
46                    f,
47                    "change of calculated ({span}) exclude newer timestamp from `{old}` to `{new}`"
48                )
49            }
50            Self::AbsoluteTimestampChanged(old, new) => {
51                write!(
52                    f,
53                    "change of exclude newer timestamp from `{old}` to `{new}`"
54                )
55            }
56        }
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum ExcludeNewerChange {
62    GlobalChanged(ExcludeNewerValueChange),
63    GlobalAdded(ExcludeNewerValue),
64    GlobalRemoved,
65    Package(ExcludeNewerPackageChange),
66}
67
68impl ExcludeNewerChange {
69    /// Whether the change is due to a change in a relative timestamp.
70    pub fn is_relative_timestamp_change(&self) -> bool {
71        match self {
72            Self::GlobalChanged(change) => change.is_relative_timestamp_change(),
73            Self::GlobalAdded(_) | Self::GlobalRemoved => false,
74            Self::Package(change) => change.is_relative_timestamp_change(),
75        }
76    }
77}
78
79impl std::fmt::Display for ExcludeNewerChange {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        match self {
82            Self::GlobalChanged(change) => {
83                write!(f, "{change}")
84            }
85            Self::GlobalAdded(value) => {
86                write!(f, "addition of global exclude newer {value}")
87            }
88            Self::GlobalRemoved => write!(f, "removal of global exclude newer"),
89            Self::Package(change) => {
90                write!(f, "{change}")
91            }
92        }
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum ExcludeNewerPackageChange {
98    PackageAdded(PackageName, ExcludeNewerOverride),
99    PackageRemoved(PackageName),
100    PackageChanged(PackageName, Box<ExcludeNewerOverrideChange>),
101}
102
103impl ExcludeNewerPackageChange {
104    pub fn is_relative_timestamp_change(&self) -> bool {
105        match self {
106            Self::PackageAdded(_, _) | Self::PackageRemoved(_) => false,
107            Self::PackageChanged(_, change) => change.is_relative_timestamp_change(),
108        }
109    }
110}
111
112impl std::fmt::Display for ExcludeNewerPackageChange {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        match self {
115            Self::PackageAdded(name, ExcludeNewerOverride::Enabled(value)) => {
116                write!(
117                    f,
118                    "addition of exclude newer `{}` for package `{name}`",
119                    value.as_ref()
120                )
121            }
122            Self::PackageAdded(name, ExcludeNewerOverride::Disabled) => {
123                write!(
124                    f,
125                    "addition of exclude newer exclusion for package `{name}`"
126                )
127            }
128            Self::PackageRemoved(name) => {
129                write!(f, "removal of exclude newer for package `{name}`")
130            }
131            Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"),
132        }
133    }
134}
135
136fn compare_exclude_newer_value(
137    this: &ExcludeNewerValue,
138    other: &ExcludeNewerValue,
139) -> Option<ExcludeNewerValueChange> {
140    match (this.span(), other.span()) {
141        (None, Some(span)) => Some(ExcludeNewerValueChange::SpanAdded(*span)),
142        (Some(_), None) => Some(ExcludeNewerValueChange::SpanRemoved),
143        (Some(self_span), Some(other_span)) if self_span != other_span => Some(
144            ExcludeNewerValueChange::SpanChanged(*self_span, *other_span),
145        ),
146        (Some(_), Some(span)) if this.timestamp() != other.timestamp() => {
147            Some(ExcludeNewerValueChange::RelativeTimestampChanged(
148                this.timestamp(),
149                other.timestamp(),
150                *span,
151            ))
152        }
153        (None, None) if this.timestamp() != other.timestamp() => Some(
154            ExcludeNewerValueChange::AbsoluteTimestampChanged(this.timestamp(), other.timestamp()),
155        ),
156        (Some(_), Some(_)) | (None, None) => None,
157    }
158}
159
160pub struct ExcludeNewerValueWithSpanRef<'a>(pub &'a ExcludeNewerValue);
161
162impl serde::Serialize for ExcludeNewerValueWithSpanRef<'_> {
163    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: serde::Serializer,
166    {
167        if let Some(span) = self.0.span() {
168            let mut map = serializer.serialize_map(Some(2))?;
169            map.serialize_entry("timestamp", &self.0.timestamp())?;
170            map.serialize_entry("span", span)?;
171            map.end()
172        } else {
173            self.0.timestamp().serialize(serializer)
174        }
175    }
176}
177
178/// A package-specific exclude-newer entry.
179#[derive(Debug, Clone, PartialEq, Eq)]
180#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
181pub struct ExcludeNewerPackageEntry {
182    pub package: PackageName,
183    pub setting: ExcludeNewerOverride,
184}
185
186impl FromStr for ExcludeNewerPackageEntry {
187    type Err = String;
188
189    /// Parses a [`ExcludeNewerPackageEntry`] from a string in the format `PACKAGE=DATE` or `PACKAGE=false`.
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        let Some((package, value)) = s.split_once('=') else {
192            return Err(format!(
193                "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE` or `PACKAGE=false`"
194            ));
195        };
196
197        let package = PackageName::from_str(package).map_err(|err| {
198            format!("Invalid `exclude-newer-package` package name `{package}`: {err}")
199        })?;
200
201        let setting = if value == "false" {
202            ExcludeNewerOverride::Disabled
203        } else {
204            ExcludeNewerOverride::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
205                |err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"),
206            )?))
207        };
208
209        Ok(Self { package, setting })
210    }
211}
212
213impl From<(PackageName, ExcludeNewerOverride)> for ExcludeNewerPackageEntry {
214    fn from((package, setting): (PackageName, ExcludeNewerOverride)) -> Self {
215        Self { package, setting }
216    }
217}
218
219impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry {
220    fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self {
221        Self {
222            package,
223            setting: ExcludeNewerOverride::Enabled(Box::new(timestamp)),
224        }
225    }
226}
227
228pub fn serialize_exclude_newer_package_with_spans<S>(
229    value: &Option<ExcludeNewerPackage>,
230    serializer: S,
231) -> Result<S::Ok, S::Error>
232where
233    S: serde::Serializer,
234{
235    let Some(value) = value else {
236        return serializer.serialize_none();
237    };
238
239    let mut map = serializer.serialize_map(Some(value.len()))?;
240    for (name, setting) in value {
241        match setting {
242            ExcludeNewerOverride::Disabled => map.serialize_entry(name, &false)?,
243            ExcludeNewerOverride::Enabled(value) => {
244                map.serialize_entry(name, &ExcludeNewerValueWithSpanRef(value.as_ref()))?;
245            }
246        }
247    }
248    map.end()
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub enum ExcludeNewerOverrideChange {
253    Disabled { was: ExcludeNewerValue },
254    Enabled { now: ExcludeNewerValue },
255    TimestampChanged(ExcludeNewerValueChange),
256}
257
258impl ExcludeNewerOverrideChange {
259    pub fn is_relative_timestamp_change(&self) -> bool {
260        match self {
261            Self::Disabled { .. } | Self::Enabled { .. } => false,
262            Self::TimestampChanged(change) => change.is_relative_timestamp_change(),
263        }
264    }
265}
266
267impl std::fmt::Display for ExcludeNewerOverrideChange {
268    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269        match self {
270            Self::Disabled { was } => {
271                write!(f, "add exclude newer exclusion (was `{was}`)")
272            }
273            Self::Enabled { now } => {
274                write!(f, "remove exclude newer exclusion (now `{now}`)")
275            }
276            Self::TimestampChanged(change) => write!(f, "{change}"),
277        }
278    }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
282#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
283pub struct ExcludeNewerPackage(FxHashMap<PackageName, ExcludeNewerOverride>);
284
285impl Deref for ExcludeNewerPackage {
286    type Target = FxHashMap<PackageName, ExcludeNewerOverride>;
287
288    fn deref(&self) -> &Self::Target {
289        &self.0
290    }
291}
292
293impl DerefMut for ExcludeNewerPackage {
294    fn deref_mut(&mut self) -> &mut Self::Target {
295        &mut self.0
296    }
297}
298
299impl FromIterator<ExcludeNewerPackageEntry> for ExcludeNewerPackage {
300    fn from_iter<T: IntoIterator<Item = ExcludeNewerPackageEntry>>(iter: T) -> Self {
301        Self(
302            iter.into_iter()
303                .map(|entry| (entry.package, entry.setting))
304                .collect(),
305        )
306    }
307}
308
309impl IntoIterator for ExcludeNewerPackage {
310    type Item = (PackageName, ExcludeNewerOverride);
311    type IntoIter = std::collections::hash_map::IntoIter<PackageName, ExcludeNewerOverride>;
312
313    fn into_iter(self) -> Self::IntoIter {
314        self.0.into_iter()
315    }
316}
317
318impl<'a> IntoIterator for &'a ExcludeNewerPackage {
319    type Item = (&'a PackageName, &'a ExcludeNewerOverride);
320    type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerOverride>;
321
322    fn into_iter(self) -> Self::IntoIter {
323        self.0.iter()
324    }
325}
326
327impl ExcludeNewerPackage {
328    /// Convert to the inner `HashMap`.
329    pub fn into_inner(self) -> FxHashMap<PackageName, ExcludeNewerOverride> {
330        self.0
331    }
332
333    /// Returns true if this map is empty (no package-specific settings).
334    pub fn is_empty(&self) -> bool {
335        self.0.is_empty()
336    }
337
338    /// Recompute all relative span timestamps relative to the current time.
339    #[must_use]
340    pub fn recompute(self) -> Self {
341        Self(
342            self.0
343                .into_iter()
344                .map(|(name, setting)| {
345                    let setting = match setting {
346                        ExcludeNewerOverride::Disabled => ExcludeNewerOverride::Disabled,
347                        ExcludeNewerOverride::Enabled(value) => {
348                            ExcludeNewerOverride::Enabled(Box::new((*value).recompute()))
349                        }
350                    };
351                    (name, setting)
352                })
353                .collect(),
354        )
355    }
356
357    pub fn compare(&self, other: &Self) -> Option<ExcludeNewerPackageChange> {
358        for (package, setting) in self {
359            match (setting, other.get(package)) {
360                (
361                    ExcludeNewerOverride::Enabled(self_timestamp),
362                    Some(ExcludeNewerOverride::Enabled(other_timestamp)),
363                ) => {
364                    if let Some(change) =
365                        compare_exclude_newer_value(self_timestamp, other_timestamp)
366                    {
367                        return Some(ExcludeNewerPackageChange::PackageChanged(
368                            package.clone(),
369                            Box::new(ExcludeNewerOverrideChange::TimestampChanged(change)),
370                        ));
371                    }
372                }
373                (
374                    ExcludeNewerOverride::Enabled(self_timestamp),
375                    Some(ExcludeNewerOverride::Disabled),
376                ) => {
377                    return Some(ExcludeNewerPackageChange::PackageChanged(
378                        package.clone(),
379                        Box::new(ExcludeNewerOverrideChange::Disabled {
380                            was: self_timestamp.as_ref().clone(),
381                        }),
382                    ));
383                }
384                (
385                    ExcludeNewerOverride::Disabled,
386                    Some(ExcludeNewerOverride::Enabled(other_timestamp)),
387                ) => {
388                    return Some(ExcludeNewerPackageChange::PackageChanged(
389                        package.clone(),
390                        Box::new(ExcludeNewerOverrideChange::Enabled {
391                            now: other_timestamp.as_ref().clone(),
392                        }),
393                    ));
394                }
395                (ExcludeNewerOverride::Disabled, Some(ExcludeNewerOverride::Disabled)) => {}
396                (_, None) => {
397                    return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone()));
398                }
399            }
400        }
401
402        for (package, value) in other {
403            if !self.contains_key(package) {
404                return Some(ExcludeNewerPackageChange::PackageAdded(
405                    package.clone(),
406                    value.clone(),
407                ));
408            }
409        }
410
411        None
412    }
413}
414
415/// A setting that excludes files newer than a timestamp, at a global level or per-package.
416#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
417#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
418pub struct ExcludeNewer {
419    /// Global timestamp that applies to all packages if no package-specific timestamp is set.
420    #[serde(default, skip_serializing_if = "Option::is_none")]
421    pub global: Option<ExcludeNewerValue>,
422    /// Per-package timestamps that override the global timestamp.
423    #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
424    pub package: ExcludeNewerPackage,
425}
426
427impl ExcludeNewer {
428    /// Create a new exclude newer configuration with just a global timestamp.
429    pub fn global(global: ExcludeNewerValue) -> Self {
430        Self {
431            global: Some(global),
432            package: ExcludeNewerPackage::default(),
433        }
434    }
435
436    /// Create a new exclude newer configuration.
437    pub fn new(global: Option<ExcludeNewerValue>, package: ExcludeNewerPackage) -> Self {
438        Self { global, package }
439    }
440
441    /// Create from CLI arguments.
442    pub fn from_args(
443        global: Option<ExcludeNewerValue>,
444        package: Vec<ExcludeNewerPackageEntry>,
445    ) -> Self {
446        let package: ExcludeNewerPackage = package.into_iter().collect();
447
448        Self { global, package }
449    }
450
451    /// Returns the exclude-newer value for a specific package, returning `Some(value)` if the
452    /// package has a package-specific setting or falls back to the global value if set, or `None`
453    /// if exclude-newer is explicitly disabled for the package (set to `false`) or if no
454    /// exclude-newer is configured.
455    pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option<ExcludeNewerValue> {
456        match self.package.get(package_name) {
457            Some(ExcludeNewerOverride::Enabled(timestamp)) => Some(timestamp.as_ref().clone()),
458            Some(ExcludeNewerOverride::Disabled) => None,
459            None => self.global.clone(),
460        }
461    }
462
463    /// Returns true if this has any configuration (global or per-package).
464    pub fn is_empty(&self) -> bool {
465        self.global.is_none() && self.package.is_empty()
466    }
467
468    /// Recompute all relative span timestamps relative to the current time.
469    ///
470    /// For values with an absolute timestamp (no span), the timestamp is unchanged.
471    #[must_use]
472    pub fn recompute(self) -> Self {
473        Self {
474            global: self.global.map(ExcludeNewerValue::recompute),
475            package: self.package.recompute(),
476        }
477    }
478
479    pub fn compare(&self, other: &Self) -> Option<ExcludeNewerChange> {
480        match (&self.global, &other.global) {
481            (Some(self_global), Some(other_global)) => {
482                if let Some(change) = compare_exclude_newer_value(self_global, other_global) {
483                    return Some(ExcludeNewerChange::GlobalChanged(change));
484                }
485            }
486            (None, Some(global)) => {
487                return Some(ExcludeNewerChange::GlobalAdded(global.clone()));
488            }
489            (Some(_), None) => return Some(ExcludeNewerChange::GlobalRemoved),
490            (None, None) => (),
491        }
492        self.package
493            .compare(&other.package)
494            .map(ExcludeNewerChange::Package)
495    }
496}
497
498impl std::fmt::Display for ExcludeNewer {
499    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500        if let Some(global) = &self.global {
501            write!(f, "global: {global}")?;
502            if !self.package.is_empty() {
503                write!(f, ", ")?;
504            }
505        }
506        let mut first = true;
507        for (name, setting) in &self.package {
508            if !first {
509                write!(f, ", ")?;
510            }
511            match setting {
512                ExcludeNewerOverride::Enabled(timestamp) => {
513                    write!(f, "{name}: {}", timestamp.as_ref())?;
514                }
515                ExcludeNewerOverride::Disabled => {
516                    write!(f, "{name}: disabled")?;
517                }
518            }
519            first = false;
520        }
521        Ok(())
522    }
523}