Skip to content

Commit 692e3e5

Browse files
committed
Eagerly reject unsupported Git schemes
Initially, we were limiting Git schemes to HTTPS and SSH as only supported schemes. We lost this validation in #3429. This incidentally allow 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 * 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 a relative path, we can reopen.
1 parent 71bda82 commit 692e3e5

File tree

8 files changed

+126
-3
lines changed

8 files changed

+126
-3
lines changed

crates/uv-distribution-types/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ pub enum Error {
2121

2222
#[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")]
2323
PackageNameMismatch(PackageName, PackageName, String),
24+
25+
#[error("Unsupported git URL scheme `{0}:` in `{1}`, only `https:`, `ssh:` and `file:` are supported")]
26+
UnsupportedGitScheme(String, Url),
2427
}

crates/uv-distribution-types/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,15 @@ impl Dist {
474474
git: GitUrl,
475475
subdirectory: Option<PathBuf>,
476476
) -> Result<Dist, Error> {
477+
match url.scheme() {
478+
"https" | "ssh" | "file" => {}
479+
unsupported => {
480+
return Err(Error::UnsupportedGitScheme(
481+
unsupported.to_string(),
482+
url.to_url(),
483+
))
484+
}
485+
}
477486
Ok(Self::Source(SourceDist::Git(GitSourceDist {
478487
name,
479488
git: Box::new(git),

crates/uv-git/src/resolver.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ use std::path::PathBuf;
33
use std::str::FromStr;
44
use std::sync::Arc;
55

6-
use tracing::debug;
7-
8-
use crate::{Fetch, GitHubRepository, GitOid, GitReference, GitSource, GitUrl, Reporter};
96
use dashmap::mapref::one::Ref;
107
use dashmap::DashMap;
118
use fs_err::tokio as fs;
129
use reqwest_middleware::ClientWithMiddleware;
10+
use tracing::debug;
11+
use url::Url;
12+
1313
use uv_cache_key::{cache_digest, RepositoryUrl};
1414
use uv_fs::LockedFile;
1515
use uv_version::version;
1616

17+
use crate::{Fetch, GitHubRepository, GitOid, GitReference, GitSource, GitUrl, Reporter};
18+
1719
#[derive(Debug, thiserror::Error)]
1820
pub enum GitResolverError {
1921
#[error(transparent)]
@@ -26,6 +28,8 @@ pub enum GitResolverError {
2628
Reqwest(#[from] reqwest::Error),
2729
#[error(transparent)]
2830
ReqwestMiddleware(#[from] reqwest_middleware::Error),
31+
#[error("Unsupported git URL scheme `{0}:` in `{1}`, only `https:`, `ssh:` and `file:` are supported")]
32+
UnsupportedGitScheme(String, Url),
2933
}
3034

3135
/// A resolver for Git repositories.
@@ -112,6 +116,16 @@ impl GitResolver {
112116
) -> Result<Fetch, GitResolverError> {
113117
debug!("Fetching source distribution from Git: {url}");
114118

119+
match url.repository.scheme() {
120+
"https" | "ssh" | "file" => {}
121+
unsupported => {
122+
return Err(GitResolverError::UnsupportedGitScheme(
123+
unsupported.to_string(),
124+
url.repository().clone(),
125+
))
126+
}
127+
}
128+
115129
let reference = RepositoryReference::from(url);
116130

117131
// If we know the precise commit already, reuse it, to ensure that all fetches within a

crates/uv-requirements/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ pub(crate) fn required_dist(
6969
} else {
7070
GitUrl::from_reference(repository.clone(), reference.clone())
7171
};
72+
match url.scheme() {
73+
"https" | "ssh" | "file" => {}
74+
unsupported => {
75+
return Err(uv_distribution_types::Error::UnsupportedGitScheme(
76+
unsupported.to_string(),
77+
url.to_url(),
78+
))
79+
}
80+
}
7281
Dist::Source(SourceDist::Git(GitSourceDist {
7382
name: requirement.name.clone(),
7483
git: Box::new(git_url),

crates/uv-workspace/src/pyproject.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,18 @@ impl<'de> Deserialize<'de> for Source {
10561056
));
10571057
}
10581058

1059+
match git.scheme() {
1060+
"https" | "ssh" | "file" => {}
1061+
unsupported => {
1062+
return Err(serde::de::Error::custom(format!(
1063+
"Unsupported git URL scheme `{}:` in `{}`, \
1064+
only `https:`, `ssh:` and `file:` are supported",
1065+
unsupported.to_string(),
1066+
git.clone(),
1067+
)))
1068+
}
1069+
}
1070+
10591071
// At most one of `rev`, `tag`, or `branch` may be set.
10601072
match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
10611073
(None, None, None) => {}

crates/uv/tests/it/edit.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9743,3 +9743,21 @@ fn repeated_index_cli_reversed() -> Result<()> {
97439743

97449744
Ok(())
97459745
}
9746+
9747+
#[test]
9748+
fn add_unsupported_git_scheme() -> Result<()> {
9749+
let context = TestContext::new("3.12");
9750+
9751+
context.init().arg(".").assert().success();
9752+
9753+
uv_snapshot!(context.filters(), context.add().arg("git+fantasy://ferris/dreams/of/urls@7701ffcbae245819b828dc5f885a5201158897ef"), @r###"
9754+
success: false
9755+
exit_code: 2
9756+
----- stdout -----
9757+
9758+
----- stderr -----
9759+
error: Unsupported git URL scheme `fantasy:` in `fantasy://ferris/dreams/of/urls`, only `https:`, `ssh:` and `file:` are supported
9760+
"###);
9761+
9762+
Ok(())
9763+
}

crates/uv/tests/it/pip_install.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8776,3 +8776,27 @@ fn no_sources_workspace_discovery() -> Result<()> {
87768776

87778777
Ok(())
87788778
}
8779+
8780+
#[test]
8781+
fn unknown_git_schema() {
8782+
let context = TestContext::new("3.12");
8783+
// Reverse direction: Check that we switch back to the workspace package with `--upgrade`.
8784+
uv_snapshot!(context.filters(), context.pip_install()
8785+
.arg("git+fantasy:/foo"), @r###"
8786+
success: false
8787+
exit_code: 2
8788+
----- stdout -----
8789+
8790+
----- stderr -----
8791+
error: Git operation failed
8792+
Caused by: failed to clone into: [CACHE_DIR]/git-v0/db/e97623e3dc7167a2
8793+
Caused by: process didn't exit successfully: `/usr/bin/git fetch --force --update-head-ok 'fantasy:/foo' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128)
8794+
--- stderr
8795+
ssh: Could not resolve hostname fantasy: Name or service not known
8796+
fatal: Could not read from remote repository.
8797+
8798+
Please make sure you have the correct access rights
8799+
and the repository exists.
8800+
"###
8801+
);
8802+
}

crates/uv/tests/it/sync.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7615,3 +7615,37 @@ fn sync_locked_script() -> Result<()> {
76157615

76167616
Ok(())
76177617
}
7618+
7619+
#[test]
7620+
fn unsupported_git_scheme() -> Result<()> {
7621+
let context = TestContext::new_with_versions(&["3.12"]);
7622+
7623+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
7624+
pyproject_toml.write_str(indoc! {r#"
7625+
[project]
7626+
name = "foo"
7627+
version = "0.1.0"
7628+
requires-python = ">=3.12"
7629+
dependencies = ["foo"]
7630+
7631+
[tool.uv.sources]
7632+
# `c:/...` looks like an absolute path, but this field requires a URL such as `file:///...`.
7633+
foo = { git = "c:/home/ferris/projects/foo", rev = "7701ffcbae245819b828dc5f885a5201158897ef" }
7634+
"#},
7635+
)?;
7636+
7637+
uv_snapshot!(context.filters(), context.sync(), @r###"
7638+
success: false
7639+
exit_code: 2
7640+
----- stdout -----
7641+
7642+
----- stderr -----
7643+
error: Failed to parse: `pyproject.toml`
7644+
Caused by: TOML parse error at line 9, column 7
7645+
|
7646+
9 | foo = { git = "c:/home/ferris/projects/foo", rev = "7701ffcbae245819b828dc5f885a5201158897ef" }
7647+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7648+
Unsupported git URL scheme `c:` in `c:/home/ferris/projects/foo`, only `https:`, `ssh:` and `file:` are supported
7649+
"###);
7650+
Ok(())
7651+
}

0 commit comments

Comments
 (0)