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 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)

View file

@ -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;
@ -39,7 +40,7 @@ pub(crate) async fn fetch_git_archive(
fs::create_dir_all(&lock_dir)
.await
.map_err(Error::CacheWrite)?;
let canonical_url = cache_key::CanonicalUrl::new(url);
let canonical_url = CanonicalUrl::new(url);
let _lock = LockedFile::acquire(
lock_dir.join(cache_key::digest(&canonical_url)),
&canonical_url,
@ -91,9 +92,8 @@ pub(crate) async fn resolve_precise(
cache: &Cache,
reporter: Option<&Arc<dyn Reporter>>,
) -> Result<Option<Url>, 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<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 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};

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.
pub fn precise(&self) -> Option<GitSha> {
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 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));
}
}

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(
requirement.name.clone(),
expected.verbatim().to_string(),

View file

@ -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<PackageName, VerbatimUrl>,
/// A map from required URL to URL that is assumed to be a less precise variant.
allowed: FxHashMap<VerbatimUrl, VerbatimUrl>,
}
pub(crate) struct Urls(FxHashMap<PackageName, VerbatimUrl>);
impl Urls {
pub(crate) fn from_manifest(
manifest: &Manifest,
markers: &MarkerEnvironment,
) -> Result<Self, ResolveError> {
let mut required: FxHashMap<PackageName, VerbatimUrl> = FxHashMap::default();
let mut allowed: FxHashMap<VerbatimUrl, VerbatimUrl> = FxHashMap::default();
let mut urls: FxHashMap<PackageName, VerbatimUrl> = 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 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(),
));
}
}
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.
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(())
}
}

View file

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

View file

@ -582,7 +582,7 @@ fn install_git_tag() -> Result<()> {
Resolved 1 package in [TIME]
Downloaded 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)
"###
);