use std::{
ops::{Deref, DerefMut},
str::FromStr,
};
use jiff::Timestamp;
use rustc_hash::FxHashMap;
use serde::ser::SerializeMap;
use uv_distribution_types::{ExcludeNewerOverride, ExcludeNewerSpan, ExcludeNewerValue};
use uv_normalize::PackageName;
use uv_preview::PreviewFeature;
use uv_warnings::warn_user_once;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum EffectiveExcludeNewerSource {
Global,
Package,
Index,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExcludeNewerValueChange {
SpanChanged(ExcludeNewerSpan, ExcludeNewerSpan),
SpanAdded(ExcludeNewerSpan),
SpanRemoved,
RelativeTimestampChanged(Timestamp, Timestamp, ExcludeNewerSpan),
AbsoluteTimestampChanged(Timestamp, Timestamp),
}
impl ExcludeNewerValueChange {
pub fn is_relative_timestamp_change(&self) -> bool {
matches!(self, Self::RelativeTimestampChanged(_, _, _))
}
}
impl std::fmt::Display for ExcludeNewerValueChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SpanChanged(old, new) => {
write!(f, "change of exclude newer span from `{old}` to `{new}`")
}
Self::SpanAdded(span) => {
write!(f, "addition of exclude newer span `{span}`")
}
Self::SpanRemoved => {
write!(f, "removal of exclude newer span")
}
Self::RelativeTimestampChanged(old, new, span) => {
write!(
f,
"change of calculated ({span}) exclude newer timestamp from `{old}` to `{new}`"
)
}
Self::AbsoluteTimestampChanged(old, new) => {
write!(
f,
"change of exclude newer timestamp from `{old}` to `{new}`"
)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExcludeNewerChange {
GlobalChanged(ExcludeNewerValueChange),
GlobalAdded(ExcludeNewerValue),
GlobalRemoved,
Package(ExcludeNewerPackageChange),
}
impl ExcludeNewerChange {
pub fn is_relative_timestamp_change(&self) -> bool {
match self {
Self::GlobalChanged(change) => change.is_relative_timestamp_change(),
Self::GlobalAdded(_) | Self::GlobalRemoved => false,
Self::Package(change) => change.is_relative_timestamp_change(),
}
}
}
impl std::fmt::Display for ExcludeNewerChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GlobalChanged(change) => {
write!(f, "{change}")
}
Self::GlobalAdded(value) => {
write!(f, "addition of global exclude newer {value}")
}
Self::GlobalRemoved => write!(f, "removal of global exclude newer"),
Self::Package(change) => {
write!(f, "{change}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExcludeNewerPackageChange {
PackageAdded(PackageName, ExcludeNewerOverride),
PackageRemoved(PackageName),
PackageChanged(PackageName, Box<ExcludeNewerOverrideChange>),
}
impl ExcludeNewerPackageChange {
pub fn is_relative_timestamp_change(&self) -> bool {
match self {
Self::PackageAdded(_, _) | Self::PackageRemoved(_) => false,
Self::PackageChanged(_, change) => change.is_relative_timestamp_change(),
}
}
}
impl std::fmt::Display for ExcludeNewerPackageChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PackageAdded(name, ExcludeNewerOverride::Enabled(value)) => {
write!(
f,
"addition of exclude newer `{}` for package `{name}`",
value.as_ref()
)
}
Self::PackageAdded(name, ExcludeNewerOverride::Disabled) => {
write!(
f,
"addition of exclude newer exclusion for package `{name}`"
)
}
Self::PackageRemoved(name) => {
write!(f, "removal of exclude newer for package `{name}`")
}
Self::PackageChanged(name, change) => write!(f, "{change} for package `{name}`"),
}
}
}
fn compare_exclude_newer_value(
this: &ExcludeNewerValue,
other: &ExcludeNewerValue,
) -> Option<ExcludeNewerValueChange> {
match (this.span(), other.span()) {
(None, Some(span)) => Some(ExcludeNewerValueChange::SpanAdded(*span)),
(Some(_), None) => Some(ExcludeNewerValueChange::SpanRemoved),
(Some(self_span), Some(other_span)) if self_span != other_span => Some(
ExcludeNewerValueChange::SpanChanged(*self_span, *other_span),
),
(Some(_), Some(span)) if this.timestamp() != other.timestamp() => {
Some(ExcludeNewerValueChange::RelativeTimestampChanged(
this.timestamp(),
other.timestamp(),
*span,
))
}
(None, None) if this.timestamp() != other.timestamp() => Some(
ExcludeNewerValueChange::AbsoluteTimestampChanged(this.timestamp(), other.timestamp()),
),
(Some(_), Some(_)) | (None, None) => None,
}
}
pub struct ExcludeNewerValueWithSpanRef<'a>(pub &'a ExcludeNewerValue);
impl serde::Serialize for ExcludeNewerValueWithSpanRef<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if let Some(span) = self.0.span() {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("timestamp", &self.0.timestamp())?;
map.serialize_entry("span", span)?;
map.end()
} else {
self.0.timestamp().serialize(serializer)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExcludeNewerPackageEntry {
pub package: PackageName,
pub setting: ExcludeNewerOverride,
}
impl FromStr for ExcludeNewerPackageEntry {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((package, value)) = s.split_once('=') else {
return Err(format!(
"Invalid `exclude-newer-package` value `{s}`: expected format `PACKAGE=DATE` or `PACKAGE=false`"
));
};
let package = PackageName::from_str(package).map_err(|err| {
format!("Invalid `exclude-newer-package` package name `{package}`: {err}")
})?;
let setting = if value == "false" {
ExcludeNewerOverride::Disabled
} else {
ExcludeNewerOverride::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
|err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"),
)?))
};
Ok(Self { package, setting })
}
}
impl From<(PackageName, ExcludeNewerOverride)> for ExcludeNewerPackageEntry {
fn from((package, setting): (PackageName, ExcludeNewerOverride)) -> Self {
Self { package, setting }
}
}
impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry {
fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self {
Self {
package,
setting: ExcludeNewerOverride::Enabled(Box::new(timestamp)),
}
}
}
pub fn serialize_exclude_newer_package_with_spans<S>(
value: &Option<ExcludeNewerPackage>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let Some(value) = value else {
return serializer.serialize_none();
};
let mut map = serializer.serialize_map(Some(value.len()))?;
for (name, setting) in value {
match setting {
ExcludeNewerOverride::Disabled => map.serialize_entry(name, &false)?,
ExcludeNewerOverride::Enabled(value) => {
map.serialize_entry(name, &ExcludeNewerValueWithSpanRef(value.as_ref()))?;
}
}
}
map.end()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExcludeNewerOverrideChange {
Disabled { was: ExcludeNewerValue },
Enabled { now: ExcludeNewerValue },
TimestampChanged(ExcludeNewerValueChange),
}
impl ExcludeNewerOverrideChange {
pub fn is_relative_timestamp_change(&self) -> bool {
match self {
Self::Disabled { .. } | Self::Enabled { .. } => false,
Self::TimestampChanged(change) => change.is_relative_timestamp_change(),
}
}
}
impl std::fmt::Display for ExcludeNewerOverrideChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disabled { was } => {
write!(f, "add exclude newer exclusion (was `{was}`)")
}
Self::Enabled { now } => {
write!(f, "remove exclude newer exclusion (now `{now}`)")
}
Self::TimestampChanged(change) => write!(f, "{change}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExcludeNewerPackage(FxHashMap<PackageName, ExcludeNewerOverride>);
impl Deref for ExcludeNewerPackage {
type Target = FxHashMap<PackageName, ExcludeNewerOverride>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ExcludeNewerPackage {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<ExcludeNewerPackageEntry> for ExcludeNewerPackage {
fn from_iter<T: IntoIterator<Item = ExcludeNewerPackageEntry>>(iter: T) -> Self {
Self(
iter.into_iter()
.map(|entry| (entry.package, entry.setting))
.collect(),
)
}
}
impl IntoIterator for ExcludeNewerPackage {
type Item = (PackageName, ExcludeNewerOverride);
type IntoIter = std::collections::hash_map::IntoIter<PackageName, ExcludeNewerOverride>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a ExcludeNewerPackage {
type Item = (&'a PackageName, &'a ExcludeNewerOverride);
type IntoIter = std::collections::hash_map::Iter<'a, PackageName, ExcludeNewerOverride>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl ExcludeNewerPackage {
pub fn into_inner(self) -> FxHashMap<PackageName, ExcludeNewerOverride> {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn recompute(self) -> Self {
Self(
self.0
.into_iter()
.map(|(name, setting)| {
let setting = match setting {
ExcludeNewerOverride::Disabled => ExcludeNewerOverride::Disabled,
ExcludeNewerOverride::Enabled(value) => {
ExcludeNewerOverride::Enabled(Box::new((*value).recompute()))
}
};
(name, setting)
})
.collect(),
)
}
pub fn compare(&self, other: &Self) -> Option<ExcludeNewerPackageChange> {
for (package, setting) in self {
match (setting, other.get(package)) {
(
ExcludeNewerOverride::Enabled(self_timestamp),
Some(ExcludeNewerOverride::Enabled(other_timestamp)),
) => {
if let Some(change) =
compare_exclude_newer_value(self_timestamp, other_timestamp)
{
return Some(ExcludeNewerPackageChange::PackageChanged(
package.clone(),
Box::new(ExcludeNewerOverrideChange::TimestampChanged(change)),
));
}
}
(
ExcludeNewerOverride::Enabled(self_timestamp),
Some(ExcludeNewerOverride::Disabled),
) => {
return Some(ExcludeNewerPackageChange::PackageChanged(
package.clone(),
Box::new(ExcludeNewerOverrideChange::Disabled {
was: self_timestamp.as_ref().clone(),
}),
));
}
(
ExcludeNewerOverride::Disabled,
Some(ExcludeNewerOverride::Enabled(other_timestamp)),
) => {
return Some(ExcludeNewerPackageChange::PackageChanged(
package.clone(),
Box::new(ExcludeNewerOverrideChange::Enabled {
now: other_timestamp.as_ref().clone(),
}),
));
}
(ExcludeNewerOverride::Disabled, Some(ExcludeNewerOverride::Disabled)) => {}
(_, None) => {
return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone()));
}
}
}
for (package, value) in other {
if !self.contains_key(package) {
return Some(ExcludeNewerPackageChange::PackageAdded(
package.clone(),
value.clone(),
));
}
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExcludeNewer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub global: Option<ExcludeNewerValue>,
#[serde(default, skip_serializing_if = "FxHashMap::is_empty")]
pub package: ExcludeNewerPackage,
}
impl ExcludeNewer {
pub fn global(global: ExcludeNewerValue) -> Self {
Self {
global: Some(global),
package: ExcludeNewerPackage::default(),
}
}
fn warn_index_exclude_newer_preview() {
if !uv_preview::is_enabled(PreviewFeature::IndexExcludeNewer) {
warn_user_once!(
"Setting `exclude-newer` on configured indexes is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
PreviewFeature::IndexExcludeNewer
);
}
}
pub fn new(global: Option<ExcludeNewerValue>, package: ExcludeNewerPackage) -> Self {
Self { global, package }
}
pub fn from_args(
global: Option<ExcludeNewerValue>,
package: Vec<ExcludeNewerPackageEntry>,
) -> Self {
let package: ExcludeNewerPackage = package.into_iter().collect();
Self { global, package }
}
pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option<ExcludeNewerValue> {
match self.package.get(package_name) {
Some(ExcludeNewerOverride::Enabled(timestamp)) => Some(timestamp.as_ref().clone()),
Some(ExcludeNewerOverride::Disabled) => None,
None => self.global.clone(),
}
}
pub fn exclude_newer_package_for_index(
&self,
package_name: &PackageName,
index: Option<&ExcludeNewerOverride>,
) -> Option<ExcludeNewerValue> {
self.exclude_newer_package_for_index_with_source(package_name, index)
.map(|(exclude_newer, _)| exclude_newer)
}
pub(crate) fn exclude_newer_package_for_index_with_source(
&self,
package_name: &PackageName,
index: Option<&ExcludeNewerOverride>,
) -> Option<(ExcludeNewerValue, EffectiveExcludeNewerSource)> {
match self.package.get(package_name) {
Some(ExcludeNewerOverride::Enabled(timestamp)) => Some((
timestamp.as_ref().clone(),
EffectiveExcludeNewerSource::Package,
)),
Some(ExcludeNewerOverride::Disabled) => None,
None => match index {
Some(ExcludeNewerOverride::Disabled) => {
Self::warn_index_exclude_newer_preview();
None
}
Some(ExcludeNewerOverride::Enabled(timestamp)) => Some((
{
Self::warn_index_exclude_newer_preview();
ExcludeNewerValue::from(timestamp.timestamp())
},
EffectiveExcludeNewerSource::Index,
)),
None => self
.global
.clone()
.map(|timestamp| (timestamp, EffectiveExcludeNewerSource::Global)),
},
}
}
pub fn is_empty(&self) -> bool {
self.global.is_none() && self.package.is_empty()
}
#[must_use]
pub fn recompute(self) -> Self {
Self {
global: self.global.map(ExcludeNewerValue::recompute),
package: self.package.recompute(),
}
}
pub fn compare(&self, other: &Self) -> Option<ExcludeNewerChange> {
match (&self.global, &other.global) {
(Some(self_global), Some(other_global)) => {
if let Some(change) = compare_exclude_newer_value(self_global, other_global) {
return Some(ExcludeNewerChange::GlobalChanged(change));
}
}
(None, Some(global)) => {
return Some(ExcludeNewerChange::GlobalAdded(global.clone()));
}
(Some(_), None) => return Some(ExcludeNewerChange::GlobalRemoved),
(None, None) => (),
}
self.package
.compare(&other.package)
.map(ExcludeNewerChange::Package)
}
}
impl std::fmt::Display for ExcludeNewer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(global) = &self.global {
write!(f, "global: {global}")?;
if !self.package.is_empty() {
write!(f, ", ")?;
}
}
let mut first = true;
for (name, setting) in &self.package {
if !first {
write!(f, ", ")?;
}
match setting {
ExcludeNewerOverride::Enabled(timestamp) => {
write!(f, "{name}: {}", timestamp.as_ref())?;
}
ExcludeNewerOverride::Disabled => {
write!(f, "{name}: disabled")?;
}
}
first = false;
}
Ok(())
}
}