use crate::repo::BlobReader;
use crate::{CodeLoreError, Result};
pub(super) struct GixBlobReader {
repo: gix::Repository,
rev: String,
root_tree_id: Option<gix::ObjectId>,
}
impl GixBlobReader {
pub(super) fn new(inner: &gix::ThreadSafeRepository, rev: &str) -> Self {
Self {
repo: inner.to_thread_local(),
rev: rev.to_string(),
root_tree_id: None,
}
}
fn resolve_root_tree_id(&self) -> Result<gix::ObjectId> {
let commit_id = self.repo.rev_parse_single(self.rev.as_str()).map_err(|e| {
CodeLoreError::Repo(format!("read_blob_at rev_parse {}: {e}", self.rev))
})?;
let commit = self.repo.find_commit(commit_id).map_err(|e| {
CodeLoreError::Repo(format!("read_blob_at find_commit {}: {e}", self.rev))
})?;
let tree = commit
.tree()
.map_err(|e| CodeLoreError::Repo(format!("read_blob_at tree {}: {e}", self.rev)))?;
Ok(tree.id)
}
}
impl BlobReader for GixBlobReader {
fn read(&mut self, path: &str) -> Result<Option<Vec<u8>>> {
let tree_id = if let Some(id) = self.root_tree_id {
id
} else {
let id = self.resolve_root_tree_id()?;
self.root_tree_id = Some(id);
id
};
let tree = self
.repo
.find_tree(tree_id)
.map_err(|e| CodeLoreError::Repo(format!("read_blob_at tree {}: {e}", self.rev)))?;
let entry = tree.lookup_entry_by_path(path).map_err(|e| {
CodeLoreError::Repo(format!("read_blob_at lookup {}:{path}: {e}", self.rev))
})?;
let Some(entry) = entry else {
return Ok(None);
};
if !entry.mode().is_blob() {
return Ok(None);
}
let mut obj = self.repo.find_object(entry.id()).map_err(|e| {
CodeLoreError::Repo(format!("read_blob_at find_object {}:{path}: {e}", self.rev))
})?;
Ok(Some(std::mem::take(&mut obj.data)))
}
}
#[cfg(all(test, feature = "test-support"))]
mod tests {
use crate::repo::{GixRepo, Repo};
#[test]
fn warm_reader_matches_read_blob_at_for_every_path_kind() {
let fixture = crate::test_support::differential_repo::build();
let repo = GixRepo::open(fixture.dir.path()).expect("open repo");
let head_rev = repo.head_sha().expect("head_sha");
let tracked = repo.tracked_paths_at_head().expect("tracked paths");
assert!(
!tracked.is_empty(),
"fixture must have tracked files at HEAD"
);
let nested = tracked
.iter()
.find(|p| p.contains('/'))
.expect("fixture should have a file nested in a directory");
let dir = &nested[..nested.rfind('/').unwrap()];
let mut reader = repo.blob_reader_at(&head_rev);
for path in tracked
.iter()
.map(String::as_str)
.chain([dir, "this-path-does-not-exist-anywhere.zzz"])
{
let direct = repo.read_blob_at(&head_rev, path);
let warm = reader.read(path);
match (direct, warm) {
(Ok(d), Ok(w)) => assert_eq!(d, w, "byte mismatch for {path}"),
(Err(d), Err(w)) => assert_eq!(
d.to_string(),
w.to_string(),
"error text mismatch for {path}"
),
(d, w) => panic!("Ok/Err variant mismatch for {path}: direct={d:?} warm={w:?}"),
}
}
let db = crate::facts::FactsDb::new_in_memory().expect("db");
let opts = crate::test_support::permissive_coupling_opts(fixture.dir.path().to_path_buf());
db.ingest(&repo, &opts).expect("ingest");
let old_rev: String = db
.query_row(
"SELECT rev FROM commits ORDER BY date ASC, rowid ASC LIMIT 1",
[],
|r| r.get(0),
)
.expect("query oldest rev");
let mut old_reader = repo.blob_reader_at(&old_rev);
for path in &tracked {
let direct = repo.read_blob_at(&old_rev, path);
let warm = old_reader.read(path);
match (direct, warm) {
(Ok(d), Ok(w)) => assert_eq!(d, w, "old-rev byte mismatch for {path}"),
(Err(_), Err(_)) => {}
(d, w) => {
panic!("old-rev Ok/Err variant mismatch for {path}: direct={d:?} warm={w:?}")
}
}
}
}
}