use std::path::Path;
use std::process::Command;
use rattler_git::LazyClient;
use rattler_git::{GitUrl, LfsFilter, sha::GitSha, source::GitSource};
use reqwest_middleware::ClientWithMiddleware;
use url::Url;
fn panic_client() -> LazyClient {
LazyClient::new(|| -> ClientWithMiddleware {
panic!("network should not be used in LFS tests")
})
}
fn is_lfs_pointer(path: &Path) -> bool {
let contents = fs_err::read_to_string(path).unwrap_or_default();
contents.starts_with("version https://git-lfs.github.com/spec/")
}
fn require_git_lfs(test: &str) -> bool {
let ok = Command::new("git")
.args(["lfs", "version"])
.output()
.is_ok_and(|o| o.status.success());
if !ok {
eprintln!("skipping {test}: git-lfs is not installed");
}
ok
}
struct LfsFixture {
_tempdir: tempfile::TempDir,
repo_path: std::path::PathBuf,
base_url: Url,
head: String,
}
impl LfsFixture {
fn new() -> Self {
let tempdir = tempfile::tempdir().expect("failed to create temp dir");
let repo_path = tempdir.path().join("lfs-sample");
fs_err::create_dir_all(&repo_path).unwrap();
let git = |args: &[&str]| {
let output = Command::new("git")
.args(args)
.current_dir(&repo_path)
.output()
.unwrap_or_else(|err| panic!("failed to spawn `git {}`: {err}", args.join(" ")));
assert!(
output.status.success(),
"`git {}` failed: stdout={:?} stderr={:?}",
args.join(" "),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
String::from_utf8(output.stdout).unwrap().trim().to_string()
};
git(&["init", "-b", "main"]);
git(&["config", "user.email", "test@test.com"]);
git(&["config", "user.name", "Test"]);
git(&["config", "commit.gpgsign", "false"]);
git(&["lfs", "install", "--local"]);
fs_err::write(
repo_path.join(".gitattributes"),
"*.bin filter=lfs diff=lfs merge=lfs -text\n",
)
.unwrap();
fs_err::write(repo_path.join("README.md"), "# lfs sample\n").unwrap();
fs_err::write(
repo_path.join("data.bin"),
b"\x00\x01\x02\x03binary payload\xff\xfe",
)
.unwrap();
fs_err::write(repo_path.join("other.bin"), b"other LFS payload").unwrap();
git(&["add", "."]);
git(&["commit", "--message", "v0.1.0"]);
let head = git(&["rev-parse", "HEAD"]);
let pointer = git(&["show", "HEAD:data.bin"]);
assert!(
pointer.starts_with("version https://git-lfs.github.com/spec/"),
"HEAD:data.bin should be an LFS pointer, got: {pointer:?}"
);
let base_url = Url::from_directory_path(&repo_path).unwrap();
Self {
_tempdir: tempdir,
repo_path,
base_url,
head,
}
}
}
#[test]
fn fixture_builds_with_lfs() {
if !require_git_lfs("fixture_builds_with_lfs") {
return;
}
let repo = LfsFixture::new();
let objects = repo.repo_path.join(".git/lfs/objects");
assert!(
objects.is_dir() && fs_err::read_dir(&objects).unwrap().next().is_some(),
"expected LFS objects under {}",
objects.display()
);
}
#[test]
fn fetch_without_lfs_leaves_pointer() {
if !require_git_lfs("fetch_without_lfs_leaves_pointer") {
return;
}
let repo = LfsFixture::new();
let cache = tempfile::tempdir().unwrap();
let git_url = GitUrl::try_from(repo.base_url.clone()).unwrap();
let fetch = GitSource::new(git_url, panic_client(), cache.path())
.with_lfs(Some(false))
.fetch()
.expect("fetch should succeed");
assert!(!fetch.lfs_ready(), "LFS was not requested");
let data = fetch.path().join("data.bin");
assert!(data.is_file(), "data.bin missing from checkout");
assert!(
is_lfs_pointer(&data),
"data.bin should still be a pointer when LFS is disabled"
);
}
#[test]
fn fetch_with_lfs_materialises_blob() {
if !require_git_lfs("fetch_with_lfs_materialises_blob") {
return;
}
let repo = LfsFixture::new();
let original = fs_err::read(repo.repo_path.join("data.bin")).unwrap();
let cache = tempfile::tempdir().unwrap();
let git_url = GitUrl::try_from(repo.base_url.clone()).unwrap();
let fetch = GitSource::new(git_url, panic_client(), cache.path())
.with_lfs(Some(true))
.fetch()
.expect("fetch should succeed");
assert!(
fetch.lfs_ready(),
"fsck should pass for a healthy LFS fixture"
);
let data = fetch.path().join("data.bin");
assert!(data.is_file());
assert!(
!is_lfs_pointer(&data),
"data.bin should be the real blob, not a pointer"
);
let got = fs_err::read(&data).unwrap();
assert_eq!(
got, original,
"checked-out data.bin should match fixture source"
);
}
#[test]
fn cached_fetch_with_lfs_artifacts_is_ready() {
if !require_git_lfs("cached_fetch_with_lfs_artifacts_is_ready") {
return;
}
let repo = LfsFixture::new();
let cache = tempfile::tempdir().unwrap();
let head: GitSha = repo.head.parse().unwrap();
let make_source = || {
let url = GitUrl::try_from(repo.base_url.clone())
.unwrap()
.with_precise(head);
GitSource::new(url, panic_client(), cache.path()).with_lfs(Some(true))
};
let first = make_source().fetch().expect("first fetch should succeed");
assert!(first.lfs_ready());
let second = make_source().fetch().expect("cached fetch should succeed");
assert!(second.lfs_ready());
assert_eq!(second.commit(), first.commit());
}
#[test]
fn same_commit_plain_then_lfs_uses_distinct_checkouts() {
if !require_git_lfs("same_commit_plain_then_lfs_uses_distinct_checkouts") {
return;
}
let repo = LfsFixture::new();
let original = fs_err::read(repo.repo_path.join("data.bin")).unwrap();
let cache = tempfile::tempdir().unwrap();
let git_url = GitUrl::try_from(repo.base_url.clone()).unwrap();
let plain = GitSource::new(git_url.clone(), panic_client(), cache.path())
.with_lfs(Some(false))
.fetch()
.expect("plain fetch should succeed");
let lfs = GitSource::new(git_url, panic_client(), cache.path())
.with_lfs(Some(true))
.fetch()
.expect("LFS fetch should succeed");
assert_eq!(plain.commit(), lfs.commit());
assert_ne!(plain.path(), lfs.path());
assert!(is_lfs_pointer(&plain.path().join("data.bin")));
assert_eq!(fs_err::read(lfs.path().join("data.bin")).unwrap(), original);
}
#[test]
fn same_commit_lfs_then_plain_uses_distinct_checkouts() {
if !require_git_lfs("same_commit_lfs_then_plain_uses_distinct_checkouts") {
return;
}
let repo = LfsFixture::new();
let original = fs_err::read(repo.repo_path.join("data.bin")).unwrap();
let cache = tempfile::tempdir().unwrap();
let git_url = GitUrl::try_from(repo.base_url.clone()).unwrap();
let lfs = GitSource::new(git_url.clone(), panic_client(), cache.path())
.with_lfs(Some(true))
.fetch()
.expect("LFS fetch should succeed");
let plain = GitSource::new(git_url, panic_client(), cache.path())
.with_lfs(None)
.fetch()
.expect("plain fetch should succeed");
assert_eq!(lfs.commit(), plain.commit());
assert_ne!(lfs.path(), plain.path());
assert!(is_lfs_pointer(&plain.path().join("data.bin")));
assert_eq!(fs_err::read(lfs.path().join("data.bin")).unwrap(), original);
}
#[test]
fn lfs_path_filters_materialize_only_the_requested_subset() {
if !require_git_lfs("lfs_path_filters_materialize_only_the_requested_subset") {
return;
}
let repo = LfsFixture::new();
let data = fs_err::read(repo.repo_path.join("data.bin")).unwrap();
let other = fs_err::read(repo.repo_path.join("other.bin")).unwrap();
let cache = tempfile::tempdir().unwrap();
let git_url = GitUrl::try_from(repo.base_url.clone()).unwrap();
let data_only = GitSource::new(git_url.clone(), panic_client(), cache.path())
.with_lfs(Some(true))
.with_lfs_filter(LfsFilter {
include: Some("*.bin".to_string()),
exclude: Some("other.bin".to_string()),
})
.fetch()
.expect("filtered LFS fetch should succeed");
assert!(data_only.lfs_ready());
assert_eq!(
fs_err::read(data_only.path().join("data.bin")).unwrap(),
data
);
assert!(is_lfs_pointer(&data_only.path().join("other.bin")));
let other_only = GitSource::new(git_url, panic_client(), cache.path())
.with_lfs(Some(true))
.with_lfs_filter(LfsFilter {
include: Some("other.bin".to_string()),
exclude: None,
})
.fetch()
.expect("second filtered LFS fetch should succeed");
assert_ne!(data_only.path(), other_only.path());
assert!(is_lfs_pointer(&other_only.path().join("data.bin")));
assert_eq!(
fs_err::read(other_only.path().join("other.bin")).unwrap(),
other
);
}