diff --git a/crates/cache-key/src/canonical_url.rs b/crates/cache-key/src/canonical_url.rs index b45e337479fb..4157fe3f2392 100644 --- a/crates/cache-key/src/canonical_url.rs +++ b/crates/cache-key/src/canonical_url.rs @@ -91,6 +91,12 @@ impl Hash for CanonicalUrl { } } +impl From for Url { + fn from(value: CanonicalUrl) -> Self { + value.0 + } +} + impl std::fmt::Display for CanonicalUrl { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.0, f) diff --git a/crates/uv-distribution/src/git.rs b/crates/uv-distribution/src/git.rs index 549e943a270d..6df565835fce 100644 --- a/crates/uv-distribution/src/git.rs +++ b/crates/uv-distribution/src/git.rs @@ -8,6 +8,7 @@ use rustc_hash::FxHashMap; use tracing::debug; use url::Url; +use cache_key::CanonicalUrl; use distribution_types::DirectGitUrl; use uv_cache::{Cache, CacheBucket}; use uv_fs::LockedFile; @@ -91,9 +92,8 @@ pub(crate) async fn resolve_precise( cache: &Cache, reporter: Option<&Arc>, ) -> Result, Error> { - let git_dir = cache.bucket(CacheBucket::Git); - - let DirectGitUrl { url, subdirectory } = DirectGitUrl::try_from(url).map_err(Error::Git)?; + let url = Url::from(CanonicalUrl::new(url)); + let DirectGitUrl { url, subdirectory } = DirectGitUrl::try_from(&url).map_err(Error::Git)?; // If the Git reference already contains a complete SHA, short-circuit. 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 // commit, etc.). let source = if let Some(reporter) = reporter { @@ -135,3 +137,134 @@ pub(crate) async fn resolve_precise( 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, +) -> 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(()) + } +} diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index a57276466497..68210968761e 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -1,6 +1,7 @@ pub use distribution_database::DistributionDatabase; pub use download::{BuiltWheel, DiskWheel, LocalWheel}; pub use error::Error; +pub use git::is_same_reference; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use reporter::Reporter; pub use source::{download_and_extract_archive, SourceDistributionBuilder}; diff --git a/crates/uv-git/src/lib.rs b/crates/uv-git/src/lib.rs index 7b6c28a6fd5f..4fb25a6ca40d 100644 --- a/crates/uv-git/src/lib.rs +++ b/crates/uv-git/src/lib.rs @@ -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. pub fn precise(&self) -> Option { self.precise diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index ad8e6effe1a9..3c7b5298beb4 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -96,9 +96,8 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { while !queue.is_empty() || !futures.is_empty() { 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 seen.insert(requirement.name.clone()) { + if seen.insert(requirement.clone()) { futures.push(self.lookahead(requirement)); } } diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 8960dea9c95d..28760cee51de 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -177,7 +177,7 @@ fn to_pubgrub( )); }; - if !urls.is_allowed(expected, url) { + if !Urls::is_allowed(expected, url) { return Err(ResolveError::ConflictingUrlsTransitive( requirement.name.clone(), expected.verbatim().to_string(), diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs index fd6877db89c4..680da6e5d820 100644 --- a/crates/uv-resolver/src/resolver/urls.rs +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -3,36 +3,28 @@ use tracing::debug; use distribution_types::Verbatim; use pep508_rs::{MarkerEnvironment, VerbatimUrl}; +use uv_distribution::is_same_reference; use uv_normalize::PackageName; use crate::{Manifest, ResolveError}; +/// A map of package names to their associated, required URLs. #[derive(Debug, Default)] -pub(crate) struct Urls { - /// A map of package names to their associated, required URLs. - required: FxHashMap, - /// A map from required URL to URL that is assumed to be a less precise variant. - allowed: FxHashMap, -} +pub(crate) struct Urls(FxHashMap); impl Urls { pub(crate) fn from_manifest( manifest: &Manifest, markers: &MarkerEnvironment, ) -> Result { - let mut required: FxHashMap = FxHashMap::default(); - let mut allowed: FxHashMap = FxHashMap::default(); + let mut urls: FxHashMap = FxHashMap::default(); // Add the themselves to the list of required URLs. 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_precise(&previous, &editable.url) { - debug!( - "Assuming {} is a precise variant of {previous}", - editable.url - ); - allowed.insert(editable.url.clone(), previous); + if is_same_reference(&previous, &editable.url) { + debug!("Allowing {} as a variant of {previous}", editable.url); } else { return Err(ResolveError::ConflictingUrlsDirect( metadata.name.clone(), @@ -47,47 +39,38 @@ impl Urls { // Add all direct requirements and constraints. If there are any conflicts, return an error. for requirement in manifest.requirements(markers) { if let Some(pep508_rs::VersionOrUrl::Url(url)) = &requirement.version_or_url { - if let Some(previous) = required.insert(requirement.name.clone(), url.clone()) { - if is_equal(&previous, url) { - continue; - } - - if is_precise(&previous, url) { - debug!("Assuming {url} is a precise variant of {previous}"); - allowed.insert(url.clone(), previous); - continue; + if let Some(previous) = urls.insert(requirement.name.clone(), url.clone()) { + if !is_equal(&previous, url) { + 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(), + )); + } } - - 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. 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. - 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)] if is_equal(expected, provided) { // If the URLs are canonically equivalent, they're compatible. true - } else if self - .allowed - .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. + } else if is_same_reference(expected, provided) { + // If the URLs refer to the same commit, they're compatible. true } else { // 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()) } -/// 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)] mod tests { use super::*; @@ -183,37 +119,4 @@ mod tests { 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(()) - } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 281c8b7f5694..fe00a513a719 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -1578,6 +1578,7 @@ fn conflicting_repeated_url_dependency_markers() -> Result<()> { fn conflicting_repeated_url_dependency_version_match() -> Result<()> { let context = TestContext::new("3.12"); 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")?; uv_snapshot!(context.compile() @@ -1621,12 +1622,15 @@ fn conflicting_transitive_url_dependency() -> Result<()> { 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] fn compatible_repeated_url_dependency() -> Result<()> { let context = TestContext::new("3.12"); 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() .arg("requirements.in"), @r###" @@ -1635,23 +1639,30 @@ fn compatible_repeated_url_dependency() -> Result<()> { ----- 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 - 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 ----- - Resolved 1 package in [TIME] + Resolved 3 packages in [TIME] "### ); 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. #[test] fn conflicting_repeated_url_dependency() -> Result<()> { let context = TestContext::new("3.12"); 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() .arg("requirements.in"), @r###" @@ -1660,22 +1671,26 @@ fn conflicting_repeated_url_dependency() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Requirements contain conflicting URLs for package `werkzeug`: - - git+https://github.com/pallets/werkzeug.git@2.0.0 - - git+https://github.com/pallets/werkzeug@3.0.0 + error: Requirements contain conflicting URLs for package `anyio`: + - git+https://github.com/agronholm/anyio.git@4.3.0 + - git+https://github.com/agronholm/anyio.git@4.0.0 "### ); Ok(()) } -/// Request Werkzeug via two different URLs: `main`, and a precise SHA. Allow the precise SHA -/// to override the `main` branch. +/// Request Werkzeug via two different URLs: `3.0.1`, and a precise SHA. Allow the precise SHA +/// to override the `3.0.1` branch. #[test] fn compatible_narrowed_url_dependency() -> Result<()> { let context = TestContext::new("3.12"); 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() .arg("requirements.in"), @r###" @@ -1684,49 +1699,96 @@ fn compatible_narrowed_url_dependency() -> Result<()> { ----- 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 - markupsafe==2.1.5 - # via werkzeug - werkzeug @ git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f + anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51 + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio ----- stderr ----- - Resolved 2 packages in [TIME] + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Request Werkzeug via two different URLs: a precise SHA, and `3.0.1`. Allow the precise SHA +/// 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: `main`, and a precise SHA, followed by `main` again. -/// We _may_ want to allow this, but we don't right now. +/// Request Werkzeug via two different URLs: `4.3.0`, and a precise SHA, followed by `4.3.0` again. #[test] fn compatible_repeated_narrowed_url_dependency() -> Result<()> { let context = TestContext::new("3.12"); 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() .arg("requirements.in"), @r###" - success: false - exit_code: 2 + 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 ----- - error: Requirements contain conflicting URLs for package `werkzeug`: - - git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f - - git+https://github.com/pallets/werkzeug.git@main + Resolved 3 packages in [TIME] "### ); Ok(()) } -/// Request Werkzeug via two different URLs: `main`, 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. +/// Request Werkzeug via two different URLs: `master`, and a precise SHA. Allow the precise SHA +/// to override the `master` branch, but error when we see yet another URL for the same package. #[test] fn incompatible_narrowed_url_dependency() -> Result<()> { let context = TestContext::new("3.12"); 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() .arg("requirements.in"), @r###" @@ -1735,9 +1797,9 @@ fn incompatible_narrowed_url_dependency() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Requirements contain conflicting URLs for package `werkzeug`: - - git+https://github.com/pallets/werkzeug@32e69512134c2f8183c6438b2b2e13fd24e9d19f - - git+https://github.com/pallets/werkzeug.git@3.0.1 + error: Requirements contain conflicting URLs for package `anyio`: + - git+https://github.com/agronholm/anyio.git@master + - git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51 "### );