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:
Charlie Marsh 2024-04-02 19:36:35 -04:00 committed by GitHub
parent 20d4762776
commit c30a65ee0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 267 additions and 158 deletions

View File

@ -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)

View File

@ -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(())
}
}

View File

@ -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};

View File

@ -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

View File

@ -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));
} }
} }

View File

@ -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(),

View File

@ -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,17 +39,11 @@ 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 {
if is_precise(&previous, url) {
debug!("Assuming {url} is a precise variant of {previous}");
allowed.insert(url.clone(), previous);
continue;
}
return Err(ResolveError::ConflictingUrlsDirect( return Err(ResolveError::ConflictingUrlsDirect(
requirement.name.clone(), requirement.name.clone(),
previous.verbatim().to_string(), previous.verbatim().to_string(),
@ -66,28 +52,25 @@ impl Urls {
} }
} }
} }
}
}
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(())
}
} }

View File

@ -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
"### "###
); );

View File

@ -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)
"### "###
); );