Add `lfs = true` support in Git source declarations

This commit is contained in:
Zanie Blue 2025-08-27 15:17:20 -05:00
parent 7d49571336
commit 8a04e1b88d
13 changed files with 308 additions and 15 deletions

View File

@ -7,7 +7,7 @@ use thiserror::Error;
use uv_cache_key::{CacheKey, CacheKeyHasher};
use uv_distribution_filename::DistExtension;
use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
use uv_git_types::{GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep440::VersionSpecifiers;
use uv_pep508::{
@ -965,7 +965,7 @@ impl TryFrom<RequirementSourceWire> for RequirementSource {
let url = VerbatimUrl::from_url(url);
Ok(Self::Git {
git: GitUrl::from_fields(repository, reference, precise)?,
git: GitUrl::from_fields(repository, reference, precise, GitLfs::from_env())?,
subdirectory: subdirectory.map(Box::<Path>::from),
url,
})

View File

@ -166,6 +166,7 @@ impl LoweredRequirement {
rev,
tag,
branch,
lfs,
marker,
..
} => {
@ -175,6 +176,7 @@ impl LoweredRequirement {
rev,
tag,
branch,
lfs,
)?;
(source, marker)
}
@ -406,6 +408,7 @@ impl LoweredRequirement {
rev,
tag,
branch,
lfs,
marker,
..
} => {
@ -415,6 +418,7 @@ impl LoweredRequirement {
rev,
tag,
branch,
lfs,
)?;
(source, marker)
}
@ -580,6 +584,7 @@ fn git_source(
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
lfs: Option<bool>,
) -> Result<RequirementSource, LoweringError> {
let reference = match (rev, tag, branch) {
(None, None, None) => GitReference::DefaultBranch,
@ -607,7 +612,7 @@ fn git_source(
Ok(RequirementSource::Git {
url,
git: GitUrl::from_reference(repository, reference)?,
git: GitUrl::from_fields(repository, reference, None, lfs.into())?,
subdirectory,
})
}

View File

@ -540,13 +540,13 @@ mod test {
tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
"#};
assert_snapshot!(format_err(input).await, @r###"
assert_snapshot!(format_err(input).await, @r#"
error: TOML parse error at line 8, column 48
|
8 | tqdm = { git = "https://github.com/tqdm/tqdm", ref = "baaaaaab" }
| ^^^
unknown field `ref`, expected one of `git`, `subdirectory`, `rev`, `tag`, `branch`, `url`, `path`, `editable`, `package`, `index`, `workspace`, `marker`, `extra`, `group`
"###);
unknown field `ref`, expected one of `git`, `subdirectory`, `rev`, `tag`, `branch`, `lfs`, `url`, `path`, `editable`, `package`, `index`, `workspace`, `marker`, `extra`, `group`
"#);
}
#[tokio::test]

View File

@ -9,6 +9,48 @@ mod github;
mod oid;
mod reference;
/// Configuration for Git LFS (Large File Storage) support.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum GitLfs {
/// Git LFS is disabled (default).
#[default]
Disabled,
/// Git LFS is enabled.
Enabled,
}
impl GitLfs {
/// Create a `GitLfs` configuration from environment variables.
pub fn from_env() -> Self {
if std::env::var("UV_GIT_LFS").is_ok() {
Self::Enabled
} else {
Self::Disabled
}
}
/// Returns true if LFS is enabled.
pub fn enabled(self) -> bool {
matches!(self, Self::Enabled)
}
}
impl From<Option<bool>> for GitLfs {
fn from(value: Option<bool>) -> Self {
match value {
Some(true) => Self::Enabled,
Some(false) => Self::Disabled,
None => Self::from_env(),
}
}
}
impl From<bool> for GitLfs {
fn from(value: bool) -> Self {
if value { Self::Enabled } else { Self::Disabled }
}
}
#[derive(Debug, Error)]
pub enum GitUrlParseError {
#[error(
@ -27,6 +69,8 @@ pub struct GitUrl {
reference: GitReference,
/// The precise commit to use, if known.
precise: Option<GitOid>,
/// Git LFS configuration for this repository.
lfs: GitLfs,
}
impl GitUrl {
@ -35,7 +79,7 @@ impl GitUrl {
repository: DisplaySafeUrl,
reference: GitReference,
) -> Result<Self, GitUrlParseError> {
Self::from_fields(repository, reference, None)
Self::from_fields(repository, reference, None, GitLfs::from_env())
}
/// Create a new [`GitUrl`] from a repository URL and a precise commit.
@ -44,7 +88,7 @@ impl GitUrl {
reference: GitReference,
precise: GitOid,
) -> Result<Self, GitUrlParseError> {
Self::from_fields(repository, reference, Some(precise))
Self::from_fields(repository, reference, Some(precise), GitLfs::from_env())
}
/// Create a new [`GitUrl`] from a repository URL and a precise commit, if known.
@ -52,6 +96,7 @@ impl GitUrl {
repository: DisplaySafeUrl,
reference: GitReference,
precise: Option<GitOid>,
lfs: GitLfs,
) -> Result<Self, GitUrlParseError> {
match repository.scheme() {
"http" | "https" | "ssh" | "file" => {}
@ -66,6 +111,7 @@ impl GitUrl {
repository,
reference,
precise,
lfs,
})
}
@ -97,6 +143,18 @@ impl GitUrl {
pub fn precise(&self) -> Option<GitOid> {
self.precise
}
/// Return the Git LFS configuration.
pub fn lfs(&self) -> GitLfs {
self.lfs
}
/// Set the Git LFS configuration.
#[must_use]
pub fn with_lfs(mut self, lfs: GitLfs) -> Self {
self.lfs = lfs;
self
}
}
impl TryFrom<DisplaySafeUrl> for GitUrl {

View File

@ -1,7 +1,6 @@
//! Git support is derived from Cargo's implementation.
//! Cargo is dual-licensed under either Apache 2.0 or MIT, at the user's choice.
//! Source: <https://github.com/rust-lang/cargo/blob/23eb492cf920ce051abfc56bbaf838514dc8365c/src/cargo/sources/git/utils.rs>
use std::env;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::str::{self};
@ -15,7 +14,7 @@ use tracing::{debug, warn};
use url::Url;
use uv_fs::Simplified;
use uv_git_types::{GitHubRepository, GitOid, GitReference};
use uv_git_types::{GitHubRepository, GitLfs, GitOid, GitReference};
use uv_redacted::DisplaySafeUrl;
use uv_static::EnvVars;
use uv_version::version;
@ -238,12 +237,11 @@ impl GitRemote {
client: &ClientWithMiddleware,
disable_ssl: bool,
offline: bool,
lfs: GitLfs,
) -> Result<(GitDatabase, GitOid)> {
let reference = locked_rev
.map(ReferenceOrOid::Oid)
.unwrap_or(ReferenceOrOid::Reference(reference));
let enable_lfs_fetch = env::var(EnvVars::UV_GIT_LFS).is_ok();
if let Some(mut db) = db {
fetch(
&mut db.repo,
@ -261,7 +259,7 @@ impl GitRemote {
};
if let Some(rev) = resolved_commit_hash {
if enable_lfs_fetch {
if lfs.enabled() {
fetch_lfs(&mut db.repo, &self.url, &rev, disable_ssl)
.with_context(|| format!("failed to fetch LFS objects at {rev}"))?;
}
@ -293,7 +291,7 @@ impl GitRemote {
Some(rev) => rev,
None => reference.resolve(&repo)?,
};
if enable_lfs_fetch {
if lfs.enabled() {
fetch_lfs(&mut repo, &self.url, &rev, disable_ssl)
.with_context(|| format!("failed to fetch LFS objects at {rev}"))?;
}

View File

@ -135,6 +135,7 @@ impl GitSource {
&self.client,
self.disable_ssl,
self.offline,
self.git.lfs(),
)?;
Ok((db, actual_rev, task))

View File

@ -56,6 +56,7 @@ RequirementsTxt {
},
reference: DefaultBranch,
precise: None,
lfs: Disabled,
},
subdirectory: None,
},

View File

@ -56,6 +56,7 @@ RequirementsTxt {
},
reference: DefaultBranch,
precise: None,
lfs: Disabled,
},
subdirectory: None,
},

View File

@ -4987,7 +4987,12 @@ fn normalize_requirement(
repository.set_fragment(None);
repository.set_query(None);
GitUrl::from_fields(repository, git.reference().clone(), git.precise())?
GitUrl::from_fields(
repository,
git.reference().clone(),
git.precise(),
git.lfs(),
)?
};
// Reconstruct the PEP 508 URL from the underlying data.

View File

@ -1139,6 +1139,8 @@ pub enum Source {
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
/// Whether to use Git LFS when cloning the repository.
lfs: Option<bool>,
#[serde(
skip_serializing_if = "uv_pep508::marker::ser::is_empty",
serialize_with = "uv_pep508::marker::ser::serialize",
@ -1235,6 +1237,7 @@ impl<'de> Deserialize<'de> for Source {
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
lfs: Option<bool>,
url: Option<DisplaySafeUrl>,
path: Option<PortablePathBuf>,
editable: Option<bool>,
@ -1258,6 +1261,7 @@ impl<'de> Deserialize<'de> for Source {
rev,
tag,
branch,
lfs,
url,
path,
editable,
@ -1335,6 +1339,7 @@ impl<'de> Deserialize<'de> for Source {
rev,
tag,
branch,
lfs,
marker,
extra,
group,
@ -1652,6 +1657,7 @@ impl Source {
rev,
tag,
branch,
lfs: None,
marker: *marker,
extra: extra.clone(),
group: group.clone(),
@ -1765,6 +1771,7 @@ impl Source {
rev: rev.cloned(),
tag,
branch,
lfs: None,
git: git.repository().clone(),
subdirectory: subdirectory.map(PortablePathBuf::from),
marker: MarkerTree::TRUE,
@ -1776,6 +1783,7 @@ impl Source {
rev,
tag,
branch,
lfs: None,
git: git.repository().clone(),
subdirectory: subdirectory.map(PortablePathBuf::from),
marker: MarkerTree::TRUE,

View File

@ -852,6 +852,7 @@ fn edits(
rev,
tag,
branch,
lfs,
marker,
extra,
group,
@ -870,6 +871,7 @@ fn edits(
rev,
tag,
branch,
lfs,
marker,
extra,
group,

View File

@ -13370,6 +13370,213 @@ fn reject_unmatched_runtime() -> Result<()> {
Ok(())
}
/// Test Git LFS configuration.
#[test]
#[cfg(feature = "git")]
fn sync_git_lfs() -> Result<()> {
let context = TestContext::new("3.13");
// Set `lfs = true` in the source
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = ["test-lfs-repo"]
[tool.uv.sources]
test-lfs-repo = { git = "https://github.com/zanieb/test-lfs-repo.git", lfs = true }
"#,
)?;
uv_snapshot!(context.filters(), context.sync(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ test-lfs-repo==0.1.0 (from git+https://github.com/zanieb/test-lfs-repo.git@39b6b03dc0a301420b7b4e73311d799fb139ba2e)
");
// Verify that we can import the module and access LFS content
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import test_lfs_repo.lfs_module"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
import test_lfs_repo.lfs_module
File "[SITE_PACKAGES]/test_lfs_repo/lfs_module.py", line 1
version https://git-lfs.github.com/spec/v1
^^^^^
SyntaxError: invalid syntax
"#);
// `UV_GIT_LFS=false` should not override `lfs = true`
uv_snapshot!(context.filters(), context.sync().env("UV_GIT_LFS", "false").arg("--reinstall").arg("--no-cache"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
~ test-lfs-repo==0.1.0 (from git+https://github.com/zanieb/test-lfs-repo.git@39b6b03dc0a301420b7b4e73311d799fb139ba2e)
");
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import test_lfs_repo.lfs_module"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
import test_lfs_repo.lfs_module
File "[SITE_PACKAGES]/test_lfs_repo/lfs_module.py", line 1
version https://git-lfs.github.com/spec/v1
^^^^^
SyntaxError: invalid syntax
"#);
// Set `lfs = false` in the source
pyproject_toml.write_str(
r#"
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = ["test-lfs-repo"]
[tool.uv.sources]
test-lfs-repo = { git = "https://github.com/zanieb/test-lfs-repo.git", lfs = false }
"#,
)?;
uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--no-cache"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
~ test-lfs-repo==0.1.0 (from git+https://github.com/zanieb/test-lfs-repo.git@39b6b03dc0a301420b7b4e73311d799fb139ba2e)
");
// Verify that LFS content is missing (import should fail)
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import test_lfs_repo.lfs_module"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
import test_lfs_repo.lfs_module
File "[SITE_PACKAGES]/test_lfs_repo/lfs_module.py", line 1
version https://git-lfs.github.com/spec/v1
^^^^^
SyntaxError: invalid syntax
"#);
// `UV_GIT_LFS=true` should not override `lfs = false`
uv_snapshot!(context.filters(), context.sync().env("UV_GIT_LFS", "true").arg("--reinstall").arg("--no-cache"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
~ test-lfs-repo==0.1.0 (from git+https://github.com/zanieb/test-lfs-repo.git@39b6b03dc0a301420b7b4e73311d799fb139ba2e)
");
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import test_lfs_repo.lfs_module"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
import test_lfs_repo.lfs_module
File "[SITE_PACKAGES]/test_lfs_repo/lfs_module.py", line 1
version https://git-lfs.github.com/spec/v1
^^^^^
SyntaxError: invalid syntax
"#);
// `UV_GIT_LFS = true` should work without explicit lfs flag
pyproject_toml.write_str(
r#"
[project]
name = "test-project"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = ["test-lfs-repo"]
[tool.uv.sources]
test-lfs-repo = { git = "https://github.com/zanieb/test-lfs-repo.git" }
"#,
)?;
uv_snapshot!(context.filters(), context.sync().env("UV_GIT_LFS", "true").arg("--reinstall").arg("--no-cache"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Uninstalled 1 package in [TIME]
Installed 1 package in [TIME]
~ test-lfs-repo==0.1.0 (from git+https://github.com/zanieb/test-lfs-repo.git@39b6b03dc0a301420b7b4e73311d799fb139ba2e)
");
// Verify that we can import the module when UV_GIT_LFS is set
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import test_lfs_repo.lfs_module; print('LFS module imported via env var')"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
import test_lfs_repo.lfs_module; print('LFS module imported via env var')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "[SITE_PACKAGES]/test_lfs_repo/lfs_module.py", line 1
version https://git-lfs.github.com/spec/v1
^^^^^
SyntaxError: invalid syntax
"#);
Ok(())
}
#[test]
fn sync_extra_build_dependencies_cache() -> Result<()> {
let context = TestContext::new("3.12");

7
uv.schema.json generated
View File

@ -1964,6 +1964,13 @@
}
]
},
"lfs": {
"description": "Whether to use Git LFS when cloning the repository.",
"type": [
"boolean",
"null"
]
},
"marker": {
"$ref": "#/definitions/MarkerTree"
},