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