1use std::{
2 collections::BTreeMap,
3 ops::{Deref, DerefMut},
4 str::FromStr,
5};
6
7use jiff::Timestamp;
8use rustc_hash::FxHashSet;
9use serde::ser::SerializeMap;
10use uv_distribution_types::{ExcludeNewerOverride, ExcludeNewerSpan, ExcludeNewerValue};
11use uv_normalize::PackageName;
12use uv_preview::PreviewFeature;
13use uv_warnings::warn_user_once;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum EffectiveExcludeNewerSource {
18 Global,
20 Package,
22 Index,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum ExcludeNewerValueChange {
28 SpanChanged(ExcludeNewerSpan, ExcludeNewerSpan),
30 SpanAdded(ExcludeNewerSpan),
32 SpanRemoved,
34 RelativeTimestampChanged(Timestamp, Timestamp, ExcludeNewerSpan),
36 AbsoluteTimestampChanged(Timestamp, Timestamp),
38}
39
40impl ExcludeNewerValueChange {
41 fn is_relative_timestamp_change(&self) -> bool {
42 matches!(self, Self::RelativeTimestampChanged(_, _, _))
43 }
44}
45
46impl std::fmt::Display for ExcludeNewerValueChange {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::SpanChanged(old, new) => {
50 write!(f, "change of exclude newer span from `{old}` to `{new}`")
51 }
52 Self::SpanAdded(span) => {
53 write!(f, "addition of exclude newer span `{span}`")
54 }
55 Self::SpanRemoved => {
56 write!(f, "removal of exclude newer span")
57 }
58 Self::RelativeTimestampChanged(old, new, span) => {
59 write!(
60 f,
61 "change of calculated ({span}) exclude newer timestamp from `{old}` to `{new}`"
62 )
63 }
64 Self::AbsoluteTimestampChanged(old, new) => {
65 write!(
66 f,
67 "change of exclude newer timestamp from `{old}` to `{new}`"
68 )
69 }
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum ExcludeNewerChange {
76 GlobalChanged(ExcludeNewerValueChange),
77 GlobalAdded(ExcludeNewerValue),
78 GlobalRemoved,
79 Package(ExcludeNewerPackageChange),
80}
81
82impl ExcludeNewerChange {
83 pub fn is_relative_timestamp_change(&self) -> bool {
85 match self {
86 Self::GlobalChanged(change) => change.is_relative_timestamp_change(),
87 Self::GlobalAdded(_) | Self::GlobalRemoved => false,
88 Self::Package(change) => change.is_relative_timestamp_change(),
89 }
90 }
91}
92
93impl std::fmt::Display for ExcludeNewerChange {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::GlobalChanged(change) => {
97 write!(f, "{change}")
98 }
99 Self::GlobalAdded(value) => {
100 write!(f, "addition of global exclude newer {value}")
101 }
102 Self::GlobalRemoved => write!(f, "removal of global exclude newer"),
103 Self::Package(change) => {
104 write!(f, "{change}")
105 }
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum ExcludeNewerPackageChange {
112 PackageAdded(PackageName, ExcludeNewerOverride),
113 PackageRemoved(PackageName),
114 PackageChanged(PackageName, Box<ExcludeNewerOverrideChange>),
115}
116
117impl ExcludeNewerPackageChange {
118 fn is_relative_timestamp_change(&self) -> bool {
119 match self {
120 Self::PackageAdded(_, _) | Self::PackageRemoved(_) => false,
121 Self::PackageChanged(_, change) => change.is_relative_timestamp_change(),
122 }
123 }
124}
125
126impl std::fmt::Display for ExcludeNewerPackageChange {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 match self {
129 Self::PackageAdded(name, ExcludeNewerOverride::Enabled(value)) => {
130 write!(
131 f,
132 "addition of exclude newer `{}` for package `{name}`",
133 value.as_ref()
134 )
135 }
136 Self::PackageAdded(name, ExcludeNewerOverride::Disabled) => {
137 write!(
138 f,
139 "addition of exclude newer exclusion for package `{name}`"
140 )
141 }
142 Self::PackageRemoved(name) => {
143 write!(f, "removal of exclude newer for package `{name}`")
144 }
145 Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"),
146 }
147 }
148}
149
150fn compare_exclude_newer_value(
151 this: &ExcludeNewerValue,
152 other: &ExcludeNewerValue,
153) -> Option<ExcludeNewerValueChange> {
154 match (this.span(), other.span()) {
155 (None, Some(span)) => Some(ExcludeNewerValueChange::SpanAdded(*span)),
156 (Some(_), None) => Some(ExcludeNewerValueChange::SpanRemoved),
157 (Some(self_span), Some(other_span)) if self_span != other_span => Some(
158 ExcludeNewerValueChange::SpanChanged(*self_span, *other_span),
159 ),
160 (Some(_), Some(span)) if this.timestamp() != other.timestamp() => {
161 Some(ExcludeNewerValueChange::RelativeTimestampChanged(
162 this.timestamp(),
163 other.timestamp(),
164 *span,
165 ))
166 }
167 (None, None) if other.timestamp() < this.timestamp() => Some(
168 ExcludeNewerValueChange::AbsoluteTimestampChanged(this.timestamp(), other.timestamp()),
169 ),
170 (Some(_), Some(_)) | (None, None) => None,
171 }
172}
173
174pub struct ExcludeNewerValueWithSpanRef<'a>(pub &'a ExcludeNewerValue);
175
176impl serde::Serialize for ExcludeNewerValueWithSpanRef<'_> {
177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178 where
179 S: serde::Serializer,
180 {
181 if let Some(span) = self.0.span() {
182 let mut map = serializer.serialize_map(Some(2))?;
183 map.serialize_entry("timestamp", &self.0.timestamp())?;
184 map.serialize_entry("span", span)?;
185 map.end()
186 } else {
187 self.0.timestamp().serialize(serializer)
188 }
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
195pub struct ExcludeNewerPackageEntry {
196 package: PackageName,
197 setting: ExcludeNewerOverride,
198}
199
200impl FromStr for ExcludeNewerPackageEntry {
201 type Err = String;
202
203 fn from_str(s: &str) -> Result<Self, Self::Err> {
205 let Some((package, value)) = s.split_once('=') else {
206 return Err(format!(
207 "Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE` or `PACKAGE=false`"
208 ));
209 };
210
211 let package = PackageName::from_str(package).map_err(|err| {
212 format!("Invalid `exclude-newer-package` package name `{package}`: {err}")
213 })?;
214
215 let setting = if value == "false" {
216 ExcludeNewerOverride::Disabled
217 } else {
218 ExcludeNewerOverride::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
219 |err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"),
220 )?))
221 };
222
223 Ok(Self { package, setting })
224 }
225}
226
227impl From<(PackageName, ExcludeNewerOverride)> for ExcludeNewerPackageEntry {
228 fn from((package, setting): (PackageName, ExcludeNewerOverride)) -> Self {
229 Self { package, setting }
230 }
231}
232
233impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry {
234 fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self {
235 Self {
236 package,
237 setting: ExcludeNewerOverride::Enabled(Box::new(timestamp)),
238 }
239 }
240}
241
242pub fn serialize_exclude_newer_package_with_spans<S>(
243 value: &Option<ExcludeNewerPackage>,
244 serializer: S,
245) -> Result<S::Ok, S::Error>
246where
247 S: serde::Serializer,
248{
249 let Some(value) = value else {
250 return serializer.serialize_none();
251 };
252
253 let mut map = serializer.serialize_map(Some(value.len()))?;
254 for (name, setting) in value {
255 match setting {
256 ExcludeNewerOverride::Disabled => map.serialize_entry(name, &false)?,
257 ExcludeNewerOverride::Enabled(value) => {
258 map.serialize_entry(name, &ExcludeNewerValueWithSpanRef(value.as_ref()))?;
259 }
260 }
261 }
262 map.end()
263}
264
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub enum ExcludeNewerOverrideChange {
267 Enabled { now: ExcludeNewerValue },
268 TimestampChanged(ExcludeNewerValueChange),
269}
270
271impl ExcludeNewerOverrideChange {
272 fn is_relative_timestamp_change(&self) -> bool {
273 match self {
274 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::Enabled { now } => {
284 write!(f, "remove exclude newer exclusion (now `{now}`)")
285 }
286 Self::TimestampChanged(change) => write!(f, "{change}"),
287 }
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
295#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
296pub struct ExcludeNewerPackage(BTreeMap<PackageName, ExcludeNewerOverride>);
297
298impl Deref for ExcludeNewerPackage {
299 type Target = BTreeMap<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::btree_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::btree_map::Iter<'a, PackageName, ExcludeNewerOverride>;
334
335 fn into_iter(self) -> Self::IntoIter {
336 self.0.iter()
337 }
338}
339
340impl ExcludeNewerPackage {
341 pub fn is_empty(&self) -> bool {
343 self.0.is_empty()
344 }
345
346 fn compare(&self, other: &Self) -> Option<ExcludeNewerPackageChange> {
347 for (package, setting) in self {
348 match (setting, other.get(package)) {
349 (
350 ExcludeNewerOverride::Enabled(self_timestamp),
351 Some(ExcludeNewerOverride::Enabled(other_timestamp)),
352 ) => {
353 if let Some(change) =
354 compare_exclude_newer_value(self_timestamp, other_timestamp)
355 {
356 return Some(ExcludeNewerPackageChange::PackageChanged(
357 package.clone(),
358 Box::new(ExcludeNewerOverrideChange::TimestampChanged(change)),
359 ));
360 }
361 }
362 (ExcludeNewerOverride::Enabled(_), Some(ExcludeNewerOverride::Disabled)) => {}
363 (
364 ExcludeNewerOverride::Disabled,
365 Some(ExcludeNewerOverride::Enabled(other_timestamp)),
366 ) => {
367 return Some(ExcludeNewerPackageChange::PackageChanged(
368 package.clone(),
369 Box::new(ExcludeNewerOverrideChange::Enabled {
370 now: other_timestamp.as_ref().clone(),
371 }),
372 ));
373 }
374 (ExcludeNewerOverride::Disabled, Some(ExcludeNewerOverride::Disabled)) => {}
375 (_, None) => {
376 return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone()));
377 }
378 }
379 }
380
381 for (package, value) in other {
382 if !self.contains_key(package)
383 && let ExcludeNewerOverride::Enabled(_) = value
384 {
385 return Some(ExcludeNewerPackageChange::PackageAdded(
386 package.clone(),
387 value.clone(),
388 ));
389 }
390 }
391
392 None
393 }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
398#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
399pub struct ExcludeNewer {
400 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub global: Option<ExcludeNewerValue>,
403 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
405 pub package: ExcludeNewerPackage,
406}
407
408impl ExcludeNewer {
409 pub fn global(global: ExcludeNewerValue) -> Self {
411 Self {
412 global: Some(global),
413 package: ExcludeNewerPackage::default(),
414 }
415 }
416
417 fn warn_index_exclude_newer_preview() {
418 if !uv_preview::is_enabled(PreviewFeature::IndexExcludeNewer) {
419 warn_user_once!(
420 "Setting `exclude-newer` on configured indexes is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
421 PreviewFeature::IndexExcludeNewer
422 );
423 }
424 }
425
426 pub fn from_args(
428 global: Option<ExcludeNewerOverride>,
429 package: Vec<ExcludeNewerPackageEntry>,
430 ) -> Self {
431 let global = global.and_then(ExcludeNewerOverride::into_value);
432 let package: ExcludeNewerPackage = package.into_iter().collect();
433
434 Self { global, package }
435 }
436
437 pub(crate) fn exclude_newer_package(&self, package_name: &PackageName) -> Option<Timestamp> {
440 match self.package.get(package_name) {
441 Some(ExcludeNewerOverride::Enabled(value)) => Some(value.timestamp()),
442 Some(ExcludeNewerOverride::Disabled) => None,
443 None => self.global.as_ref().map(ExcludeNewerValue::timestamp),
444 }
445 }
446
447 pub fn exclude_newer_package_for_index(
449 &self,
450 package_name: &PackageName,
451 index: Option<&ExcludeNewerOverride>,
452 ) -> Option<Timestamp> {
453 self.exclude_newer_package_for_index_with_source(package_name, index)
454 .map(|(timestamp, _)| timestamp)
455 }
456
457 pub(crate) fn exclude_newer_package_for_index_with_source(
460 &self,
461 package_name: &PackageName,
462 index: Option<&ExcludeNewerOverride>,
463 ) -> Option<(Timestamp, EffectiveExcludeNewerSource)> {
464 match self.package.get(package_name) {
465 Some(ExcludeNewerOverride::Enabled(value)) => {
466 Some((value.timestamp(), EffectiveExcludeNewerSource::Package))
467 }
468 Some(ExcludeNewerOverride::Disabled) => None,
469 None => match index {
470 Some(ExcludeNewerOverride::Disabled) => {
471 Self::warn_index_exclude_newer_preview();
472 None
473 }
474 Some(ExcludeNewerOverride::Enabled(value)) => Some((
475 {
476 Self::warn_index_exclude_newer_preview();
477 value.timestamp()
478 },
479 EffectiveExcludeNewerSource::Index,
480 )),
481 None => self
482 .global
483 .as_ref()
484 .map(|value| (value.timestamp(), EffectiveExcludeNewerSource::Global)),
485 },
486 }
487 }
488
489 pub(crate) fn is_empty(&self) -> bool {
491 self.global.is_none() && self.package.is_empty()
492 }
493
494 #[must_use]
496 pub fn filter_packages<'a>(self, packages: impl IntoIterator<Item = &'a PackageName>) -> Self {
497 let packages = packages.into_iter().collect::<FxHashSet<_>>();
498 Self {
499 global: self.global,
500 package: ExcludeNewerPackage(
501 self.package
502 .into_iter()
503 .filter(|(package, _)| packages.contains(package))
504 .collect(),
505 ),
506 }
507 }
508
509 pub fn compare(&self, other: &Self) -> Option<ExcludeNewerChange> {
514 match (&self.global, &other.global) {
515 (Some(self_global), Some(other_global)) => {
516 if let Some(change) = compare_exclude_newer_value(self_global, other_global) {
517 if !change.is_relative_timestamp_change() {
518 return Some(ExcludeNewerChange::GlobalChanged(change));
519 }
520 }
521 }
522 (None, Some(global)) => {
523 return Some(ExcludeNewerChange::GlobalAdded(global.clone()));
524 }
525 (Some(_), None) => return Some(ExcludeNewerChange::GlobalRemoved),
526 (None, None) => (),
527 }
528 self.package
529 .compare(&other.package)
530 .map(ExcludeNewerChange::Package)
531 }
532}
533
534impl std::fmt::Display for ExcludeNewer {
535 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536 if let Some(global) = &self.global {
537 write!(f, "global: {global}")?;
538 if !self.package.is_empty() {
539 write!(f, ", ")?;
540 }
541 }
542 let mut first = true;
543 for (name, setting) in &self.package {
544 if !first {
545 write!(f, ", ")?;
546 }
547 match setting {
548 ExcludeNewerOverride::Enabled(timestamp) => {
549 write!(f, "{name}: {}", timestamp.as_ref())?;
550 }
551 ExcludeNewerOverride::Disabled => {
552 write!(f, "{name}: disabled")?;
553 }
554 }
555 first = false;
556 }
557 Ok(())
558 }
559}