Move and rename exclude-newer override type (#18838)
## Summary We want to reuse this in `[[tool.uv.index]]` definitions, so it both (1) needs to be more generic (rather than `ExcludeNewerPackage`) and (2) needs to be accessible to more crates (so it's now in `uv-distribution-types`).
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
use std::borrow::Cow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone};
|
||||
use serde::Deserialize;
|
||||
use serde::de::value::MapAccessDeserializer;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExcludeNewerSpan(Span);
|
||||
|
||||
impl std::fmt::Display for ExcludeNewerSpan {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ExcludeNewerSpan {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.fieldwise() == other.0.fieldwise()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ExcludeNewerSpan {}
|
||||
|
||||
impl PartialOrd for ExcludeNewerSpan {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ExcludeNewerSpan {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.0.to_string().cmp(&other.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for ExcludeNewerSpan {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.0.to_string().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExcludeNewerSpan {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for ExcludeNewerSpan {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = <Cow<'_, str>>::deserialize(deserializer)?;
|
||||
let span: Span = s.parse().map_err(serde::de::Error::custom)?;
|
||||
Ok(Self(span))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ExcludeNewerValue {
|
||||
timestamp: Timestamp,
|
||||
span: Option<ExcludeNewerSpan>,
|
||||
}
|
||||
|
||||
impl ExcludeNewerValue {
|
||||
pub fn into_parts(self) -> (Timestamp, Option<ExcludeNewerSpan>) {
|
||||
(self.timestamp, self.span)
|
||||
}
|
||||
|
||||
/// Return the [`Timestamp`] in milliseconds.
|
||||
pub fn timestamp_millis(&self) -> i64 {
|
||||
self.timestamp.as_millisecond()
|
||||
}
|
||||
|
||||
/// Return the [`Timestamp`].
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// Return the [`ExcludeNewerSpan`] used to construct the [`Timestamp`], if any.
|
||||
pub fn span(&self) -> Option<&ExcludeNewerSpan> {
|
||||
self.span.as_ref()
|
||||
}
|
||||
|
||||
/// Create a new [`ExcludeNewerValue`].
|
||||
pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self {
|
||||
Self { timestamp, span }
|
||||
}
|
||||
|
||||
/// If this value was derived from a relative span, recompute the timestamp relative to now.
|
||||
///
|
||||
/// Returns `self` unchanged if there is no span (i.e., the timestamp is absolute).
|
||||
#[must_use]
|
||||
pub fn recompute(self) -> Self {
|
||||
let Some(span) = self.span else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
|
||||
test_time
|
||||
.parse::<Timestamp>()
|
||||
.expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
|
||||
.to_zoned(TimeZone::UTC)
|
||||
} else {
|
||||
Timestamp::now().to_zoned(TimeZone::UTC)
|
||||
};
|
||||
|
||||
let Ok(cutoff) = now.checked_sub(span.0.abs()) else {
|
||||
return Self {
|
||||
timestamp: self.timestamp,
|
||||
span: Some(span),
|
||||
};
|
||||
};
|
||||
|
||||
Self {
|
||||
timestamp: cutoff.into(),
|
||||
span: Some(span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExcludeNewerValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.timestamp.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for ExcludeNewerValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TableForm {
|
||||
timestamp: Timestamp,
|
||||
span: Option<ExcludeNewerSpan>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
String(String),
|
||||
Table(Box<TableForm>),
|
||||
}
|
||||
|
||||
match Helper::deserialize(deserializer)? {
|
||||
Helper::String(s) => Self::from_str(&s).map_err(serde::de::Error::custom),
|
||||
Helper::Table(table) => Ok(Self::new(table.timestamp, table.span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Timestamp> for ExcludeNewerValue {
|
||||
fn from(timestamp: Timestamp) -> Self {
|
||||
Self {
|
||||
timestamp,
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_exclude_newer_error(
|
||||
input: &str,
|
||||
date_err: &jiff::Error,
|
||||
span_err: &jiff::Error,
|
||||
) -> String {
|
||||
let trimmed = input.trim();
|
||||
|
||||
let after_sign = trimmed.trim_start_matches(['+', '-']);
|
||||
if after_sign.starts_with('P') || after_sign.starts_with('p') {
|
||||
return format!("`{input}` could not be parsed as an ISO 8601 duration: {span_err}");
|
||||
}
|
||||
|
||||
let after_sign_trimmed = after_sign.trim_start();
|
||||
let mut chars = after_sign_trimmed.chars().peekable();
|
||||
if chars.peek().is_some_and(char::is_ascii_digit) {
|
||||
while chars.peek().is_some_and(char::is_ascii_digit) {
|
||||
chars.next();
|
||||
}
|
||||
while chars.peek().is_some_and(|c| c.is_whitespace()) {
|
||||
chars.next();
|
||||
}
|
||||
if chars.peek().is_some_and(char::is_ascii_alphabetic) {
|
||||
return format!("`{input}` could not be parsed as a duration: {span_err}");
|
||||
}
|
||||
}
|
||||
|
||||
let mut chars = after_sign.chars();
|
||||
let looks_like_date = chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c == '-');
|
||||
|
||||
if looks_like_date {
|
||||
return format!("`{input}` could not be parsed as a valid date: {date_err}");
|
||||
}
|
||||
|
||||
format!(
|
||||
"`{input}` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`)"
|
||||
)
|
||||
}
|
||||
|
||||
impl FromStr for ExcludeNewerValue {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(timestamp) = input.parse::<Timestamp>() {
|
||||
return Ok(Self::new(timestamp, None));
|
||||
}
|
||||
|
||||
let date_err = match input.parse::<jiff::civil::Date>() {
|
||||
Ok(date) => {
|
||||
let timestamp = date
|
||||
.checked_add(1.day())
|
||||
.and_then(|date| date.to_zoned(TimeZone::system()))
|
||||
.map(|zdt| zdt.timestamp())
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"`{input}` parsed to date `{date}`, but could not be converted to a timestamp: {err}",
|
||||
)
|
||||
})?;
|
||||
return Ok(Self::new(timestamp, None));
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
let span_err = match input.parse::<Span>() {
|
||||
Ok(span) => {
|
||||
let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
|
||||
test_time
|
||||
.parse::<Timestamp>()
|
||||
.expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
|
||||
.to_zoned(TimeZone::UTC)
|
||||
} else {
|
||||
Timestamp::now().to_zoned(TimeZone::UTC)
|
||||
};
|
||||
|
||||
if span.get_years() != 0 {
|
||||
let years = span
|
||||
.total((Unit::Year, &now))
|
||||
.map(f64::ceil)
|
||||
.unwrap_or(1.0)
|
||||
.abs();
|
||||
let days = years * 365.0;
|
||||
return Err(format!(
|
||||
"Duration `{input}` uses unit 'years' which is not allowed; use days instead, e.g., `{days:.0} days`.",
|
||||
));
|
||||
}
|
||||
if span.get_months() != 0 {
|
||||
let months = span
|
||||
.total((Unit::Month, &now))
|
||||
.map(f64::ceil)
|
||||
.unwrap_or(1.0)
|
||||
.abs();
|
||||
let days = months * 30.0;
|
||||
return Err(format!(
|
||||
"Duration `{input}` uses 'months' which is not allowed; use days instead, e.g., `{days:.0} days`."
|
||||
));
|
||||
}
|
||||
|
||||
let cutoff = now.checked_sub(span.abs()).map_err(|err| {
|
||||
format!("Duration `{input}` is too large to subtract from current time: {err}")
|
||||
})?;
|
||||
return Ok(Self::new(cutoff.into(), Some(ExcludeNewerSpan(span))));
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
Err(format_exclude_newer_error(input, &date_err, &span_err))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExcludeNewerValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.timestamp.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for ExcludeNewerValue {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("ExcludeNewerValue")
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`), as well as relative durations (e.g., `1 week`, `30 days`, `6 months`). Relative durations are resolved to a timestamp at lock time.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `exclude-newer` is disabled or enabled with an explicit cutoff.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ExcludeNewerOverride {
|
||||
/// Disable exclude-newer (allow all versions regardless of upload date).
|
||||
Disabled,
|
||||
/// Enable exclude-newer with this cutoff.
|
||||
Enabled(Box<ExcludeNewerValue>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for ExcludeNewerOverride {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("ExcludeNewerOverride")
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"const": false,
|
||||
"description": "Disable exclude-newer."
|
||||
},
|
||||
generator.subschema_for::<ExcludeNewerValue>(),
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for ExcludeNewerOverride {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = ExcludeNewerOverride;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(
|
||||
"a date/timestamp/duration string, false to disable exclude-newer, or a table \
|
||||
with timestamp/span",
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
ExcludeNewerValue::from_str(v)
|
||||
.map(|ts| ExcludeNewerOverride::Enabled(Box::new(ts)))
|
||||
.map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}")))
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if v {
|
||||
Err(E::custom(
|
||||
"expected false to disable exclude-newer, got true",
|
||||
))
|
||||
} else {
|
||||
Ok(ExcludeNewerOverride::Disabled)
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
Ok(ExcludeNewerOverride::Enabled(Box::new(
|
||||
ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExcludeNewerOverride {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer),
|
||||
Self::Disabled => serializer.serialize_bool(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ pub use crate::dependency_metadata::*;
|
||||
pub use crate::diagnostic::*;
|
||||
pub use crate::dist_error::*;
|
||||
pub use crate::error::*;
|
||||
pub use crate::exclude_newer::*;
|
||||
pub use crate::file::*;
|
||||
pub use crate::hash::*;
|
||||
pub use crate::id::*;
|
||||
@@ -94,6 +95,7 @@ mod dependency_metadata;
|
||||
mod diagnostic;
|
||||
mod dist_error;
|
||||
mod error;
|
||||
mod exclude_newer;
|
||||
mod file;
|
||||
mod hash;
|
||||
mod id;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use jiff::{Span, Timestamp, ToSpan, Unit, tz::TimeZone};
|
||||
use jiff::Timestamp;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
use serde::de::value::MapAccessDeserializer;
|
||||
use serde::ser::SerializeMap;
|
||||
use uv_distribution_types::{ExcludeNewerOverride, ExcludeNewerSpan, ExcludeNewerValue};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -97,9 +95,9 @@ impl std::fmt::Display for ExcludeNewerChange {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ExcludeNewerPackageChange {
|
||||
PackageAdded(PackageName, PackageExcludeNewer),
|
||||
PackageAdded(PackageName, ExcludeNewerOverride),
|
||||
PackageRemoved(PackageName),
|
||||
PackageChanged(PackageName, Box<PackageExcludeNewerChange>),
|
||||
PackageChanged(PackageName, Box<ExcludeNewerOverrideChange>),
|
||||
}
|
||||
|
||||
impl ExcludeNewerPackageChange {
|
||||
@@ -114,14 +112,14 @@ impl ExcludeNewerPackageChange {
|
||||
impl std::fmt::Display for ExcludeNewerPackageChange {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::PackageAdded(name, PackageExcludeNewer::Enabled(value)) => {
|
||||
Self::PackageAdded(name, ExcludeNewerOverride::Enabled(value)) => {
|
||||
write!(
|
||||
f,
|
||||
"addition of exclude newer `{}` for package `{name}`",
|
||||
value.as_ref()
|
||||
)
|
||||
}
|
||||
Self::PackageAdded(name, PackageExcludeNewer::Disabled) => {
|
||||
Self::PackageAdded(name, ExcludeNewerOverride::Disabled) => {
|
||||
write!(
|
||||
f,
|
||||
"addition of exclude newer exclusion for package `{name}`"
|
||||
@@ -134,88 +132,28 @@ impl std::fmt::Display for ExcludeNewerPackageChange {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A timestamp that excludes files newer than it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExcludeNewerValue {
|
||||
/// The resolved timestamp.
|
||||
timestamp: Timestamp,
|
||||
/// The span used to derive the [`Timestamp`], if any.
|
||||
span: Option<ExcludeNewerSpan>,
|
||||
}
|
||||
|
||||
impl ExcludeNewerValue {
|
||||
pub fn into_parts(self) -> (Timestamp, Option<ExcludeNewerSpan>) {
|
||||
(self.timestamp, self.span)
|
||||
}
|
||||
|
||||
pub fn compare(&self, other: &Self) -> Option<ExcludeNewerValueChange> {
|
||||
match (&self.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 self.timestamp != other.timestamp => {
|
||||
Some(ExcludeNewerValueChange::RelativeTimestampChanged(
|
||||
self.timestamp,
|
||||
other.timestamp,
|
||||
*span,
|
||||
))
|
||||
}
|
||||
(None, None) if self.timestamp != other.timestamp => Some(
|
||||
ExcludeNewerValueChange::AbsoluteTimestampChanged(self.timestamp, other.timestamp),
|
||||
),
|
||||
(Some(_), Some(_)) | (None, None) => None,
|
||||
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,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ExcludeNewerSpan(Span);
|
||||
|
||||
impl std::fmt::Display for ExcludeNewerSpan {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ExcludeNewerSpan {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.fieldwise() == other.0.fieldwise()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ExcludeNewerSpan {}
|
||||
|
||||
impl serde::Serialize for ExcludeNewerSpan {
|
||||
/// Serialize to an ISO 8601 duration string.
|
||||
///
|
||||
/// We use ISO 8601 format for serialization (rather than the "friendly" format).
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for ExcludeNewerSpan {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = <Cow<'_, str>>::deserialize(deserializer)?;
|
||||
let span: Span = s.parse().map_err(serde::de::Error::custom)?;
|
||||
Ok(Self(span))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for ExcludeNewerValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.timestamp.serialize(serializer)
|
||||
(None, None) if this.timestamp() != other.timestamp() => Some(
|
||||
ExcludeNewerValueChange::AbsoluteTimestampChanged(this.timestamp(), other.timestamp()),
|
||||
),
|
||||
(Some(_), Some(_)) | (None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,299 +175,12 @@ impl serde::Serialize for ExcludeNewerValueWithSpanRef<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for ExcludeNewerValue {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
// Support both a simple string ("2024-03-11T00:00:00Z") and a table
|
||||
// ({ timestamp = "2024-03-11T00:00:00Z", span = "P2W" })
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TableForm {
|
||||
timestamp: Timestamp,
|
||||
span: Option<ExcludeNewerSpan>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
String(String),
|
||||
Table(Box<TableForm>),
|
||||
}
|
||||
|
||||
match Helper::deserialize(deserializer)? {
|
||||
Helper::String(s) => Self::from_str(&s).map_err(serde::de::Error::custom),
|
||||
Helper::Table(table) => Ok(Self::new(table.timestamp, table.span)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExcludeNewerValue {
|
||||
/// Return the [`Timestamp`] in milliseconds.
|
||||
pub fn timestamp_millis(&self) -> i64 {
|
||||
self.timestamp.as_millisecond()
|
||||
}
|
||||
|
||||
/// Return the [`Timestamp`].
|
||||
pub fn timestamp(&self) -> Timestamp {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// Return the [`ExcludeNewerSpan`] used to construct the [`Timestamp`], if any.
|
||||
pub fn span(&self) -> Option<&ExcludeNewerSpan> {
|
||||
self.span.as_ref()
|
||||
}
|
||||
|
||||
/// Create a new [`ExcludeNewerValue`].
|
||||
pub fn new(timestamp: Timestamp, span: Option<ExcludeNewerSpan>) -> Self {
|
||||
Self { timestamp, span }
|
||||
}
|
||||
|
||||
/// If this value was derived from a relative span, recompute the timestamp relative to now.
|
||||
///
|
||||
/// Returns `self` unchanged if there is no span (i.e., the timestamp is absolute).
|
||||
#[must_use]
|
||||
pub fn recompute(self) -> Self {
|
||||
let Some(span) = self.span else {
|
||||
return self;
|
||||
};
|
||||
|
||||
let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
|
||||
test_time
|
||||
.parse::<Timestamp>()
|
||||
.expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
|
||||
.to_zoned(TimeZone::UTC)
|
||||
} else {
|
||||
Timestamp::now().to_zoned(TimeZone::UTC)
|
||||
};
|
||||
|
||||
let Ok(cutoff) = now.checked_sub(span.0.abs()) else {
|
||||
return Self {
|
||||
timestamp: self.timestamp,
|
||||
span: Some(span),
|
||||
};
|
||||
};
|
||||
|
||||
Self {
|
||||
timestamp: cutoff.into(),
|
||||
span: Some(span),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Timestamp> for ExcludeNewerValue {
|
||||
fn from(timestamp: Timestamp) -> Self {
|
||||
Self {
|
||||
timestamp,
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine what format the user likely intended and return an appropriate error message.
|
||||
fn format_exclude_newer_error(
|
||||
input: &str,
|
||||
date_err: &jiff::Error,
|
||||
span_err: &jiff::Error,
|
||||
) -> String {
|
||||
let trimmed = input.trim();
|
||||
|
||||
// Check for ISO 8601 duration (`[-+]?[Pp]`), e.g., "P2W", "+P1D", "-P30D"
|
||||
let after_sign = trimmed.trim_start_matches(['+', '-']);
|
||||
if after_sign.starts_with('P') || after_sign.starts_with('p') {
|
||||
return format!("`{input}` could not be parsed as an ISO 8601 duration: {span_err}");
|
||||
}
|
||||
|
||||
// Check for friendly duration (`[-+]?\s*[0-9]+\s*[A-Za-z]`), e.g., "2 weeks", "-30 days",
|
||||
// "1hour"
|
||||
let after_sign_trimmed = after_sign.trim_start();
|
||||
let mut chars = after_sign_trimmed.chars().peekable();
|
||||
|
||||
// Check if we start with a digit
|
||||
if chars.peek().is_some_and(char::is_ascii_digit) {
|
||||
// Skip digits
|
||||
while chars.peek().is_some_and(char::is_ascii_digit) {
|
||||
chars.next();
|
||||
}
|
||||
// Skip optional whitespace
|
||||
while chars.peek().is_some_and(|c| c.is_whitespace()) {
|
||||
chars.next();
|
||||
}
|
||||
// Check if next character is a letter (unit designator)
|
||||
if chars.peek().is_some_and(char::is_ascii_alphabetic) {
|
||||
return format!("`{input}` could not be parsed as a duration: {span_err}");
|
||||
}
|
||||
}
|
||||
|
||||
// Check for date/timestamp (`[-+]?[0-9]{4}-`), e.g., "2024-01-01", "2024-01-01T00:00:00Z"
|
||||
let mut chars = after_sign.chars();
|
||||
let looks_like_date = chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c.is_ascii_digit())
|
||||
&& chars.next().is_some_and(|c| c == '-');
|
||||
|
||||
if looks_like_date {
|
||||
return format!("`{input}` could not be parsed as a valid date: {date_err}");
|
||||
}
|
||||
|
||||
// If we can't tell, return a generic error message
|
||||
format!(
|
||||
"`{input}` could not be parsed as a valid exclude-newer value (expected a date like `2024-01-01`, a timestamp like `2024-01-01T00:00:00Z`, or a duration like `3 days` or `P3D`)"
|
||||
)
|
||||
}
|
||||
|
||||
impl FromStr for ExcludeNewerValue {
|
||||
type Err = String;
|
||||
|
||||
/// Parse an [`ExcludeNewerValue`] from a string.
|
||||
///
|
||||
/// Accepts RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`), local dates in the same format
|
||||
/// (e.g., `2006-12-02`), "friendly" durations (e.g., `1 week`, `30 days`), and ISO 8601
|
||||
/// durations (e.g., `PT24H`, `P7D`, `P30D`).
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
// Try parsing as a timestamp first
|
||||
if let Ok(timestamp) = input.parse::<Timestamp>() {
|
||||
return Ok(Self::new(timestamp, None));
|
||||
}
|
||||
|
||||
// Try parsing as a date
|
||||
// In Jiff, if an RFC 3339 timestamp could be parsed, then it must necessarily be the case
|
||||
// that a date can also be parsed. So we can collapse the error cases here. That is, if we
|
||||
// fail to parse a timestamp and a date, then it should be sufficient to just report the
|
||||
// error from parsing the date. If someone tried to write a timestamp but committed an error
|
||||
// in the non-date portion, the date parsing below will still report a holistic error that
|
||||
// will make sense to the user. (I added a snapshot test for that case.)
|
||||
let date_err = match input.parse::<jiff::civil::Date>() {
|
||||
Ok(date) => {
|
||||
let timestamp = date
|
||||
.checked_add(1.day())
|
||||
.and_then(|date| date.to_zoned(TimeZone::system()))
|
||||
.map(|zdt| zdt.timestamp())
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"`{input}` parsed to date `{date}`, but could not \
|
||||
be converted to a timestamp: {err}",
|
||||
)
|
||||
})?;
|
||||
return Ok(Self::new(timestamp, None));
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
// Try parsing as a span
|
||||
let span_err = match input.parse::<Span>() {
|
||||
Ok(span) => {
|
||||
// Allow overriding the current time in tests for deterministic snapshots
|
||||
let now = if let Ok(test_time) = std::env::var("UV_TEST_CURRENT_TIMESTAMP") {
|
||||
test_time
|
||||
.parse::<Timestamp>()
|
||||
.expect("UV_TEST_CURRENT_TIMESTAMP must be a valid RFC 3339 timestamp")
|
||||
.to_zoned(TimeZone::UTC)
|
||||
} else {
|
||||
Timestamp::now().to_zoned(TimeZone::UTC)
|
||||
};
|
||||
|
||||
// We do not allow years and months as units, as the amount of time they represent
|
||||
// is not fixed and can differ depending on the local time zone. We could allow this
|
||||
// via the CLI in the future, but shouldn't allow it via persistent configuration.
|
||||
if span.get_years() != 0 {
|
||||
let years = span
|
||||
.total((Unit::Year, &now))
|
||||
.map(f64::ceil)
|
||||
.unwrap_or(1.0)
|
||||
.abs();
|
||||
let days = years * 365.0;
|
||||
return Err(format!(
|
||||
"Duration `{input}` uses unit 'years' which is not allowed; use days instead, e.g., `{days:.0} days`.",
|
||||
));
|
||||
}
|
||||
if span.get_months() != 0 {
|
||||
let months = span
|
||||
.total((Unit::Month, &now))
|
||||
.map(f64::ceil)
|
||||
.unwrap_or(1.0)
|
||||
.abs();
|
||||
let days = months * 30.0;
|
||||
return Err(format!(
|
||||
"Duration `{input}` uses 'months' which is not allowed; use days instead, e.g., `{days:.0} days`."
|
||||
));
|
||||
}
|
||||
|
||||
// We're using a UTC timezone so there are no transitions (e.g., DST) and days are
|
||||
// always 24 hours. This means that we can also allow weeks as a unit.
|
||||
//
|
||||
// Note we use `span.abs()` so `1 day ago` has the same effect as `1 day` instead
|
||||
// of resulting in a future date.
|
||||
let cutoff = now.checked_sub(span.abs()).map_err(|err| {
|
||||
format!("Duration `{input}` is too large to subtract from current time: {err}")
|
||||
})?;
|
||||
|
||||
return Ok(Self::new(cutoff.into(), Some(ExcludeNewerSpan(span))));
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
// Return a targeted error message based on heuristics about what the user likely intended
|
||||
Err(format_exclude_newer_error(input, &date_err, &span_err))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExcludeNewerValue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.timestamp.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-package exclude-newer setting.
|
||||
///
|
||||
/// This enum represents whether exclude-newer should be disabled for a package,
|
||||
/// or if a specific cutoff (absolute or relative) should be used.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PackageExcludeNewer {
|
||||
/// Disable exclude-newer for this package (allow all versions regardless of upload date).
|
||||
Disabled,
|
||||
/// Enable exclude-newer with this cutoff for this package.
|
||||
Enabled(Box<ExcludeNewerValue>),
|
||||
}
|
||||
|
||||
impl PackageExcludeNewer {
|
||||
/// Recompute the relative span timestamp relative to the current time, if applicable.
|
||||
#[must_use]
|
||||
pub fn recompute(self) -> Self {
|
||||
match self {
|
||||
Self::Disabled => Self::Disabled,
|
||||
Self::Enabled(value) => Self::Enabled(Box::new((*value).recompute())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for PackageExcludeNewer {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("PackageExcludeNewer")
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"const": false,
|
||||
"description": "Disable exclude-newer for this package."
|
||||
},
|
||||
generator.subschema_for::<ExcludeNewerValue>(),
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A package-specific exclude-newer entry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ExcludeNewerPackageEntry {
|
||||
pub package: PackageName,
|
||||
pub setting: PackageExcludeNewer,
|
||||
pub setting: ExcludeNewerOverride,
|
||||
}
|
||||
|
||||
impl FromStr for ExcludeNewerPackageEntry {
|
||||
@@ -548,9 +199,9 @@ impl FromStr for ExcludeNewerPackageEntry {
|
||||
})?;
|
||||
|
||||
let setting = if value == "false" {
|
||||
PackageExcludeNewer::Disabled
|
||||
ExcludeNewerOverride::Disabled
|
||||
} else {
|
||||
PackageExcludeNewer::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
|
||||
ExcludeNewerOverride::Enabled(Box::new(ExcludeNewerValue::from_str(value).map_err(
|
||||
|err| format!("Invalid `exclude-newer-package` value `{value}`: {err}"),
|
||||
)?))
|
||||
};
|
||||
@@ -559,8 +210,8 @@ impl FromStr for ExcludeNewerPackageEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(PackageName, PackageExcludeNewer)> for ExcludeNewerPackageEntry {
|
||||
fn from((package, setting): (PackageName, PackageExcludeNewer)) -> Self {
|
||||
impl From<(PackageName, ExcludeNewerOverride)> for ExcludeNewerPackageEntry {
|
||||
fn from((package, setting): (PackageName, ExcludeNewerOverride)) -> Self {
|
||||
Self { package, setting }
|
||||
}
|
||||
}
|
||||
@@ -569,72 +220,7 @@ impl From<(PackageName, ExcludeNewerValue)> for ExcludeNewerPackageEntry {
|
||||
fn from((package, timestamp): (PackageName, ExcludeNewerValue)) -> Self {
|
||||
Self {
|
||||
package,
|
||||
setting: PackageExcludeNewer::Enabled(Box::new(timestamp)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for PackageExcludeNewer {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct Visitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||
type Value = PackageExcludeNewer;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str(
|
||||
"a date/timestamp/duration string, false to disable exclude-newer, or a table \
|
||||
with timestamp/span",
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
ExcludeNewerValue::from_str(v)
|
||||
.map(|ts| PackageExcludeNewer::Enabled(Box::new(ts)))
|
||||
.map_err(|e| E::custom(format!("failed to parse exclude-newer value: {e}")))
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
if v {
|
||||
Err(E::custom(
|
||||
"expected false to disable exclude-newer, got true",
|
||||
))
|
||||
} else {
|
||||
Ok(PackageExcludeNewer::Disabled)
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
Ok(PackageExcludeNewer::Enabled(Box::new(
|
||||
ExcludeNewerValue::deserialize(MapAccessDeserializer::new(map))?,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for PackageExcludeNewer {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Enabled(timestamp) => timestamp.to_string().serialize(serializer),
|
||||
Self::Disabled => serializer.serialize_bool(false),
|
||||
setting: ExcludeNewerOverride::Enabled(Box::new(timestamp)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -653,8 +239,8 @@ where
|
||||
let mut map = serializer.serialize_map(Some(value.len()))?;
|
||||
for (name, setting) in value {
|
||||
match setting {
|
||||
PackageExcludeNewer::Disabled => map.serialize_entry(name, &false)?,
|
||||
PackageExcludeNewer::Enabled(value) => {
|
||||
ExcludeNewerOverride::Disabled => map.serialize_entry(name, &false)?,
|
||||
ExcludeNewerOverride::Enabled(value) => {
|
||||
map.serialize_entry(name, &ExcludeNewerValueWithSpanRef(value.as_ref()))?;
|
||||
}
|
||||
}
|
||||
@@ -663,13 +249,13 @@ where
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PackageExcludeNewerChange {
|
||||
pub enum ExcludeNewerOverrideChange {
|
||||
Disabled { was: ExcludeNewerValue },
|
||||
Enabled { now: ExcludeNewerValue },
|
||||
TimestampChanged(ExcludeNewerValueChange),
|
||||
}
|
||||
|
||||
impl PackageExcludeNewerChange {
|
||||
impl ExcludeNewerOverrideChange {
|
||||
pub fn is_relative_timestamp_change(&self) -> bool {
|
||||
match self {
|
||||
Self::Disabled { .. } | Self::Enabled { .. } => false,
|
||||
@@ -678,7 +264,7 @@ impl PackageExcludeNewerChange {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackageExcludeNewerChange {
|
||||
impl std::fmt::Display for ExcludeNewerOverrideChange {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Disabled { was } => {
|
||||
@@ -694,10 +280,10 @@ impl std::fmt::Display for PackageExcludeNewerChange {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ExcludeNewerPackage(FxHashMap<PackageName, PackageExcludeNewer>);
|
||||
pub struct ExcludeNewerPackage(FxHashMap<PackageName, ExcludeNewerOverride>);
|
||||
|
||||
impl Deref for ExcludeNewerPackage {
|
||||
type Target = FxHashMap<PackageName, PackageExcludeNewer>;
|
||||
type Target = FxHashMap<PackageName, ExcludeNewerOverride>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
@@ -721,8 +307,8 @@ impl FromIterator<ExcludeNewerPackageEntry> for ExcludeNewerPackage {
|
||||
}
|
||||
|
||||
impl IntoIterator for ExcludeNewerPackage {
|
||||
type Item = (PackageName, PackageExcludeNewer);
|
||||
type IntoIter = std::collections::hash_map::IntoIter<PackageName, PackageExcludeNewer>;
|
||||
type Item = (PackageName, ExcludeNewerOverride);
|
||||
type IntoIter = std::collections::hash_map::IntoIter<PackageName, ExcludeNewerOverride>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
@@ -730,8 +316,8 @@ impl IntoIterator for ExcludeNewerPackage {
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a ExcludeNewerPackage {
|
||||
type Item = (&'a PackageName, &'a PackageExcludeNewer);
|
||||
type IntoIter = std::collections::hash_map::Iter<'a, PackageName, PackageExcludeNewer>;
|
||||
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()
|
||||
@@ -740,7 +326,7 @@ impl<'a> IntoIterator for &'a ExcludeNewerPackage {
|
||||
|
||||
impl ExcludeNewerPackage {
|
||||
/// Convert to the inner `HashMap`.
|
||||
pub fn into_inner(self) -> FxHashMap<PackageName, PackageExcludeNewer> {
|
||||
pub fn into_inner(self) -> FxHashMap<PackageName, ExcludeNewerOverride> {
|
||||
self.0
|
||||
}
|
||||
|
||||
@@ -755,7 +341,15 @@ impl ExcludeNewerPackage {
|
||||
Self(
|
||||
self.0
|
||||
.into_iter()
|
||||
.map(|(name, setting)| (name, setting.recompute()))
|
||||
.map(|(name, setting)| {
|
||||
let setting = match setting {
|
||||
ExcludeNewerOverride::Disabled => ExcludeNewerOverride::Disabled,
|
||||
ExcludeNewerOverride::Enabled(value) => {
|
||||
ExcludeNewerOverride::Enabled(Box::new((*value).recompute()))
|
||||
}
|
||||
};
|
||||
(name, setting)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -764,39 +358,41 @@ impl ExcludeNewerPackage {
|
||||
for (package, setting) in self {
|
||||
match (setting, other.get(package)) {
|
||||
(
|
||||
PackageExcludeNewer::Enabled(self_timestamp),
|
||||
Some(PackageExcludeNewer::Enabled(other_timestamp)),
|
||||
ExcludeNewerOverride::Enabled(self_timestamp),
|
||||
Some(ExcludeNewerOverride::Enabled(other_timestamp)),
|
||||
) => {
|
||||
if let Some(change) = self_timestamp.compare(other_timestamp) {
|
||||
if let Some(change) =
|
||||
compare_exclude_newer_value(self_timestamp, other_timestamp)
|
||||
{
|
||||
return Some(ExcludeNewerPackageChange::PackageChanged(
|
||||
package.clone(),
|
||||
Box::new(PackageExcludeNewerChange::TimestampChanged(change)),
|
||||
Box::new(ExcludeNewerOverrideChange::TimestampChanged(change)),
|
||||
));
|
||||
}
|
||||
}
|
||||
(
|
||||
PackageExcludeNewer::Enabled(self_timestamp),
|
||||
Some(PackageExcludeNewer::Disabled),
|
||||
ExcludeNewerOverride::Enabled(self_timestamp),
|
||||
Some(ExcludeNewerOverride::Disabled),
|
||||
) => {
|
||||
return Some(ExcludeNewerPackageChange::PackageChanged(
|
||||
package.clone(),
|
||||
Box::new(PackageExcludeNewerChange::Disabled {
|
||||
Box::new(ExcludeNewerOverrideChange::Disabled {
|
||||
was: self_timestamp.as_ref().clone(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
(
|
||||
PackageExcludeNewer::Disabled,
|
||||
Some(PackageExcludeNewer::Enabled(other_timestamp)),
|
||||
ExcludeNewerOverride::Disabled,
|
||||
Some(ExcludeNewerOverride::Enabled(other_timestamp)),
|
||||
) => {
|
||||
return Some(ExcludeNewerPackageChange::PackageChanged(
|
||||
package.clone(),
|
||||
Box::new(PackageExcludeNewerChange::Enabled {
|
||||
Box::new(ExcludeNewerOverrideChange::Enabled {
|
||||
now: other_timestamp.as_ref().clone(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
(PackageExcludeNewer::Disabled, Some(PackageExcludeNewer::Disabled)) => {}
|
||||
(ExcludeNewerOverride::Disabled, Some(ExcludeNewerOverride::Disabled)) => {}
|
||||
(_, None) => {
|
||||
return Some(ExcludeNewerPackageChange::PackageRemoved(package.clone()));
|
||||
}
|
||||
@@ -858,8 +454,8 @@ impl ExcludeNewer {
|
||||
/// exclude-newer is configured.
|
||||
pub fn exclude_newer_package(&self, package_name: &PackageName) -> Option<ExcludeNewerValue> {
|
||||
match self.package.get(package_name) {
|
||||
Some(PackageExcludeNewer::Enabled(timestamp)) => Some(timestamp.as_ref().clone()),
|
||||
Some(PackageExcludeNewer::Disabled) => None,
|
||||
Some(ExcludeNewerOverride::Enabled(timestamp)) => Some(timestamp.as_ref().clone()),
|
||||
Some(ExcludeNewerOverride::Disabled) => None,
|
||||
None => self.global.clone(),
|
||||
}
|
||||
}
|
||||
@@ -883,7 +479,7 @@ impl ExcludeNewer {
|
||||
pub fn compare(&self, other: &Self) -> Option<ExcludeNewerChange> {
|
||||
match (&self.global, &other.global) {
|
||||
(Some(self_global), Some(other_global)) => {
|
||||
if let Some(change) = self_global.compare(other_global) {
|
||||
if let Some(change) = compare_exclude_newer_value(self_global, other_global) {
|
||||
return Some(ExcludeNewerChange::GlobalChanged(change));
|
||||
}
|
||||
}
|
||||
@@ -913,10 +509,10 @@ impl std::fmt::Display for ExcludeNewer {
|
||||
write!(f, ", ")?;
|
||||
}
|
||||
match setting {
|
||||
PackageExcludeNewer::Enabled(timestamp) => {
|
||||
ExcludeNewerOverride::Enabled(timestamp) => {
|
||||
write!(f, "{name}: {}", timestamp.as_ref())?;
|
||||
}
|
||||
PackageExcludeNewer::Disabled => {
|
||||
ExcludeNewerOverride::Disabled => {
|
||||
write!(f, "{name}: disabled")?;
|
||||
}
|
||||
}
|
||||
@@ -925,17 +521,3 @@ impl std::fmt::Display for ExcludeNewer {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
impl schemars::JsonSchema for ExcludeNewerValue {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Cow::Borrowed("ExcludeNewerValue")
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
|
||||
schemars::json_schema!({
|
||||
"type": "string",
|
||||
"description": "Exclude distributions uploaded after the given timestamp.\n\nAccepts both RFC 3339 timestamps (e.g., `2006-12-02T02:07:43Z`) and local dates in the same format (e.g., `2006-12-02`), as well as relative durations (e.g., `1 week`, `30 days`, `6 months`). Relative durations are resolved to a timestamp at lock time.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
pub use dependency_mode::DependencyMode;
|
||||
pub use error::{ErrorTree, NoSolutionError, NoSolutionHeader, ResolveError, SentinelRange};
|
||||
pub use exclude_newer::{
|
||||
ExcludeNewer, ExcludeNewerChange, ExcludeNewerPackage, ExcludeNewerPackageChange,
|
||||
ExcludeNewerPackageEntry, ExcludeNewerSpan, ExcludeNewerValue, ExcludeNewerValueChange,
|
||||
ExcludeNewerValueWithSpanRef, PackageExcludeNewer, PackageExcludeNewerChange,
|
||||
serialize_exclude_newer_package_with_spans,
|
||||
ExcludeNewer, ExcludeNewerChange, ExcludeNewerOverrideChange, ExcludeNewerPackage,
|
||||
ExcludeNewerPackageChange, ExcludeNewerPackageEntry, ExcludeNewerValueChange,
|
||||
ExcludeNewerValueWithSpanRef, serialize_exclude_newer_package_with_spans,
|
||||
};
|
||||
pub use exclusions::Exclusions;
|
||||
pub use flat_index::{FlatDistributions, FlatIndex};
|
||||
@@ -30,6 +29,7 @@ pub use resolver::{
|
||||
};
|
||||
pub use universal_marker::{ConflictMarker, UniversalMarker};
|
||||
pub use upgrade::UpgradePackages;
|
||||
pub use uv_distribution_types::{ExcludeNewerOverride, ExcludeNewerSpan, ExcludeNewerValue};
|
||||
pub use version_map::VersionMap;
|
||||
pub use yanks::AllowedYanks;
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ use uv_small_str::SmallString;
|
||||
use uv_types::{BuildContext, HashStrategy};
|
||||
use uv_workspace::{Editability, WorkspaceMember};
|
||||
|
||||
use crate::exclude_newer::ExcludeNewerSpan;
|
||||
use crate::fork_strategy::ForkStrategy;
|
||||
pub(crate) use crate::lock::export::PylockTomlPackage;
|
||||
pub use crate::lock::export::RequirementsTxtExport;
|
||||
@@ -65,8 +64,8 @@ pub use crate::lock::tree::TreeDisplay;
|
||||
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
|
||||
use crate::universal_marker::{ConflictMarker, UniversalMarker};
|
||||
use crate::{
|
||||
ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, InMemoryIndex, MetadataResponse,
|
||||
PackageExcludeNewer, PrereleaseMode, ResolutionMode, ResolverOutput,
|
||||
ExcludeNewer, ExcludeNewerOverride, ExcludeNewerPackage, ExcludeNewerSpan, ExcludeNewerValue,
|
||||
InMemoryIndex, MetadataResponse, PrereleaseMode, ResolutionMode, ResolverOutput,
|
||||
};
|
||||
|
||||
mod export;
|
||||
@@ -1173,7 +1172,7 @@ impl Lock {
|
||||
let mut package_table = toml_edit::Table::new();
|
||||
for (name, setting) in &exclude_newer.package {
|
||||
match setting {
|
||||
PackageExcludeNewer::Enabled(exclude_newer_value) => {
|
||||
ExcludeNewerOverride::Enabled(exclude_newer_value) => {
|
||||
if let Some(span) = exclude_newer_value.span() {
|
||||
// Serialize as inline table with timestamp and span
|
||||
let mut inline = toml_edit::InlineTable::new();
|
||||
@@ -1191,7 +1190,7 @@ impl Lock {
|
||||
);
|
||||
}
|
||||
}
|
||||
PackageExcludeNewer::Disabled => {
|
||||
ExcludeNewerOverride::Disabled => {
|
||||
package_table.insert(name.as_ref(), value(false));
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+13
-13
@@ -838,10 +838,22 @@
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"ExcludeNewerOverride": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Disable exclude-newer.",
|
||||
"type": "boolean",
|
||||
"const": false
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ExcludeNewerValue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExcludeNewerPackage": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/PackageExcludeNewer"
|
||||
"$ref": "#/definitions/ExcludeNewerOverride"
|
||||
}
|
||||
},
|
||||
"ExcludeNewerValue": {
|
||||
@@ -1136,18 +1148,6 @@
|
||||
"$ref": "#/definitions/ConfigSettings"
|
||||
}
|
||||
},
|
||||
"PackageExcludeNewer": {
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Disable exclude-newer for this package.",
|
||||
"type": "boolean",
|
||||
"const": false
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ExcludeNewerValue"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PackageName": {
|
||||
"description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses runs of `-`, `_`, and `.` down to a single `-`.\nFor example, `---`, `.`, and `__` are all converted to a single `-`.\n\nSee: <https://packaging.python.org/en/latest/specifications/name-normalization/>",
|
||||
"type": "string"
|
||||
|
||||
Reference in New Issue
Block a user