Eagerly reject unsupported Git schemes (#11514)

Initially, we were limiting Git schemes to HTTPS and SSH as only
supported schemes. We lost this validation in #3429. This incidentally
allowed file schemes, which apparently work with Git out of the box.

A caveat for this is that in tool.uv.sources, we parse the git field
always as URL. This caused a problem with #11425: repo = { git =
'c:\path\to\repo', rev = "xxxxx" } was parsed as a URL where c: is the
scheme, causing a bad error message down the line.

This PR:

* Puts Git URL validation back in place. It bans everything but HTTPS,
SSH, and file URLs. This could be a breaking change, if users were using
a git transport protocol were not aware of, even though never
intentionally supported.
* Allows file: URL in Git: This seems to be supported by Git and we were
supporting it albeit unintentionally, so it's reasonable to continue to
support it.
* It does not allow relative paths in the git field in tool.uv.sources.
Absolute file URLs are supported, whether we want relative file URLs for
Git too should be discussed separately.

Closes #3429: We reject the input with a proper error message, while
hinting the user towards file:. If there's still desire for relative
path support, we can keep it open.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
konsti 2025-02-18 03:14:06 +01:00 committed by GitHub
parent 8c3a6b2155
commit 29c2be3e97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 212 additions and 171 deletions

1
Cargo.lock generated
View File

