use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use uv_cache_key::{CanonicalUrl, RepositoryUrl};
use uv_git_types::GitUrl;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_pypi_types::{
HashDigest, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl,
};
use uv_redacted::DisplaySafeUrl;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PackageId {
Name(PackageName),
Url(CanonicalUrl),
}
impl PackageId {
pub fn from_registry(name: PackageName) -> Self {
Self::Name(name)
}
pub fn from_url(url: &DisplaySafeUrl) -> Self {
Self::Url(CanonicalUrl::new(url))
}
}
impl Display for PackageId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Name(name) => write!(f, "{name}"),
Self::Url(url) => write!(f, "{url}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum VersionId {
NameVersion(PackageName, Version),
ArchiveUrl {
location: CanonicalUrl,
subdirectory: Option<PathBuf>,
},
Git {
url: GitUrl,
subdirectory: Option<PathBuf>,
},
Path(PathBuf),
Directory(PathBuf),
Unknown(DisplaySafeUrl),
}
impl VersionId {
pub fn from_registry(name: PackageName, version: Version) -> Self {
Self::NameVersion(name, version)
}
pub fn from_parsed_url(url: &ParsedUrl) -> Self {
match url {
ParsedUrl::Path(path) => Self::from_path_url(path),
ParsedUrl::Directory(directory) => Self::from_directory_url(directory),
ParsedUrl::Git(git) => Self::from_git_url(git),
ParsedUrl::Archive(archive) => Self::from_archive_url(archive),
}
}
pub fn from_url(url: &DisplaySafeUrl) -> Self {
match ParsedUrl::try_from(url.clone()) {
Ok(parsed) => Self::from_parsed_url(&parsed),
Err(_) => Self::Unknown(url.clone()),
}
}
pub fn from_archive(location: &DisplaySafeUrl, subdirectory: Option<&Path>) -> Self {
Self::ArchiveUrl {
location: CanonicalUrl::new(location),
subdirectory: subdirectory.map(Path::to_path_buf),
}
}
pub fn from_git(git: &GitUrl, subdirectory: Option<&Path>) -> Self {
Self::Git {
url: git.clone(),
subdirectory: subdirectory.map(Path::to_path_buf),
}
}
pub fn from_path(path: &Path) -> Self {
Self::Path(path.to_path_buf())
}
pub fn from_directory(path: &Path) -> Self {
Self::Directory(path.to_path_buf())
}
fn from_archive_url(archive: &ParsedArchiveUrl) -> Self {
Self::from_archive(&archive.url, archive.subdirectory.as_deref())
}
fn from_path_url(path: &ParsedPathUrl) -> Self {
Self::from_path(path.install_path.as_ref())
}
fn from_directory_url(directory: &ParsedDirectoryUrl) -> Self {
Self::from_directory(directory.install_path.as_ref())
}
fn from_git_url(git: &ParsedGitUrl) -> Self {
Self::from_git(&git.url, git.subdirectory.as_deref())
}
}
impl Display for VersionId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NameVersion(name, version) => write!(f, "{name}-{version}"),
Self::ArchiveUrl {
location,
subdirectory,
} => {
let mut location = DisplaySafeUrl::from(location.clone());
if let Some(subdirectory) = subdirectory {
location
.set_fragment(Some(&format!("subdirectory={}", subdirectory.display())));
}
write!(f, "{location}")
}
Self::Git { url, subdirectory } => {
let mut git_url = DisplaySafeUrl::parse(&format!("git+{}", url.url()))
.expect("Git URLs should be display-safe");
if let Some(precise) = url.precise() {
let path = format!("{}@{}", git_url.path(), precise);
git_url.set_path(&path);
} else if let Some(reference) = url.reference().as_str() {
let path = format!("{}@{}", git_url.path(), reference);
git_url.set_path(&path);
}
let mut fragments = Vec::new();
if let Some(subdirectory) = subdirectory {
fragments.push(format!("subdirectory={}", subdirectory.display()));
}
if url.lfs().enabled() {
fragments.push("lfs=true".to_string());
}
if !fragments.is_empty() {
git_url.set_fragment(Some(&fragments.join("&")));
}
write!(f, "{git_url}")
}
Self::Path(path) | Self::Directory(path) => {
if let Ok(url) = DisplaySafeUrl::from_file_path(path) {
write!(f, "{url}")
} else {
write!(f, "{}", path.display())
}
}
Self::Unknown(url) => write!(f, "{url}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum DistributionId {
Url(CanonicalUrl),
PathBuf(PathBuf),
Digest(HashDigest),
AbsoluteUrl(String),
RelativeUrl(String, String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ResourceId {
Url(RepositoryUrl),
PathBuf(PathBuf),
Digest(HashDigest),
AbsoluteUrl(String),
RelativeUrl(String, String),
}
impl From<&Self> for VersionId {
fn from(value: &Self) -> Self {
value.clone()
}
}
impl From<&Self> for DistributionId {
fn from(value: &Self) -> Self {
value.clone()
}
}
impl From<&Self> for ResourceId {
fn from(value: &Self) -> Self {
value.clone()
}
}
#[cfg(test)]
mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
use fs_err as fs;
use super::VersionId;
use uv_redacted::DisplaySafeUrl;
#[test]
fn version_id_ignores_hash_fragments() {
let first = DisplaySafeUrl::parse(
"https://example.com/pkg-0.1.0.whl#sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.unwrap();
let second = DisplaySafeUrl::parse(
"https://example.com/pkg-0.1.0.whl#sha512=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
)
.unwrap();
assert_eq!(VersionId::from_url(&first), VersionId::from_url(&second));
}
#[test]
fn version_id_preserves_non_hash_fragments() {
let first =
DisplaySafeUrl::parse("https://example.com/pkg-0.1.0.tar.gz#subdirectory=foo").unwrap();
let second =
DisplaySafeUrl::parse("https://example.com/pkg-0.1.0.tar.gz#subdirectory=bar").unwrap();
assert_ne!(VersionId::from_url(&first), VersionId::from_url(&second));
}
#[test]
fn version_id_ignores_hash_fragments_with_subdirectory() {
let first = DisplaySafeUrl::parse(
"https://example.com/pkg-0.1.0.tar.gz#subdirectory=foo&sha256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.unwrap();
let second = DisplaySafeUrl::parse(
"https://example.com/pkg-0.1.0.tar.gz#sha512=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&subdirectory=foo",
)
.unwrap();
assert_eq!(VersionId::from_url(&first), VersionId::from_url(&second));
}
#[test]
fn version_id_preserves_non_archive_fragments() {
let first =
DisplaySafeUrl::parse("git+https://example.com/pkg.git#subdirectory=foo").unwrap();
let second =
DisplaySafeUrl::parse("git+https://example.com/pkg.git#subdirectory=bar").unwrap();
assert_ne!(VersionId::from_url(&first), VersionId::from_url(&second));
}
#[test]
fn version_id_ignores_irrelevant_git_fragments() {
let first =
DisplaySafeUrl::parse("git+https://example.com/pkg.git@main#egg=pkg&subdirectory=foo")
.unwrap();
let second =
DisplaySafeUrl::parse("git+https://example.com/pkg.git@main#subdirectory=foo").unwrap();
assert_eq!(VersionId::from_url(&first), VersionId::from_url(&second));
}
#[test]
fn version_id_uses_file_kinds() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let root = std::env::temp_dir().join(format!("uv-version-id-{nonce}"));
let file = root.join("pkg-0.1.0.whl");
let directory = root.join("pkg");
fs::create_dir_all(&directory).unwrap();
fs::write(&file, b"wheel").unwrap();
let file_url = DisplaySafeUrl::from_file_path(&file).unwrap();
let directory_url = DisplaySafeUrl::from_file_path(&directory).unwrap();
assert!(matches!(VersionId::from_url(&file_url), VersionId::Path(_)));
assert!(matches!(
VersionId::from_url(&directory_url),
VersionId::Directory(_)
));
fs::remove_file(file).unwrap();
fs::remove_dir_all(root).unwrap();
}
#[test]
fn version_id_uses_unknown_for_invalid_git_like_urls() {
let url =
DisplaySafeUrl::parse("git+ftp://example.com/pkg.git@main#subdirectory=foo").unwrap();
assert!(matches!(VersionId::from_url(&url), VersionId::Unknown(_)));
}
}