use std::{
path::Path,
sync::Arc,
time::{Duration, Instant},
};
use cli::{bridge::git_core::clone_url_to_bare, cli::commands::GitOverlayBlobHydrator};
use objects::{object::Blob, store::ObjectStore};
use repo::Repository;
use sley::{
CommitObject, EntryKind, GitObjectType, GitTime, ObjectId, RefPrecondition, ReferenceTarget,
Repository as SleyRepository, Signature,
plumbing::{
sley_core::ByteString as GitByteString, sley_object::EncodedObject, sley_refs::ReflogEntry,
},
};
use tempfile::TempDir;
fn build_local_bare_with_one_blob() -> (TempDir, std::path::PathBuf, ObjectId, Vec<u8>) {
let temp = TempDir::new().expect("temp for bare git");
let bare = temp.path().join("source.git");
let repo = SleyRepository::init_bare(&bare).expect("init bare git");
let blob_bytes = b"# Hydration sentinel\nlazy lazy lazy\n".to_vec();
let blob_oid = repo.write_blob(blob_bytes.as_slice()).expect("write blob");
let empty_tree = repo
.write_tree(sley::TreeEditor::new())
.expect("write empty tree");
let mut editor = repo.edit_tree(&empty_tree).expect("edit tree");
editor.upsert("README.md", EntryKind::Blob, blob_oid);
let tree_oid = repo.write_tree(editor).expect("write tree");
let sig = test_signature();
let commit = CommitObject {
tree: tree_oid,
parents: Vec::new(),
author: sig.to_ident_bytes(),
committer: sig.to_ident_bytes(),
encoding: None,
message: b"seed".to_vec(),
};
let commit_oid = repo
.write_object(EncodedObject::new(GitObjectType::Commit, commit.write()))
.expect("commit");
set_reference(&repo, "refs/heads/main", commit_oid);
(temp, bare, blob_oid, blob_bytes)
}
fn test_signature() -> Signature {
Signature {
name: GitByteString::new(b"Heddle Test".to_vec()),
email: GitByteString::new(b"test@heddle".to_vec()),
time: GitTime::new(0, 0),
raw: b"Heddle Test <test@heddle> 0 +0000".to_vec(),
}
}
fn set_reference(repo: &SleyRepository, name: &str, target: ObjectId) {
let sig = test_signature();
let refs = repo.references();
let old_oid = match refs.read_ref(name).expect("read ref") {
Some(ReferenceTarget::Direct(oid)) => oid,
_ => ObjectId::null(repo.object_format()),
};
let mut tx = refs.transaction();
tx.update_to(
name.to_string(),
ReferenceTarget::Direct(target),
RefPrecondition::Any,
Some(ReflogEntry {
old_oid,
new_oid: target,
committer: sig.to_ident_bytes(),
message: b"test: update ref".to_vec(),
}),
);
tx.commit().expect("update ref");
}
fn open_git(path: impl AsRef<Path>) -> Result<SleyRepository, String> {
SleyRepository::open(path.as_ref())
.or_else(|_| SleyRepository::discover(path.as_ref()))
.map_err(|err| err.to_string())
}
fn drive_hydration_round_trip(git_bare: &Path, blob_oid: ObjectId, blob_bytes: &[u8]) -> Duration {
let blake3 = Blob::new(blob_bytes.to_vec()).hash();
let heddle_temp = TempDir::new().expect("heddle temp");
let repo = Repository::init_default(heddle_temp.path()).expect("init heddle repo");
repo.record_missing_blob(blake3)
.expect("record missing marker");
assert!(
repo.is_missing_blob(&blake3).expect("read marker"),
"precondition: blake3 must be marked missing",
);
assert!(
repo.store()
.get_blob(&blake3)
.expect("blob lookup")
.is_none(),
"precondition: heddle store must not yet hold the blob",
);
let hydrator = Arc::new(GitOverlayBlobHydrator::new(git_bare.to_path_buf()));
hydrator.record_blob_oid(blake3, blob_oid);
repo.set_blob_hydrator(hydrator);
let started = Instant::now();
let blob = repo
.require_blob(&blake3)
.expect("require_blob must hydrate");
let elapsed = started.elapsed();
assert_eq!(
blob.content(),
blob_bytes,
"hydrated bytes must match the upstream blob exactly",
);
assert!(
!repo.is_missing_blob(&blake3).expect("re-read marker"),
"missing marker must be cleared after successful hydration",
);
assert!(
repo.store()
.get_blob(&blake3)
.expect("blob in store")
.is_some(),
"blob must be persisted in the heddle store after hydration",
);
elapsed
}
#[test]
fn hydration_fires_against_local_git_overlay() {
let (_temp, bare, oid, bytes) = build_local_bare_with_one_blob();
let elapsed = drive_hydration_round_trip(&bare, oid, &bytes);
assert!(
elapsed < Duration::from_secs(5),
"local hydration round-trip should be quick; took {elapsed:?}",
);
eprintln!("local hydration round-trip: {elapsed:?}");
}
#[test]
fn hydration_survives_repository_reopen() {
use objects::error::Result as HResult;
use repo::{
BlobHydrator,
lazy_hydrator::{
BlobHydratorFactory, HydratorSection, KIND_GIT_OVERLAY, LazyHydratorConfig,
register_factory,
},
};
let (_bare_temp, bare, blob_oid, blob_bytes) = build_local_bare_with_one_blob();
let blake3 = Blob::new(blob_bytes.clone()).hash();
let heddle_temp = TempDir::new().expect("heddle temp");
let heddle_root = heddle_temp.path().to_path_buf();
let repo = Repository::init_default(&heddle_root).expect("init heddle repo");
let heddle_dir = repo.heddle_dir().to_path_buf();
repo.record_missing_blob(blake3).expect("record marker");
LazyHydratorConfig::git_overlay()
.save(&heddle_dir)
.expect("write lazy-hydrator.toml");
drop(repo);
let bare_for_factory = bare.clone();
let factory: BlobHydratorFactory = std::sync::Arc::new(
move |_root: &Path, _section: &HydratorSection| -> HResult<Arc<dyn BlobHydrator>> {
let h = GitOverlayBlobHydrator::new(bare_for_factory.clone());
h.record_blob_oid(blake3, blob_oid);
Ok(Arc::new(h))
},
);
register_factory(KIND_GIT_OVERLAY, factory);
let reopened = Repository::open(&heddle_root).expect("reopen heddle repo");
let blob = reopened
.require_blob(&blake3)
.expect("first reopen: hydrator must be installed by Repository::open");
assert_eq!(
blob.content(),
blob_bytes.as_slice(),
"hydrated bytes must match upstream",
);
assert!(!reopened.is_missing_blob(&blake3).unwrap());
drop(reopened);
let bare_open = open_git(&bare).expect("open bare for second blob");
let payload2 = b"second-blob-after-reopen\n".to_vec();
let oid2 = bare_open
.write_blob(payload2.as_slice())
.expect("write blob 2");
let blake3_2 = Blob::new(payload2.clone()).hash();
let bare_for_factory_2 = bare.clone();
let factory_2: BlobHydratorFactory = std::sync::Arc::new(
move |_root: &Path, _section: &HydratorSection| -> HResult<Arc<dyn BlobHydrator>> {
let h = GitOverlayBlobHydrator::new(bare_for_factory_2.clone());
h.record_blob_oid(blake3_2, oid2);
Ok(Arc::new(h))
},
);
register_factory(KIND_GIT_OVERLAY, factory_2);
let reopened2 = Repository::open(&heddle_root).expect("reopen 2");
reopened2
.record_missing_blob(blake3_2)
.expect("mark blob 2 missing");
let blob2 = reopened2
.require_blob(&blake3_2)
.expect("second reopen: hydrator must be re-installed");
assert_eq!(blob2.content(), payload2.as_slice());
}
#[test]
#[ignore = "clones torvalds/linux.git; run via --include-ignored or nightly job"]
fn hydration_fires_against_torvalds_linux() {
if std::process::Command::new("git")
.arg("--version")
.output()
.ok()
.filter(|o| o.status.success())
.is_none()
{
eprintln!("SKIP: git binary not on PATH");
return;
}
let temp = TempDir::new().expect("temp for linux clone");
let bare = temp.path().join("linux.git");
let url = "https://github.com/torvalds/linux.git";
eprintln!("cloning torvalds/linux.git at depth=1 + filter=blob:none ...");
let started = Instant::now();
if let Err(err) = clone_url_to_bare(url, &bare, Some(1), Some("blob:none")) {
eprintln!("SKIP: kernel clone failed (network?): {err}");
return;
}
eprintln!("clone completed in {:?}", started.elapsed());
let git_repo = open_git(&bare).expect("open kernel bare repo");
let tip = git_repo.head().expect("HEAD").oid.expect("HEAD commit");
let tree = git_repo.read_commit(&tip).expect("find HEAD commit").tree;
let tree_obj = git_repo.read_tree(&tree).expect("read tip tree");
let blob_oid = tree_obj
.entries
.iter()
.find(|entry| EntryKind::from_mode(entry.mode) == Some(EntryKind::Blob))
.map(|entry| entry.oid)
.expect("tip tree must contain at least one blob entry");
eprintln!("targeting blob {blob_oid} for hydration");
let cat = std::process::Command::new("git")
.arg("-C")
.arg(&bare)
.args(["cat-file", "-p"])
.arg(blob_oid.to_string())
.output()
.expect("git cat-file invocation");
assert!(
cat.status.success(),
"git cat-file failed in setup: {}",
String::from_utf8_lossy(&cat.stderr)
);
let bytes = cat.stdout;
eprintln!("blob materialised ({} bytes)", bytes.len());
let _ = git_repo;
let elapsed = drive_hydration_round_trip(&bare, blob_oid, &bytes);
eprintln!("hydration round-trip: {elapsed:?}");
assert!(
elapsed < Duration::from_secs(120),
"hydration should complete within 2 minutes even over the network; took {elapsed:?}",
);
}