@ -5520,7 +5520,6 @@ dependencies = [
"uv-distribution-types", "uv-distribution-types",
"uv-fs", "uv-fs",
"uv-git", "uv-git",
"uv-git-types",
"uv-normalize", "uv-normalize",
"uv-pep508", "uv-pep508",
"uv-pypi-types", "uv-pypi-types",

View File

@ -714,9 +714,7 @@ impl GitSourceDist {
/// Return the [`ParsedUrl`] for the distribution. /// Return the [`ParsedUrl`] for the distribution.
pub fn parsed_url(&self) -> ParsedUrl { pub fn parsed_url(&self) -> ParsedUrl {
ParsedUrl::Git(ParsedGitUrl::from_source( ParsedUrl::Git(ParsedGitUrl::from_source(
self.git.repository().clone(), (*self.git).clone(),
self.git.reference().clone(),
self.git.precise(),
self.subdirectory.clone(), self.subdirectory.clone(),
)) ))
} }

View File

@ -266,10 +266,8 @@ impl From<&ResolvedDist> for RequirementSource {
} }
} }
Dist::Source(SourceDist::Git(sdist)) => RequirementSource::Git { Dist::Source(SourceDist::Git(sdist)) => RequirementSource::Git {
git: (*sdist.git).clone(),
url: sdist.url.clone(), url: sdist.url.clone(),
repository: sdist.git.repository().clone(),
reference: sdist.git.reference().clone(),
precise: sdist.git.precise(),
subdirectory: sdist.subdirectory.clone(), subdirectory: sdist.subdirectory.clone(),
}, },
Dist::Source(SourceDist::Path(sdist)) => RequirementSource::Path { Dist::Source(SourceDist::Path(sdist)) => RequirementSource::Path {

View File

@ -8,7 +8,7 @@ use url::Url;
use uv_distribution_filename::DistExtension; use uv_distribution_filename::DistExtension;
use uv_distribution_types::{Index, IndexLocations, IndexName, Origin}; use uv_distribution_types::{Index, IndexLocations, IndexName, Origin};
use uv_git_types::GitReference; use uv_git_types::{GitReference, GitUrl, GitUrlParseError};
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
use uv_pep508::{looks_like_git_repository, MarkerTree, VerbatimUrl, VersionOrUrl}; use uv_pep508::{looks_like_git_repository, MarkerTree, VerbatimUrl, VersionOrUrl};
@ -291,9 +291,7 @@ impl LoweredRequirement {
.expect("Workspace member must be relative"); .expect("Workspace member must be relative");
let subdirectory = uv_fs::normalize_path_buf(subdirectory); let subdirectory = uv_fs::normalize_path_buf(subdirectory);
RequirementSource::Git { RequirementSource::Git {
repository: git_member.git_source.git.repository().clone(), git: git_member.git_source.git.clone(),
reference: git_member.git_source.git.reference().clone(),
precise: git_member.git_source.git.precise(),
subdirectory: if subdirectory == PathBuf::new() { subdirectory: if subdirectory == PathBuf::new() {
None None
} else { } else {
@ -497,6 +495,8 @@ pub enum LoweringError {
UndeclaredWorkspacePackage(PackageName), UndeclaredWorkspacePackage(PackageName),
#[error("Can only specify one of: `rev`, `tag`, or `branch`")] #[error("Can only specify one of: `rev`, `tag`, or `branch`")]
MoreThanOneGitRef, MoreThanOneGitRef,
#[error(transparent)]
GitUrlParse(#[from] GitUrlParseError),
#[error("Package `{0}` references an undeclared index: `{1}`")] #[error("Package `{0}` references an undeclared index: `{1}`")]
MissingIndex(PackageName, IndexName), MissingIndex(PackageName, IndexName),
#[error("Workspace members are not allowed in non-workspace contexts")] #[error("Workspace members are not allowed in non-workspace contexts")]
@ -575,9 +575,7 @@ fn git_source(
Ok(RequirementSource::Git { Ok(RequirementSource::Git {
url, url,
repository, git: GitUrl::from_reference(repository, reference)?,
reference,
precise: None,
subdirectory, subdirectory,
}) })
} }
@ -679,9 +677,7 @@ fn path_source(
.expect("Workspace member must be relative"); .expect("Workspace member must be relative");
let subdirectory = uv_fs::normalize_path_buf(subdirectory); let subdirectory = uv_fs::normalize_path_buf(subdirectory);
return Ok(RequirementSource::Git { return Ok(RequirementSource::Git {
repository: git_member.git_source.git.repository().clone(), git: git_member.git_source.git.clone(),
reference: git_member.git_source.git.reference().clone(),
precise: git_member.git_source.git.precise(),
subdirectory: if subdirectory == PathBuf::new() { subdirectory: if subdirectory == PathBuf::new() {
None None
} else { } else {

View File

@ -1,12 +1,22 @@
pub use crate::github::GitHubRepository; pub use crate::github::GitHubRepository;
pub use crate::oid::{GitOid, OidParseError}; pub use crate::oid::{GitOid, OidParseError};
pub use crate::reference::GitReference; pub use crate::reference::GitReference;
use thiserror::Error;
use url::Url; use url::Url;
mod github; mod github;
mod oid; mod oid;
mod reference; mod reference;
#[derive(Debug, Error)]
pub enum GitUrlParseError {
#[error(
"Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)"
)]
UnsupportedGitScheme(String, Url),
}
/// A URL reference to a Git repository. /// A URL reference to a Git repository.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Hash, Ord)]
pub struct GitUrl { pub struct GitUrl {
@ -21,21 +31,42 @@ pub struct GitUrl {
impl GitUrl { impl GitUrl {
/// Create a new [`GitUrl`] from a repository URL and a reference. /// Create a new [`GitUrl`] from a repository URL and a reference.
pub fn from_reference(repository: Url, reference: GitReference) -> Self { pub fn from_reference(
Self { repository: Url,
repository, reference: GitReference,
reference, ) -> Result<Self, GitUrlParseError> {
precise: None, Self::from_fields(repository, reference, None)
}
} }
/// Create a new [`GitUrl`] from a repository URL and a precise commit. /// Create a new [`GitUrl`] from a repository URL and a precise commit.
pub fn from_commit(repository: Url, reference: GitReference, precise: GitOid) -> Self { pub fn from_commit(
Self { repository: Url,
reference: GitReference,
precise: GitOid,
) -> Result<Self, GitUrlParseError> {
Self::from_fields(repository, reference, Some(precise))
}
/// Create a new [`GitUrl`] from a repository URL and a precise commit, if known.
pub fn from_fields(
repository: Url,
reference: GitReference,
precise: Option<GitOid>,
) -> Result<Self, GitUrlParseError> {
match repository.scheme() {
"https" | "ssh" | "file" => {}
unsupported => {
return Err(GitUrlParseError::UnsupportedGitScheme(
unsupported.to_string(),
repository,
))
}
}
Ok(Self {
repository, repository,
reference, reference,
precise: Some(precise), precise,
} })
} }
/// Set the precise [`GitOid`] to use for this Git URL. /// Set the precise [`GitOid`] to use for this Git URL.
@ -69,7 +100,7 @@ impl GitUrl {
} }
impl TryFrom<Url> for GitUrl { impl TryFrom<Url> for GitUrl {
type Error = OidParseError; type Error = GitUrlParseError;
/// Initialize a [`GitUrl`] source from a URL. /// Initialize a [`GitUrl`] source from a URL.
fn try_from(mut url: Url) -> Result<Self, Self::Error> { fn try_from(mut url: Url) -> Result<Self, Self::Error> {
@ -89,7 +120,7 @@ impl TryFrom<Url> for GitUrl {
url.set_path(&prefix); url.set_path(&prefix);
} }
Ok(Self::from_reference(url, reference)) Self::from_reference(url, reference)
} }
} }

View File

@ -97,9 +97,7 @@ impl RequirementSatisfaction {
} }
RequirementSource::Git { RequirementSource::Git {
url: _, url: _,
repository: requested_repository, git: requested_git,
reference: _,
precise: requested_precise,
subdirectory: requested_subdirectory, subdirectory: requested_subdirectory,
} => { } => {
let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution let InstalledDist::Url(InstalledDirectUrlDist { direct_url, .. }) = &distribution
@ -129,21 +127,25 @@ impl RequirementSatisfaction {
} }
if !RepositoryUrl::parse(installed_url).is_ok_and(|installed_url| { if !RepositoryUrl::parse(installed_url).is_ok_and(|installed_url| {
installed_url == RepositoryUrl::new(requested_repository) installed_url == RepositoryUrl::new(requested_git.repository())
}) { }) {
debug!( debug!(
"Repository mismatch: {:?} vs. {:?}", "Repository mismatch: {:?} vs. {:?}",
installed_url, requested_repository installed_url,
requested_git.repository()
); );
return Ok(Self::Mismatch); return Ok(Self::Mismatch);
} }
// TODO(charlie): It would be more consistent for us to compare the requested // TODO(charlie): It would be more consistent for us to compare the requested
// revisions here. // revisions here.
if installed_precise.as_deref() != requested_precise.as_ref().map(GitOid::as_str) { if installed_precise.as_deref()
!= requested_git.precise().as_ref().map(GitOid::as_str)
{
debug!( debug!(
"Precise mismatch: {:?} vs. {:?}", "Precise mismatch: {:?} vs. {:?}",
installed_precise, requested_precise installed_precise,
requested_git.precise()
); );
return Ok(Self::OutOfDate); return Ok(Self::OutOfDate);
} }

View File

@ -5,7 +5,7 @@ use thiserror::Error;
use url::{ParseError, Url}; use url::{ParseError, Url};
use uv_distribution_filename::{DistExtension, ExtensionError}; use uv_distribution_filename::{DistExtension, ExtensionError};
use uv_git_types::{GitOid, GitReference, GitUrl, OidParseError}; use uv_git_types::{GitUrl, GitUrlParseError};
use uv_pep508::{ use uv_pep508::{
looks_like_git_repository, Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError, looks_like_git_repository, Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError,
}; };
@ -22,8 +22,8 @@ pub enum ParsedUrlError {
}, },
#[error("Invalid path in file URL: `{0}`")] #[error("Invalid path in file URL: `{0}`")]
InvalidFileUrl(String), InvalidFileUrl(String),
#[error("Failed to parse Git reference from URL: `{0}`")] #[error(transparent)]
GitOidParse(String, #[source] OidParseError), GitUrlParse(#[from] GitUrlParseError),
#[error("Not a valid URL: `{0}`")] #[error("Not a valid URL: `{0}`")]
UrlParse(String, #[source] ParseError), UrlParse(String, #[source] ParseError),
#[error(transparent)] #[error(transparent)]
@ -244,17 +244,7 @@ pub struct ParsedGitUrl {
impl ParsedGitUrl { impl ParsedGitUrl {
/// Construct a [`ParsedGitUrl`] from a Git requirement source. /// Construct a [`ParsedGitUrl`] from a Git requirement source.
pub fn from_source( pub fn from_source(url: GitUrl, subdirectory: Option<PathBuf>) -> Self {
repository: Url,
reference: GitReference,
precise: Option<GitOid>,
subdirectory: Option<PathBuf>,
) -> Self {
let url = if let Some(precise) = precise {
GitUrl::from_commit(repository, reference, precise)
} else {
GitUrl::from_reference(repository, reference)
};
Self { url, subdirectory } Self { url, subdirectory }
} }
} }
@ -274,8 +264,7 @@ impl TryFrom<Url> for ParsedGitUrl {
.strip_prefix("git+") .strip_prefix("git+")
.unwrap_or(url_in.as_str()); .unwrap_or(url_in.as_str());
let url = Url::parse(url).map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?; let url = Url::parse(url).map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?;
let url = GitUrl::try_from(url) let url = GitUrl::try_from(url)?;
.map_err(|err| ParsedUrlError::GitOidParse(url_in.to_string(), err))?;
Ok(Self { url, subdirectory }) Ok(Self { url, subdirectory })
} }
} }

View File

@ -8,7 +8,7 @@ use url::Url;
use uv_distribution_filename::DistExtension; use uv_distribution_filename::DistExtension;
use uv_fs::{relative_to, PortablePath, PortablePathBuf, CWD}; use uv_fs::{relative_to, PortablePath, PortablePathBuf, CWD};
use uv_git_types::{GitOid, GitReference, GitUrl, OidParseError}; use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::VersionSpecifiers; use uv_pep440::VersionSpecifiers;
use uv_pep508::{ use uv_pep508::{
@ -30,6 +30,8 @@ pub enum RequirementError {
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error(transparent)] #[error(transparent)]
OidParseError(#[from] OidParseError), OidParseError(#[from] OidParseError),
#[error(transparent)]
GitUrlParse(#[from] GitUrlParseError),
} }
/// A representation of dependency on a package, an extension over a PEP 508's requirement. /// A representation of dependency on a package, an extension over a PEP 508's requirement.
@ -226,25 +228,16 @@ impl From<Requirement> for uv_pep508::Requirement<VerbatimParsedUrl> {
verbatim: url, verbatim: url,
})), })),
RequirementSource::Git { RequirementSource::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url, url,
} => { } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
let git_url = if let Some(precise) = precise {
GitUrl::from_commit(repository, reference, precise)
} else {
GitUrl::from_reference(repository, reference)
};
Some(VersionOrUrl::Url(VerbatimParsedUrl {
parsed_url: ParsedUrl::Git(ParsedGitUrl { parsed_url: ParsedUrl::Git(ParsedGitUrl {
url: git_url, url: git,
subdirectory, subdirectory,
}), }),
verbatim: url, verbatim: url,
})) })),
}
RequirementSource::Path { RequirementSource::Path {
install_path, install_path,
ext, ext,
@ -336,13 +329,11 @@ impl Display for Requirement {
} }
RequirementSource::Git { RequirementSource::Git {
url: _, url: _,
repository, git,
reference,
precise: _,
subdirectory, subdirectory,
} => { } => {
write!(f, " @ git+{repository}")?; write!(f, " @ git+{}", git.repository())?;
if let Some(reference) = reference.as_str() { if let Some(reference) = git.reference().as_str() {
write!(f, "@{reference}")?; write!(f, "@{reference}")?;
} }
if let Some(subdirectory) = subdirectory { if let Some(subdirectory) = subdirectory {
@ -401,12 +392,8 @@ pub enum RequirementSource {
}, },
/// A remote Git repository, over either HTTPS or SSH. /// A remote Git repository, over either HTTPS or SSH.
Git { Git {
/// The repository URL (without the `git+` prefix). /// The repository URL and reference to the commit to use.
repository: Url, git: GitUrl,
/// Optionally, the revision, tag, or branch to use.
reference: GitReference,
/// The precise commit to use, if known.
precise: Option<GitOid>,
/// The path to the source distribution if it is not in the repository root. /// The path to the source distribution if it is not in the repository root.
subdirectory: Option<PathBuf>, subdirectory: Option<PathBuf>,
/// The PEP 508 style url in the format /// The PEP 508 style url in the format
@ -457,10 +444,8 @@ impl RequirementSource {
url, url,
}, },
ParsedUrl::Git(git) => RequirementSource::Git { ParsedUrl::Git(git) => RequirementSource::Git {
git: git.url.clone(),
url, url,
repository: git.url.repository().clone(),
reference: git.url.reference().clone(),
precise: git.url.precise(),
subdirectory: git.subdirectory, subdirectory: git.subdirectory,
}, },
ParsedUrl::Archive(archive) => RequirementSource::Url { ParsedUrl::Archive(archive) => RequirementSource::Url {
@ -516,16 +501,12 @@ impl RequirementSource {
verbatim: url.clone(), verbatim: url.clone(),
}), }),
Self::Git { Self::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url, url,
} => Some(VerbatimParsedUrl { } => Some(VerbatimParsedUrl {
parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source( parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source(
repository.clone(), git.clone(),
reference.clone(),
*precise,
subdirectory.clone(), subdirectory.clone(),
)), )),
verbatim: url.clone(), verbatim: url.clone(),
@ -628,13 +609,11 @@ impl Display for RequirementSource {
} }
Self::Git { Self::Git {
url: _, url: _,
repository, git,
reference,
precise: _,
subdirectory, subdirectory,
} => { } => {
write!(f, " git+{repository}")?; write!(f, " git+{}", git.repository())?;
if let Some(reference) = reference.as_str() { if let Some(reference) = git.reference().as_str() {
write!(f, "@{reference}")?; write!(f, "@{reference}")?;
} }
if let Some(subdirectory) = subdirectory { if let Some(subdirectory) = subdirectory {
@ -706,13 +685,11 @@ impl From<RequirementSource> for RequirementSourceWire {
subdirectory: subdirectory.map(PortablePathBuf::from), subdirectory: subdirectory.map(PortablePathBuf::from),
}, },
RequirementSource::Git { RequirementSource::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url: _, url: _,
} => { } => {
let mut url = repository; let mut url = git.repository().clone();
// Redact the credentials. // Redact the credentials.
redact_credentials(&mut url); redact_credentials(&mut url);
@ -733,7 +710,7 @@ impl From<RequirementSource> for RequirementSourceWire {
} }
// Put the requested reference in the query. // Put the requested reference in the query.
match reference { match git.reference() {
GitReference::Branch(branch) => { GitReference::Branch(branch) => {
url.query_pairs_mut().append_pair("branch", branch.as_str()); url.query_pairs_mut().append_pair("branch", branch.as_str());
} }
@ -749,7 +726,7 @@ impl From<RequirementSource> for RequirementSourceWire {
} }
// Put the precise commit in the fragment. // Put the precise commit in the fragment.
if let Some(precise) = precise { if let Some(precise) = git.precise() {
url.set_fragment(Some(&precise.to_string())); url.set_fragment(Some(&precise.to_string()));
} }
@ -839,9 +816,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
let url = VerbatimUrl::from_url(url); let url = VerbatimUrl::from_url(url);
Ok(Self::Git { Ok(Self::Git {
repository, git: GitUrl::from_fields(repository, reference, precise)?,
reference,
precise,
subdirectory: subdirectory.map(PathBuf::from), subdirectory: subdirectory.map(PathBuf::from),
url, url,
}) })

View File

@ -25,7 +25,6 @@ uv-distribution-filename = { workspace = true }
uv-distribution-types = { workspace = true } uv-distribution-types = { workspace = true }
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-git = { workspace = true } uv-git = { workspace = true }
uv-git-types = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-pep508 = { workspace = true } uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }

View File

@ -6,7 +6,6 @@ pub use crate::specification::*;
pub use crate::unnamed::*; pub use crate::unnamed::*;
use uv_distribution_types::{Dist, DistErrorKind, GitSourceDist, SourceDist}; use uv_distribution_types::{Dist, DistErrorKind, GitSourceDist, SourceDist};
use uv_git_types::GitUrl;
use uv_pypi_types::{Requirement, RequirementSource}; use uv_pypi_types::{Requirement, RequirementSource};
mod extras; mod extras;
@ -58,24 +57,15 @@ pub(crate) fn required_dist(
*ext, *ext,
)?, )?,
RequirementSource::Git { RequirementSource::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url, url,
} => { } => Dist::Source(SourceDist::Git(GitSourceDist {
let git_url = if let Some(precise) = precise {
GitUrl::from_commit(repository.clone(), reference.clone(), *precise)
} else {
GitUrl::from_reference(repository.clone(), reference.clone())
};
Dist::Source(SourceDist::Git(GitSourceDist {
name: requirement.name.clone(), name: requirement.name.clone(),
git: Box::new(git_url), git: Box::new(git.clone()),
subdirectory: subdirectory.clone(), subdirectory: subdirectory.clone(),
url: url.clone(), url: url.clone(),
})) })),
}
RequirementSource::Path { RequirementSource::Path {
install_path, install_path,
ext, ext,

View File

@ -32,7 +32,7 @@ use uv_distribution_types::{
}; };
use uv_fs::{relative_to, PortablePath, PortablePathBuf}; use uv_fs::{relative_to, PortablePath, PortablePathBuf};
use uv_git::{RepositoryReference, ResolvedRepositoryReference}; use uv_git::{RepositoryReference, ResolvedRepositoryReference};
use uv_git_types::{GitOid, GitReference}; use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError};
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::Version; use uv_pep440::Version;
use uv_pep508::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; use uv_pep508::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError};
@ -2273,7 +2273,7 @@ impl Package {
url, url,
GitReference::from(git.kind.clone()), GitReference::from(git.kind.clone()),
git.precise, git.precise,
); )?;
// Reconstruct the PEP 508-compatible URL from the `GitSource`. // Reconstruct the PEP 508-compatible URL from the `GitSource`.
let url = Url::from(ParsedGitUrl { let url = Url::from(ParsedGitUrl {
@ -4331,12 +4331,14 @@ fn normalize_requirement(
// Normalize the requirement source. // Normalize the requirement source.
match requirement.source { match requirement.source {
RequirementSource::Git { RequirementSource::Git {
mut repository, git,
reference,
precise,
subdirectory, subdirectory,
url: _, url: _,
} => { } => {
// Reconstruct the Git URL.
let git = {
let mut repository = git.repository().clone();
// Redact the credentials. // Redact the credentials.
redact_credentials(&mut repository); redact_credentials(&mut repository);
@ -4344,9 +4346,12 @@ fn normalize_requirement(
repository.set_fragment(None); repository.set_fragment(None);
repository.set_query(None); repository.set_query(None);
GitUrl::from_fields(repository, git.reference().clone(), git.precise())?
};
// Reconstruct the PEP 508 URL from the underlying data. // Reconstruct the PEP 508 URL from the underlying data.
let url = Url::from(ParsedGitUrl { let url = Url::from(ParsedGitUrl {
url: uv_git_types::GitUrl::from_reference(repository.clone(), reference.clone()), url: git.clone(),
subdirectory: subdirectory.clone(), subdirectory: subdirectory.clone(),
}); });
@ -4356,9 +4361,7 @@ fn normalize_requirement(
groups: requirement.groups, groups: requirement.groups,
marker: requirement.marker, marker: requirement.marker,
source: RequirementSource::Git { source: RequirementSource::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url: VerbatimUrl::from_url(url), url: VerbatimUrl::from_url(url),
}, },
@ -5036,6 +5039,8 @@ enum LockErrorKind {
package2: PackageName, package2: PackageName,
extra2: ExtraName, extra2: ExtraName,
}, },
#[error(transparent)]
GitUrlParse(#[from] GitUrlParseError),
} }
/// An error that occurs when a source string could not be parsed. /// An error that occurs when a source string could not be parsed.

View File

@ -317,7 +317,8 @@ impl std::fmt::Display for RequirementsTxtExport<'_> {
url, url,
GitReference::from(git.kind.clone()), GitReference::from(git.kind.clone()),
git.precise, git.precise,
); )
.expect("Internal Git URLs must have supported schemes");
// Reconstruct the PEP 508-compatible URL from the `GitSource`. // Reconstruct the PEP 508-compatible URL from the `GitSource`.
let url = Url::from(ParsedGitUrl { let url = Url::from(ParsedGitUrl {

View File

@ -181,18 +181,12 @@ impl PubGrubRequirement {
(url, parsed_url) (url, parsed_url)
} }
RequirementSource::Git { RequirementSource::Git {
repository, git,
reference,
precise,
url, url,
subdirectory, subdirectory,
} => { } => {
let parsed_url = ParsedUrl::Git(ParsedGitUrl::from_source( let parsed_url =
repository.clone(), ParsedUrl::Git(ParsedGitUrl::from_source(git.clone(), subdirectory.clone()));
reference.clone(),
*precise,
subdirectory.clone(),
));
(url, parsed_url) (url, parsed_url)
} }
RequirementSource::Path { RequirementSource::Path {

View File

@ -6,10 +6,10 @@
//! //!
//! Then lowers them into a dependency specification. //! Then lowers them into a dependency specification.
use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use std::{collections::BTreeMap, mem};
use glob::Pattern; use glob::Pattern;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
@ -1500,25 +1500,22 @@ impl Source {
group: None, group: None,
}, },
RequirementSource::Git { RequirementSource::Git {
repository, git, subdirectory, ..
mut reference,
subdirectory,
..
} => { } => {
if rev.is_none() && tag.is_none() && branch.is_none() { if rev.is_none() && tag.is_none() && branch.is_none() {
let rev = match reference { let rev = match git.reference() {
GitReference::Branch(ref mut rev) => Some(mem::take(rev)), GitReference::Branch(rev) => Some(rev),
GitReference::Tag(ref mut rev) => Some(mem::take(rev)), GitReference::Tag(rev) => Some(rev),
GitReference::BranchOrTag(ref mut rev) => Some(mem::take(rev)), GitReference::BranchOrTag(rev) => Some(rev),
GitReference::BranchOrTagOrCommit(ref mut rev) => Some(mem::take(rev)), GitReference::BranchOrTagOrCommit(rev) => Some(rev),
GitReference::NamedRef(ref mut rev) => Some(mem::take(rev)), GitReference::NamedRef(rev) => Some(rev),
GitReference::DefaultBranch => None, GitReference::DefaultBranch => None,
}; };
Source::Git { Source::Git {
rev, rev: rev.cloned(),
tag, tag,
branch, branch,
git: repository, git: git.repository().clone(),
subdirectory: subdirectory.map(PortablePathBuf::from), subdirectory: subdirectory.map(PortablePathBuf::from),
marker: MarkerTree::TRUE, marker: MarkerTree::TRUE,
extra: None, extra: None,
@ -1529,7 +1526,7 @@ impl Source {
rev, rev,
tag, tag,
branch, branch,
git: repository, git: git.repository().clone(),
subdirectory: subdirectory.map(PortablePathBuf::from), subdirectory: subdirectory.map(PortablePathBuf::from),
marker: MarkerTree::TRUE, marker: MarkerTree::TRUE,
extra: None, extra: None,

View File

@ -907,25 +907,21 @@ fn augment_requirement(
UnresolvedRequirement::Named(uv_pypi_types::Requirement { UnresolvedRequirement::Named(uv_pypi_types::Requirement {
source: match requirement.source { source: match requirement.source {
RequirementSource::Git { RequirementSource::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url, url,
} => { } => {
let reference = if let Some(rev) = rev { let git = if let Some(rev) = rev {
GitReference::from_rev(rev.to_string()) git.with_reference(GitReference::from_rev(rev.to_string()))
} else if let Some(tag) = tag { } else if let Some(tag) = tag {
GitReference::Tag(tag.to_string()) git.with_reference(GitReference::Tag(tag.to_string()))
} else if let Some(branch) = branch { } else if let Some(branch) = branch {
GitReference::Branch(branch.to_string()) git.with_reference(GitReference::Branch(branch.to_string()))
} else { } else {
reference git
}; };
RequirementSource::Git { RequirementSource::Git {
repository, git,
reference,
precise,
subdirectory, subdirectory,
url, url,
} }

View File

@ -9770,3 +9770,23 @@ fn add_with_build_constraints() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
#[cfg(feature = "git")]
fn add_unsupported_git_scheme() {
let context = TestContext::new("3.12");
context.init().arg(".").assert().success();
uv_snapshot!(context.filters(), context.add().arg("git+fantasy://ferris/dreams/of/urls@7701ffcbae245819b828dc5f885a5201158897ef"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `git+fantasy://ferris/dreams/of/urls@7701ffcbae245819b828dc5f885a5201158897ef`
Caused by: Unsupported Git URL scheme `fantasy:` in `fantasy://ferris/dreams/of/urls` (expected one of `https:`, `ssh:`, or `file:`)
git+fantasy://ferris/dreams/of/urls@7701ffcbae245819b828dc5f885a5201158897ef
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"###);
}

View File

@ -8940,3 +8940,21 @@ fn no_sources_workspace_discovery() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn unsupported_git_scheme() {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.pip_install()
.arg("git+fantasy://foo"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `git+fantasy://foo`
Caused by: Unsupported Git URL scheme `fantasy:` in `fantasy://foo` (expected one of `https:`, `ssh:`, or `file:`)
git+fantasy://foo
^^^^^^^^^^^^^^^^^
"###
);
}

View File

@ -7681,3 +7681,36 @@ fn sync_locked_script() -> Result<()> {
Ok(()) Ok(())
} }
#[test]
fn unsupported_git_scheme() -> Result<()> {
let context = TestContext::new_with_versions(&["3.12"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["foo"]
[tool.uv.sources]
# `c:/...` looks like an absolute path, but this field requires a URL such as `file:///...`.
foo = { git = "c:/home/ferris/projects/foo", rev = "7701ffcbae245819b828dc5f885a5201158897ef" }
"#},
)?;
uv_snapshot!(context.filters(), context.sync(), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
× Failed to build `foo @ file://[TEMP_DIR]/`
Failed to parse entry: `foo`
Unsupported Git URL scheme `c:` in `c:/home/ferris/projects/foo` (expected one of `https:`, `ssh:`, or `file:`)
"###);
Ok(())
}