Skip to content

Commit

Permalink
Allow Git URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Apr 2, 2024
1 parent 03b709a commit 5331729
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 156 deletions.
6 changes: 6 additions & 0 deletions crates/cache-key/src/canonical_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
139 changes: 136 additions & 3 deletions crates/uv-distribution/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand All @@ -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 {
Expand All @@ -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(())
}
}
1 change: 1 addition & 0 deletions crates/uv-distribution/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
5 changes: 5 additions & 0 deletions crates/uv-git/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions crates/uv-requirements/src/lookahead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/pubgrub/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit 5331729

Please sign in to comment.