mirror of https://github.com/astral-sh/uv
Allow conflicting Git URLs that refer to the same commit SHA (#2769)
## Summary This PR leverages our lookahead direct URL resolution to significantly improve the range of Git URLs that we can accept (e.g., if a user provides the same requirement, once as a direct dependency, and once as a tag). We did some of this in #2285, but the solution here is more general and works for arbitrary transitive URLs. Closes https://github.com/astral-sh/uv/issues/2614.
This commit is contained in:
parent
20d4762776
commit
c30a65ee0c
|
|
@ -91,6 +91,12 @@ impl Hash for CanonicalUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CanonicalUrl> for Url {
|
||||||
|
fn from(value: CanonicalUrl) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for CanonicalUrl {
|
impl std::fmt::Display for CanonicalUrl {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
std::fmt::Display::fmt(&self.0, f)
|
std::fmt::Display::fmt(&self.0, f)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use rustc_hash::FxHashMap;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use cache_key::CanonicalUrl;
|
||||||
use distribution_types::DirectGitUrl;
|
use distribution_types::DirectGitUrl;
|
||||||
use uv_cache::{Cache, CacheBucket};
|
use uv_cache::{Cache, CacheBucket};
|
||||||
use uv_fs::LockedFile;
|
use uv_fs::LockedFile;
|
||||||
|
|
@ -39,7 +40,7 @@ pub(crate) async fn fetch_git_archive(
|
||||||
fs::create_dir_all(&lock_dir)
|
fs::create_dir_all(&lock_dir)
|
||||||
.await
|
.await
|
||||||
.map_err(Error::CacheWrite)?;
|
.map_err(Error::CacheWrite)?;
|
||||||
let canonical_url = cache_key::CanonicalUrl::new(url);
|
let canonical_url = CanonicalUrl::new(url);
|
||||||
let _lock = LockedFile::acquire(
|
let _lock = LockedFile::acquire(
|
||||||
lock_dir.join(cache_key::digest(&canonical_url)),
|
lock_dir.join(cache_key::digest(&canonical_url)),
|
||||||
&canonical_url,
|
&canonical_url,
|
||||||
|
|
@ -91,9 +92,8 @@ pub(crate) async fn resolve_precise(
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
reporter: Option<&Arc<dyn Reporter>>,
|
reporter: Option<&Arc<dyn Reporter>>,
|
||||||
) -> Result<Option<Url>, Error> {
|
) -> Result<Option<Url>, Error> {
|
||||||
let git_dir = cache.bucket(CacheBucket::Git);
|
let url = Url::from(CanonicalUrl::new(url));
|
||||||
|
let DirectGitUrl { url, subdirectory } = DirectGitUrl::try_from(&url).map_err(Error::Git)?;
|
||||||
let DirectGitUrl { url, subdirectory } = DirectGitUrl::try_from(url).map_err(Error::Git)?;
|
|
||||||
|
|
||||||
// If the Git reference already contains a complete SHA, short-circuit.
|
// If the Git reference already contains a complete SHA, short-circuit.
|
||||||
if url.precise().is_some() {
|
if url.precise().is_some() {
|
||||||
|
|
@ -111,6 +111,8 @@ pub(crate) async fn resolve_precise(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let git_dir = cache.bucket(CacheBucket::Git);
|
||||||
|
|
||||||
// Fetch the precise SHA of the Git reference (which could be a branch, a tag, a partial
|
// Fetch the precise SHA of the Git reference (which could be a branch, a tag, a partial
|
||||||
// commit, etc.).
|
// commit, etc.).
|
||||||
let source = if let Some(reporter) = reporter {
|
let source = if let Some(reporter) = reporter {
|
||||||
|
|
@ -135,3 +137,134 @@ pub(crate) async fn resolve_precise(
|
||||||
subdirectory,
|
subdirectory,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the URLs refer to the same Git commit.
|
||||||
|
///
|
||||||
|
/// For example, the previous URL could be a branch or tag, while the current URL would be a
|
||||||
|
/// precise commit hash.
|
||||||
|
pub fn is_same_reference<'a>(a: &'a Url, b: &'a Url) -> bool {
|
||||||
|
let resolved_git_refs = RESOLVED_GIT_REFS.lock().unwrap();
|
||||||
|
is_same_reference_impl(a, b, &resolved_git_refs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the URLs refer to the same Git commit.
|
||||||
|
///
|
||||||
|
/// Like [`is_same_reference`], but accepts a resolved reference cache for testing.
|
||||||
|
fn is_same_reference_impl<'a>(
|
||||||
|
a: &'a Url,
|
||||||
|
b: &'a Url,
|
||||||
|
resolved_refs: &FxHashMap<GitUrl, GitUrl>,
|
||||||
|
) -> bool {
|
||||||
|
// Convert `a` to a Git URL, if possible.
|
||||||
|
let Ok(a_git) = DirectGitUrl::try_from(&Url::from(CanonicalUrl::new(a))) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert `b` to a Git URL, if possible.
|
||||||
|
let Ok(b_git) = DirectGitUrl::try_from(&Url::from(CanonicalUrl::new(b))) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The URLs must refer to the same subdirectory, if any.
|
||||||
|
if a_git.subdirectory != b_git.subdirectory {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The URLs must refer to the same repository.
|
||||||
|
if a_git.url.repository() != b_git.url.repository() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the URLs have the same tag, they refer to the same commit.
|
||||||
|
if a_git.url.reference() == b_git.url.reference() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, the URLs must resolve to the same precise commit.
|
||||||
|
let Some(a_precise) = a_git
|
||||||
|
.url
|
||||||
|
.precise()
|
||||||
|
.or_else(|| resolved_refs.get(&a_git.url).and_then(GitUrl::precise))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(b_precise) = b_git
|
||||||
|
.url
|
||||||
|
.precise()
|
||||||
|
.or_else(|| resolved_refs.get(&b_git.url).and_then(GitUrl::precise))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
a_precise == b_precise
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use anyhow::Result;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use uv_git::GitUrl;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_reference() -> Result<()> {
|
||||||
|
let empty = FxHashMap::default();
|
||||||
|
|
||||||
|
// Same repository, same tag.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main")?;
|
||||||
|
let b = Url::parse("git+https://example.com/MyProject.git@main")?;
|
||||||
|
assert!(super::is_same_reference_impl(&a, &b, &empty));
|
||||||
|
|
||||||
|
// Same repository, same tag, same subdirectory.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main#subdirectory=pkg_dir")?;
|
||||||
|
let b = Url::parse("git+https://example.com/MyProject.git@main#subdirectory=pkg_dir")?;
|
||||||
|
assert!(super::is_same_reference_impl(&a, &b, &empty));
|
||||||
|
|
||||||
|
// Different repositories, same tag.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main")?;
|
||||||
|
let b = Url::parse("git+https://example.com/MyOtherProject.git@main")?;
|
||||||
|
assert!(!super::is_same_reference_impl(&a, &b, &empty));
|
||||||
|
|
||||||
|
// Same repository, different tags.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main")?;
|
||||||
|
let b = Url::parse("git+https://example.com/MyProject.git@v1.0")?;
|
||||||
|
assert!(!super::is_same_reference_impl(&a, &b, &empty));
|
||||||
|
|
||||||
|
// Same repository, same tag, different subdirectory.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main#subdirectory=pkg_dir")?;
|
||||||
|
let b = Url::parse("git+https://example.com/MyProject.git@main#subdirectory=other_dir")?;
|
||||||
|
assert!(!super::is_same_reference_impl(&a, &b, &empty));
|
||||||
|
|
||||||
|
// Same repository, different tags, but same precise commit.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main")?;
|
||||||
|
let b = Url::parse(
|
||||||
|
"git+https://example.com/MyProject.git@164a8735b081663fede48c5041667b194da15d25",
|
||||||
|
)?;
|
||||||
|
let mut resolved_refs = FxHashMap::default();
|
||||||
|
resolved_refs.insert(
|
||||||
|
GitUrl::try_from(Url::parse("https://example.com/MyProject@main")?)?,
|
||||||
|
GitUrl::try_from(Url::parse(
|
||||||
|
"https://example.com/MyProject@164a8735b081663fede48c5041667b194da15d25",
|
||||||
|
)?)?,
|
||||||
|
);
|
||||||
|
assert!(super::is_same_reference_impl(&a, &b, &resolved_refs));
|
||||||
|
|
||||||
|
// Same repository, different tags, different precise commit.
|
||||||
|
let a = Url::parse("git+https://example.com/MyProject.git@main")?;
|
||||||
|
let b = Url::parse(
|
||||||
|
"git+https://example.com/MyProject.git@164a8735b081663fede48c5041667b194da15d25",
|
||||||
|
)?;
|
||||||
|
let mut resolved_refs = FxHashMap::default();
|
||||||
|
resolved_refs.insert(
|
||||||
|
GitUrl::try_from(Url::parse("https://example.com/MyProject@main")?)?,
|
||||||
|
GitUrl::try_from(Url::parse(
|
||||||
|
"https://example.com/MyProject@f2c9e88f3ec9526bbcec68d150b176d96a750aba",
|
||||||
|
)?)?,
|
||||||
|
);
|
||||||
|
assert!(!super::is_same_reference_impl(&a, &b, &resolved_refs));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub use distribution_database::DistributionDatabase;
|
pub use distribution_database::DistributionDatabase;
|
||||||
pub use download::{BuiltWheel, DiskWheel, LocalWheel};
|
pub use download::{BuiltWheel, DiskWheel, LocalWheel};
|
||||||
pub use error::Error;
|
pub use error::Error;
|
||||||
|
pub use git::is_same_reference;
|
||||||
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
||||||
pub use reporter::Reporter;
|
pub use reporter::Reporter;
|
||||||
pub use source::{download_and_extract_archive, SourceDistributionBuilder};
|
pub use source::{download_and_extract_archive, SourceDistributionBuilder};
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ impl GitUrl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the reference is a full commit.
|
||||||
|
pub fn is_full_commit(&self) -> bool {
|
||||||
|
matches!(self.reference, GitReference::FullCommit(_))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the precise commit, if known.
|
/// Return the precise commit, if known.
|
||||||
pub fn precise(&self) -> Option<GitSha> {
|
pub fn precise(&self) -> Option<GitSha> {
|
||||||
self.precise
|
self.precise
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,8 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> {
|
||||||
|
|
||||||
while !queue.is_empty() || !futures.is_empty() {
|
while !queue.is_empty() || !futures.is_empty() {
|
||||||
while let Some(requirement) = queue.pop_front() {
|
while let Some(requirement) = queue.pop_front() {
|
||||||
// Ignore duplicates. If we have conflicting URLs, we'll catch that later.
|
|
||||||
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) {
|
||||||
if seen.insert(requirement.name.clone()) {
|
if seen.insert(requirement.clone()) {
|
||||||
futures.push(self.lookahead(requirement));
|
futures.push(self.lookahead(requirement));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ fn to_pubgrub(
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
if !urls.is_allowed(expected, url) {
|
if !Urls::is_allowed(expected, url) {
|
||||||
return Err(ResolveError::ConflictingUrlsTransitive(
|
return Err(ResolveError::ConflictingUrlsTransitive(
|
||||||
requirement.name.clone(),
|
requirement.name.clone(),
|
||||||
expected.verbatim().to_string(),
|
expected.verbatim().to_string(),
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,28 @@ use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::Verbatim;
|
use distribution_types::Verbatim;
|
||||||
use pep508_rs::{MarkerEnvironment, VerbatimUrl};
|
use pep508_rs::{MarkerEnvironment, VerbatimUrl};
|
||||||
|
use uv_distribution::is_same_reference;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::{Manifest, ResolveError};
|
use crate::{Manifest, ResolveError};
|
||||||
|
|
||||||
|
/// A map of package names to their associated, required URLs.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct Urls {
|
pub(crate) struct Urls(FxHashMap<PackageName, VerbatimUrl>);
|
||||||
/// A map of package names to their associated, required URLs.
|
|
||||||
required: FxHashMap<PackageName, VerbatimUrl>,
|
|
||||||
/// A map from required URL to URL that is assumed to be a less precise variant.
|
|
||||||
allowed: FxHashMap<VerbatimUrl, VerbatimUrl>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Urls {
|
impl Urls {
|
||||||
pub(crate) fn from_manifest(
|
pub(crate) fn from_manifest(
|
||||||
manifest: &Manifest,
|
manifest: &Manifest,
|
||||||
markers: &MarkerEnvironment,
|
markers: &MarkerEnvironment,
|
||||||
) -> Result<Self, ResolveError> {
|
) -> Result<Self, ResolveError> {
|
||||||
let mut required: FxHashMap<PackageName, VerbatimUrl> = FxHashMap::default();
|
let mut urls: FxHashMap<PackageName, VerbatimUrl> = FxHashMap::default();
|
||||||
let mut allowed: FxHashMap<VerbatimUrl, VerbatimUrl> = FxHashMap::default();
|
|
||||||
|
|
||||||
// Add the themselves to the list of required URLs.
|
// Add the themselves to the list of required URLs.
|
||||||
for (editable, metadata) in &manifest.editables {
|
for (editable, metadata) in &manifest.editables {
|
||||||
if let Some(previous) = required.insert(metadata.name.clone(), editable.url.clone()) {
|
if let Some(previous) = urls.insert(metadata.name.clone(), editable.url.clone()) {
|
||||||
if !is_equal(&previous, &editable.url) {
|
if !is_equal(&previous, &editable.url) {
|
||||||
if is_precise(&previous, &editable.url) {
|
if is_same_reference(&previous, &editable.url) {
|
||||||
debug!(
|
debug!("Allowing {} as a variant of {previous}", editable.url);
|
||||||
"Assuming {} is a precise variant of {previous}",
|
|
||||||
editable.url
|
|
||||||
);
|
|
||||||
allowed.insert(editable.url.clone(), previous);
|
|
||||||
} else {
|
} else {
|
||||||
return Err(ResolveError::ConflictingUrlsDirect(
|
return Err(ResolveError::ConflictingUrlsDirect(
|
||||||
metadata.name.clone(),
|
metadata.name.clone(),
|
||||||
|
|
@ -47,47 +39,38 @@ impl Urls {
|
||||||
// Add all direct requirements and constraints. If there are any conflicts, return an error.
|
// Add all direct requirements and constraints. If there are any conflicts, return an error.
|
||||||
for requirement in manifest.requirements(markers) {
|
for requirement in manifest.requirements(markers) {
|
||||||
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
|
if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url {
|
||||||
if let Some(previous) = required.insert(requirement.name.clone(), url.clone()) {
|
if let Some(previous) = urls.insert(requirement.name.clone(), url.clone()) {
|
||||||
if is_equal(&previous, url) {
|
if !is_equal(&previous, url) {
|
||||||
continue;
|
if is_same_reference(&previous, url) {
|
||||||
|
debug!("Allowing {url} as a variant of {previous}");
|
||||||
|
} else {
|
||||||
|
return Err(ResolveError::ConflictingUrlsDirect(
|
||||||
|
requirement.name.clone(),
|
||||||
|
previous.verbatim().to_string(),
|
||||||
|
url.verbatim().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_precise(&previous, url) {
|
|
||||||
debug!("Assuming {url} is a precise variant of {previous}");
|
|
||||||
allowed.insert(url.clone(), previous);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(ResolveError::ConflictingUrlsDirect(
|
|
||||||
requirement.name.clone(),
|
|
||||||
previous.verbatim().to_string(),
|
|
||||||
url.verbatim().to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self { required, allowed })
|
Ok(Self(urls))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`VerbatimUrl`] associated with the given package name, if any.
|
/// Return the [`VerbatimUrl`] associated with the given package name, if any.
|
||||||
pub(crate) fn get(&self, package: &PackageName) -> Option<&VerbatimUrl> {
|
pub(crate) fn get(&self, package: &PackageName) -> Option<&VerbatimUrl> {
|
||||||
self.required.get(package)
|
self.0.get(package)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the provided URL is compatible with the given "allowed" URL.
|
/// Returns `true` if the provided URL is compatible with the given "allowed" URL.
|
||||||
pub(crate) fn is_allowed(&self, expected: &VerbatimUrl, provided: &VerbatimUrl) -> bool {
|
pub(crate) fn is_allowed(expected: &VerbatimUrl, provided: &VerbatimUrl) -> bool {
|
||||||
#[allow(clippy::if_same_then_else)]
|
#[allow(clippy::if_same_then_else)]
|
||||||
if is_equal(expected, provided) {
|
if is_equal(expected, provided) {
|
||||||
// If the URLs are canonically equivalent, they're compatible.
|
// If the URLs are canonically equivalent, they're compatible.
|
||||||
true
|
true
|
||||||
} else if self
|
} else if is_same_reference(expected, provided) {
|
||||||
.allowed
|
// If the URLs refer to the same commit, they're compatible.
|
||||||
.get(expected)
|
|
||||||
.is_some_and(|allowed| is_equal(allowed, provided))
|
|
||||||
{
|
|
||||||
// If the URL is canonically equivalent to the imprecise variant of the URL, they're
|
|
||||||
// compatible.
|
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, they're incompatible.
|
// Otherwise, they're incompatible.
|
||||||
|
|
@ -103,53 +86,6 @@ fn is_equal(previous: &VerbatimUrl, url: &VerbatimUrl) -> bool {
|
||||||
cache_key::CanonicalUrl::new(previous.raw()) == cache_key::CanonicalUrl::new(url.raw())
|
cache_key::CanonicalUrl::new(previous.raw()) == cache_key::CanonicalUrl::new(url.raw())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the [`VerbatimUrl`] appears to be a more precise variant of the previous
|
|
||||||
/// [`VerbatimUrl`].
|
|
||||||
///
|
|
||||||
/// Primarily, this method intends to accept URLs that map to the same repository, but with a
|
|
||||||
/// precise Git commit hash overriding a looser tag or branch. For example, if the previous URL
|
|
||||||
/// is `git+https://github.com/pallets/werkzeug.git@main`, this method would accept
|
|
||||||
/// `git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f`, and
|
|
||||||
/// assume that the latter is a more precise variant of the former. This is particularly useful
|
|
||||||
/// for workflows in which the output of `uv pip compile` is used as an input constraint on a
|
|
||||||
/// subsequent resolution, since `uv` will pin the exact commit hash of the package.
|
|
||||||
fn is_precise(previous: &VerbatimUrl, url: &VerbatimUrl) -> bool {
|
|
||||||
if cache_key::RepositoryUrl::new(previous.raw()) != cache_key::RepositoryUrl::new(url.raw()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no tag in the overriding URL, consider it incompatible.
|
|
||||||
let Some(url_tag) = url
|
|
||||||
.raw()
|
|
||||||
.path()
|
|
||||||
.rsplit_once('@')
|
|
||||||
.map(|(_prefix, suffix)| suffix)
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Accept the overriding URL, as long as it's a full commit hash...
|
|
||||||
let url_is_commit = url_tag.len() == 40 && url_tag.chars().all(|ch| ch.is_ascii_hexdigit());
|
|
||||||
if !url_is_commit {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no tag in the previous URL, consider it compatible.
|
|
||||||
let Some(previous_tag) = previous
|
|
||||||
.raw()
|
|
||||||
.path()
|
|
||||||
.rsplit_once('@')
|
|
||||||
.map(|(_prefix, suffix)| suffix)
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the previous URL is a full commit hash, consider it incompatible.
|
|
||||||
let previous_is_commit =
|
|
||||||
previous_tag.len() == 40 && previous_tag.chars().all(|ch| ch.is_ascii_hexdigit());
|
|
||||||
!previous_is_commit
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -183,37 +119,4 @@ mod tests {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_precision() -> Result<(), url::ParseError> {
|
|
||||||
// Same repository, no tag on the previous URL, non-SHA on the overriding URL.
|
|
||||||
let previous = VerbatimUrl::parse_url("git+https://example.com/MyProject.git")?;
|
|
||||||
let url = VerbatimUrl::parse_url("git+https://example.com/MyProject.git@v1.0")?;
|
|
||||||
assert!(!is_precise(&previous, &url));
|
|
||||||
|
|
||||||
// Same repository, no tag on the previous URL, SHA on the overriding URL.
|
|
||||||
let previous = VerbatimUrl::parse_url("git+https://example.com/MyProject.git")?;
|
|
||||||
let url = VerbatimUrl::parse_url(
|
|
||||||
"git+https://example.com/MyProject.git@c3cd550a7a7c41b2c286ca52fbb6dec5fea195ef",
|
|
||||||
)?;
|
|
||||||
assert!(is_precise(&previous, &url));
|
|
||||||
|
|
||||||
// Same repository, tag on the previous URL, SHA on the overriding URL.
|
|
||||||
let previous = VerbatimUrl::parse_url("git+https://example.com/MyProject.git@v1.0")?;
|
|
||||||
let url = VerbatimUrl::parse_url(
|
|
||||||
"git+https://example.com/MyProject.git@c3cd550a7a7c41b2c286ca52fbb6dec5fea195ef",
|
|
||||||
)?;
|
|
||||||
assert!(is_precise(&previous, &url));
|
|
||||||
|
|
||||||
// Same repository, SHA on the previous URL, different SHA on the overriding URL.
|
|
||||||
let previous = VerbatimUrl::parse_url(
|
|
||||||
"git+https://example.com/MyProject.git@5ae5980c885e350a34ca019a84ba14a2a228d262",
|
|
||||||
)?;
|
|
||||||
let url = VerbatimUrl::parse_url(
|
|
||||||
"git+https://example.com/MyProject.git@c3cd550a7a7c41b2c286ca52fbb6dec5fea195ef",
|
|
||||||
)?;
|
|
||||||
assert!(!is_precise(&previous, &url));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1578,6 +1578,7 @@ fn conflicting_repeated_url_dependency_markers() -> Result<()> {
|
||||||
fn conflicting_repeated_url_dependency_version_match() -> Result<()> {
|
fn conflicting_repeated_url_dependency_version_match() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
|
||||||
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@2.0.0\nwerkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl")?;
|
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@2.0.0\nwerkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl")?;
|
||||||
|
|
||||||
uv_snapshot!(context.compile()
|
uv_snapshot!(context.compile()
|
||||||
|
|
@ -1621,12 +1622,15 @@ fn conflicting_transitive_url_dependency() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request Werkzeug via two different URLs which resolve to the same canonical version.
|
/// Request `anyio` via two different URLs which resolve to the same canonical version.
|
||||||
#[test]
|
#[test]
|
||||||
fn compatible_repeated_url_dependency() -> Result<()> {
|
fn compatible_repeated_url_dependency() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@2.0.0\nwerkzeug @ git+https://github.com/pallets/werkzeug@2.0.0")?;
|
requirements_in.write_str(indoc! {r"
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@4.3.0
|
||||||
|
"})?;
|
||||||
|
|
||||||
uv_snapshot!(context.compile()
|
uv_snapshot!(context.compile()
|
||||||
.arg("requirements.in"), @r###"
|
.arg("requirements.in"), @r###"
|
||||||
|
|
@ -1635,23 +1639,30 @@ fn compatible_repeated_url_dependency() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
werkzeug @ git+https://github.com/pallets/werkzeug@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74
|
anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
idna==3.6
|
||||||
|
# via anyio
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 1 package in [TIME]
|
Resolved 3 packages in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request Werkzeug via two different URLs which resolve to the same repository, but different
|
/// Request `anyio` via two different URLs which resolve to the same repository, but different
|
||||||
/// commits.
|
/// commits.
|
||||||
#[test]
|
#[test]
|
||||||
fn conflicting_repeated_url_dependency() -> Result<()> {
|
fn conflicting_repeated_url_dependency() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@2.0.0\nwerkzeug @ git+https://github.com/pallets/werkzeug@3.0.0")?;
|
requirements_in.write_str(indoc! {r"
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.0.0
|
||||||
|
"})?;
|
||||||
|
|
||||||
uv_snapshot!(context.compile()
|
uv_snapshot!(context.compile()
|
||||||
.arg("requirements.in"), @r###"
|
.arg("requirements.in"), @r###"
|
||||||
|
|
@ -1660,22 +1671,26 @@ fn conflicting_repeated_url_dependency() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `werkzeug`:
|
error: Requirements contain conflicting URLs for package `anyio`:
|
||||||
- git+https://github.com/pallets/werkzeug.git@2.0.0
|
- git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
- git+https://github.com/pallets/werkzeug@3.0.0
|
- git+https://github.com/agronholm/anyio.git@4.0.0
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request Werkzeug via two different URLs: `main`, and a precise SHA. Allow the precise SHA
|
/// Request Werkzeug via two different URLs: `3.0.1`, and a precise SHA. Allow the precise SHA
|
||||||
/// to override the `main` branch.
|
/// to override the `3.0.1` branch.
|
||||||
#[test]
|
#[test]
|
||||||
fn compatible_narrowed_url_dependency() -> Result<()> {
|
fn compatible_narrowed_url_dependency() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@main\nwerkzeug @ git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f")?;
|
requirements_in.write_str(indoc! {r"
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@437a7e31
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
"})?;
|
||||||
|
|
||||||
uv_snapshot!(context.compile()
|
uv_snapshot!(context.compile()
|
||||||
.arg("requirements.in"), @r###"
|
.arg("requirements.in"), @r###"
|
||||||
|
|
@ -1684,49 +1699,96 @@ fn compatible_narrowed_url_dependency() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
markupsafe==2.1.5
|
anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
# via werkzeug
|
idna==3.6
|
||||||
werkzeug @ git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f
|
# via anyio
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 2 packages in [TIME]
|
Resolved 3 packages in [TIME]
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request Werkzeug via two different URLs: `main`, and a precise SHA, followed by `main` again.
|
/// Request Werkzeug via two different URLs: a precise SHA, and `3.0.1`. Allow the precise SHA
|
||||||
/// We _may_ want to allow this, but we don't right now.
|
/// to override the `3.0.1` branch.
|
||||||
|
#[test]
|
||||||
|
fn compatible_broader_url_dependency() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str(indoc! {r"
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@437a7e31
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
"})?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("requirements.in"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
idna==3.6
|
||||||
|
# via anyio
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 3 packages in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request Werkzeug via two different URLs: `4.3.0`, and a precise SHA, followed by `4.3.0` again.
|
||||||
#[test]
|
#[test]
|
||||||
fn compatible_repeated_narrowed_url_dependency() -> Result<()> {
|
fn compatible_repeated_narrowed_url_dependency() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug@main\nwerkzeug @ git+https://github.com/pallets/werkzeug.git@main\nwerkzeug @ git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f\nwerkzeug @ git+https://github.com/pallets/werkzeug.git@main")?;
|
requirements_in.write_str(indoc! {r"
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
"})?;
|
||||||
|
|
||||||
uv_snapshot!(context.compile()
|
uv_snapshot!(context.compile()
|
||||||
.arg("requirements.in"), @r###"
|
.arg("requirements.in"), @r###"
|
||||||
success: false
|
success: true
|
||||||
exit_code: 2
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
idna==3.6
|
||||||
|
# via anyio
|
||||||
|
sniffio==1.3.1
|
||||||
|
# via anyio
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `werkzeug`:
|
Resolved 3 packages in [TIME]
|
||||||
- git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f
|
|
||||||
- git+https://github.com/pallets/werkzeug.git@main
|
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request Werkzeug via two different URLs: `main`, and a precise SHA. Allow the precise SHA
|
/// Request Werkzeug via two different URLs: `master`, and a precise SHA. Allow the precise SHA
|
||||||
/// to override the `main` branch, but error when we see yet another URL for the same package.
|
/// to override the `master` branch, but error when we see yet another URL for the same package.
|
||||||
#[test]
|
#[test]
|
||||||
fn incompatible_narrowed_url_dependency() -> Result<()> {
|
fn incompatible_narrowed_url_dependency() -> Result<()> {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
requirements_in.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@main\nwerkzeug @ git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f\nwerkzeug @ git+https://github.com/pallets/werkzeug.git@3.0.1")?;
|
requirements_in.write_str(indoc! {r"
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@master
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
|
anyio @ git+https://github.com/agronholm/anyio.git@4.3.0
|
||||||
|
"})?;
|
||||||
|
|
||||||
uv_snapshot!(context.compile()
|
uv_snapshot!(context.compile()
|
||||||
.arg("requirements.in"), @r###"
|
.arg("requirements.in"), @r###"
|
||||||
|
|
@ -1735,9 +1797,9 @@ fn incompatible_narrowed_url_dependency() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Requirements contain conflicting URLs for package `werkzeug`:
|
error: Requirements contain conflicting URLs for package `anyio`:
|
||||||
- git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f
|
- git+https://github.com/agronholm/anyio.git@master
|
||||||
- git+https://github.com/pallets/werkzeug.git@3.0.1
|
- git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -582,7 +582,7 @@ fn install_git_tag() -> Result<()> {
|
||||||
Resolved 1 package in [TIME]
|
Resolved 1 package in [TIME]
|
||||||
Downloaded 1 package in [TIME]
|
Downloaded 1 package in [TIME]
|
||||||
Installed 1 package in [TIME]
|
Installed 1 package in [TIME]
|
||||||
+ werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74)
|
+ werkzeug==2.0.0 (from git+https://github.com/pallets/werkzeug@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74)
|
||||||
"###
|
"###
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue