Skip to content
Clone
8ee42c0
402 lines 14.3 KB Rust
//! tree usecase: impl TreeOps for GitRepository.
use crate::GitRepository;
use crate::object::types::ObjectType;
use crate::tree::impls::TreeOps;
use crate::tree::types::{
EntryFollowResult, EntryMode, PathLastUpdated, PathResult, Tree, TreeEntry,
};
fn entry_to_type(mode: gix::objs::tree::EntryMode) -> ObjectType {
match mode.kind() {
gix::objs::tree::EntryKind::Tree => ObjectType::Tree,
gix::objs::tree::EntryKind::Blob
| gix::objs::tree::EntryKind::BlobExecutable
| gix::objs::tree::EntryKind::Link => ObjectType::Blob,
gix::objs::tree::EntryKind::Commit => ObjectType::Commit,
}
}
fn entry_to_tree_entry(repo: &gix::Repository, e: gix::objs::tree::EntryRef) -> TreeEntry {
let mode = EntryMode::from(e.mode);
let size = if mode.is_blob() {
repo.find_object(e.oid).ok().map(|o| o.data.len() as i64)
} else {
None
};
TreeEntry {
id: e.oid.to_owned(),
name: e.filename.to_string(),
mode,
size,
object_type: entry_to_type(e.mode),
}
}
impl TreeOps for GitRepository {
fn tree_read(&self, id: &gix::hash::ObjectId) -> Result<Tree, crate::Error> {
if let Some(hit) = self.cache_get_oid::<Tree>("tree", id) {
return Ok(hit);
}
let repo = self.repo()?;
let tree = repo
.find_tree(*id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let refs = tree
.decode()
.map_err(|e| crate::Error::Find(e.to_string()))?;
let entries = refs
.entries
.iter()
.map(|e| entry_to_tree_entry(&repo, *e))
.collect();
let v = Tree { id: *id, entries };
self.cache_put_oid("tree", id, &v);
Ok(v)
}
fn tree_resolve_path(
&self,
tree_id: &gix::hash::ObjectId,
path: &str,
) -> Result<TreeEntry, crate::Error> {
let tip = self.tip_id()?;
let key = format!("{tree_id}.{path}");
if let Some(hit) = self.cache_get_rev::<TreeEntry>("tree", "resolve", &key, tip) {
return Ok(hit);
}
let repo = self.repo()?;
let mut current = repo
.find_tree(*tree_id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let mut parts = path.split('/').filter(|s| !s.is_empty());
let mut last = None;
while let Some(part) = parts.next() {
let entry =
current
.find_entry(part.as_bytes())
.ok_or_else(|| crate::Error::NotFound {
name: part.to_string(),
})?;
let te = entry_to_tree_entry(&repo, entry.inner);
if parts.clone().next().is_some() {
if !te.mode.is_dir() {
return Err(crate::Error::NotFound {
name: path.to_string(),
});
}
current = repo
.find_tree(te.id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
}
last = Some(te);
}
let v = last.ok_or_else(|| crate::Error::NotFound {
name: path.to_string(),
})?;
self.cache_put_rev("tree", "resolve", &key, tip, &v);
Ok(v)
}
fn tree_walk(&self, tree_id: &gix::hash::ObjectId) -> Result<Vec<PathResult>, crate::Error> {
if let Some(hit) = self.cache_get_oid::<Vec<PathResult>>("tree_walk", tree_id) {
return Ok(hit);
}
let mut out = Vec::new();
let mut stack = vec![(*tree_id, String::new())];
let repo = self.repo()?;
while let Some((id, prefix)) = stack.pop() {
let tree = repo
.find_tree(id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let refs = tree
.decode()
.map_err(|e| crate::Error::Find(e.to_string()))?;
for e in refs.entries.iter() {
let name = e.filename.to_string();
let path = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
let te = entry_to_tree_entry(&repo, *e);
out.push(PathResult {
entry: te.clone(),
path: path.clone(),
});
if te.mode.is_dir() {
stack.push((te.id, path));
}
}
}
self.cache_put_oid("tree_walk", tree_id, &out);
Ok(out)
}
fn tree_follow(&self, entry: &TreeEntry) -> Result<EntryFollowResult, crate::Error> {
if let Some(hit) = self.cache_get_oid::<EntryFollowResult>("tree_follow", &entry.id) {
return Ok(hit);
}
let repo = self.repo()?;
let v = if !entry.mode.is_link() {
EntryFollowResult {
symlink_content: None,
target_full_path: None,
target_entry: Some(entry.clone()),
}
} else {
let blob = repo
.find_blob(entry.id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let content = String::from_utf8_lossy(&blob.data).to_string();
EntryFollowResult {
symlink_content: Some(content),
target_full_path: None,
target_entry: None,
}
};
self.cache_put_oid("tree_follow", &entry.id, &v);
Ok(v)
}
fn tree_last_updated(&self, rev: &str) -> Result<Vec<PathLastUpdated>, crate::Error> {
crate::validate::validate_short_name("revision", rev)?;
let tip = self.tip_id()?;
if let Some(hit) =
self.cache_get_rev::<Vec<PathLastUpdated>>("tree", "last_updated", rev, tip)
{
return Ok(hit);
}
let repo = self.repo()?;
let commit_id = repo
.rev_parse_single(rev.as_bytes())
.map_err(|e| crate::Error::RevParse(e.to_string()))?
.detach();
let commit = repo
.find_commit(commit_id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let decoded = commit
.decode()
.map_err(|e| crate::Error::Find(e.to_string()))?
.into_owned()
.map_err(|e| crate::Error::Find(e.to_string()))?;
let commit_time = decoded.committer.time.seconds;
let tree_id = decoded.tree;
// Single pass over history: path -> (last commit time, last commit id)
// (newest commit wins, since git log is newest-first).
let repo_path = repo.path();
let log = crate::gitcli::run(
repo_path,
&[
"log",
"--format=%H%n%ct",
"--name-only",
"--no-renames",
rev,
],
)?;
let mut last: std::collections::HashMap<String, (i64, gix::hash::ObjectId)> =
std::collections::HashMap::new();
let mut cur_hash: Option<gix::hash::ObjectId> = None;
let mut cur_time: Option<i64> = None;
let mut stage = 0u8; // 0=hash, 1=time, 2=files
for line in log.lines() {
let line = line.trim();
if line.is_empty() {
if stage == 2 {
stage = 0;
cur_hash = None;
cur_time = None;
}
continue;
}
match stage {
0 => {
if line.len() == 40
&& let Ok(id) = gix::hash::ObjectId::from_hex(line.as_bytes())
{
cur_hash = Some(id);
stage = 1;
}
}
1 => {
if let Ok(ts) = line.parse::<i64>() {
cur_time = Some(ts);
stage = 2;
}
}
_ => {
if let (Some(ts), Some(id)) = (cur_time, cur_hash) {
last.entry(line.to_string()).or_insert((ts, id));
}
}
}
}
let mut out = Vec::new();
walk_last_updated(&repo, tree_id, "", &last, commit_time, commit_id, &mut out)?;
let tip = self.tip_id()?;
self.cache_put_rev("tree", "last_updated", rev, tip, &out);
Ok(out)
}
}
// Recursively walk a tree, emitting each path's last-update info.
// For directories, the last-update time/id is the max over its descendants.
fn walk_last_updated(
repo: &gix::Repository,
tree_id: gix::ObjectId,
prefix: &str,
last: &std::collections::HashMap<String, (i64, gix::hash::ObjectId)>,
commit_time: i64,
commit_id: gix::hash::ObjectId,
out: &mut Vec<PathLastUpdated>,
) -> Result<(i64, gix::hash::ObjectId), crate::Error> {
let tree = repo
.find_tree(tree_id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let refs = tree
.decode()
.map_err(|e| crate::Error::Find(e.to_string()))?;
let mut dir_max_time = commit_time;
let mut dir_max_id = commit_id;
for e in refs.entries.iter() {
let name = e.filename.to_string();
let path = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
let mode = EntryMode::from(e.mode);
let (this_time, this_id) = last.get(&path).copied().unwrap_or((commit_time, commit_id));
if mode.is_dir() {
let (child_max_time, child_max_id) = walk_last_updated(
repo,
e.oid.to_owned(),
&path,
last,
commit_time,
commit_id,
out,
)?;
let (t, id) = if this_time >= child_max_time {
(this_time, this_id)
} else {
(child_max_time, child_max_id)
};
out.push(PathLastUpdated {
path,
object_type: ObjectType::Tree,
last_updated: t,
commit_id: id,
});
if t > dir_max_time {
dir_max_time = t;
dir_max_id = id;
}
} else {
out.push(PathLastUpdated {
path,
object_type: entry_to_type(e.mode),
last_updated: this_time,
commit_id: this_id,
});
if this_time > dir_max_time {
dir_max_time = this_time;
dir_max_id = this_id;
}
}
}
Ok((dir_max_time, dir_max_id))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GitRepository;
use crate::object::cache::ObjectCache;
use cache::CacheBuilder;
use storage::config::StorageSettings;
use storage::local::LocalStorage;
fn block_on<F: std::future::Future>(f: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(f)
}
fn make_repo(dir: &std::path::Path, name: &str) -> GitRepository {
let cache_cfg = cache::config::cache::AppCacheSettings::default();
let cache = block_on(CacheBuilder::from_config(cache_cfg).build())
.expect("cache build")
.expect("cache enabled");
let cache = std::sync::Arc::new(cache);
let oc = ObjectCache {
prefix: "test".to_string(),
cache,
expire_ttl: 300,
};
let mut settings = StorageSettings::default();
settings.set_local(
storage::config::LocalStorageSettings::builder()
.root_dir(dir.join("storage").to_path_buf())
.build(),
);
let storage: std::sync::Arc<Box<dyn storage::StorageRepository>> = std::sync::Arc::new(
Box::new(LocalStorage::from_config(&settings).expect("storage")),
);
GitRepository::new(dir.join(name), oc, storage)
}
fn run_stdin(repo_path: &std::path::Path, args: &[&str], input: &[u8]) -> String {
let out = duct::cmd("git", args)
.env("GIT_DIR", repo_path)
.env("GIT_TERMINAL_PROMPT", "0")
.stdin_bytes(input)
.stdout_capture()
.stderr_capture()
.run()
.unwrap();
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
#[test]
fn tree_last_updated_reports_blob_and_dir_times() {
let tmp = std::env::temp_dir().join(format!("git-lastupd-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let repo = make_repo(&tmp, "main");
crate::gitcli::run(&repo.path, &["init", "-q", "--bare", "-b", "main"]).unwrap();
crate::gitcli::run(&repo.path, &["config", "user.email", "t@t"]).unwrap();
crate::gitcli::run(&repo.path, &["config", "user.name", "t"]).unwrap();
let blob_a = run_stdin(&repo.path, &["hash-object", "-w", "--stdin"], b"a\n");
let blob_b = run_stdin(&repo.path, &["hash-object", "-w", "--stdin"], b"b\n");
let tree_d = run_stdin(
&repo.path,
&["mktree", "-z"],
&format!("100644 blob {blob_b}\td.txt\0").into_bytes(),
);
let tree_root = run_stdin(
&repo.path,
&["mktree", "-z"],
&format!("100644 blob {blob_a}\ta.txt\040000 tree {tree_d}\td\0").into_bytes(),
);
let c1 = run_stdin(&repo.path, &["commit-tree", &tree_root, "-m", "init"], b"");
crate::gitcli::run(&repo.path, &["update-ref", "refs/heads/main", &c1]).unwrap();
let res = TreeOps::tree_last_updated(&repo, "main").unwrap();
let by_path: std::collections::HashMap<_, _> =
res.iter().map(|p| (p.path.clone(), p)).collect();
assert!(by_path.contains_key("a.txt"));
assert!(by_path.contains_key("d/d.txt"));
assert!(by_path.contains_key("d"));
assert!(by_path["a.txt"].last_updated > 0);
assert_eq!(by_path["a.txt"].commit_id.to_string(), c1);
assert_eq!(by_path["d/d.txt"].commit_id.to_string(), c1);
assert_eq!(by_path["d"].commit_id.to_string(), c1);
assert!(by_path["d"].last_updated >= by_path["d/d.txt"].last_updated);
let _ = std::fs::remove_dir_all(&tmp);
}
}