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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub(crate) enum EffectiveExcludeNewerSource {
17 Global,
19 Package,
21 Index,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ExcludeNewerValueChange {
27 SpanChanged(ExcludeNewerSpan, ExcludeNewerSpan),
29 SpanAdded(ExcludeNewerSpan),
31 SpanRemoved,
33 RelativeTimestampChanged(Timestamp, Timestamp, ExcludeNewerSpan),
35 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 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#[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 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 pub fn into_inner(self) -> FxHashMap<PackageName, ExcludeNewerOverride> {
343 self.0
344 }
345
346 pub fn is_empty(&self) -> bool {
348 self.0.is_empty()
349 }
350
351 #[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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
430#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
431pub struct ExcludeNewer {
432 #[serde(default, skip_serializing_if = "Option::is_none")]
434 pub global: Option<ExcludeNewerValue>,
435 #[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
437 pub package: ExcludeNewerPackage,
438}
439
440impl ExcludeNewer {
441 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 pub fn new(global: Option<ExcludeNewerValue>, package: ExcludeNewerPackage) -> Self {
460 Self { global, package }
461 }
462
463 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 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 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 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 pub fn is_empty(&self) -> bool {
530 self.global.is_none() && self.package.is_empty()
531 }
532
533 #[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}