use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; use async_trait::async_trait; use gitbaby::GitBaby; use gitbaby::commit::TreeEntryMode; use gitbaby::error::BabyError; use gitbaby::repo::RepositoryFacade; static COUNTER: AtomicU32 = AtomicU32::new(0); struct TestFacade { dir: PathBuf, } #[async_trait] impl RepositoryFacade for TestFacade { async fn git_repo_dir(&self) -> Result { Ok(self.dir.clone()) } async fn git_alternate_object_directories(&self) -> Result, BabyError> { Ok(Vec::new()) } async fn gix_repo(&self) -> Result { gix::open(&self.dir).map_err(|e| BabyError::Custom(e.to_string())) } } fn temp_repo() -> PathBuf { let n = COUNTER.fetch_add(1, Ordering::SeqCst); let dir = std::env::temp_dir().join(format!("gitbaby-tree-test-{}-{n}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("mkdir repo"); let out = Command::new("git") .args(["init", "-q"]) .current_dir(&dir) .output() .expect("git init failed"); assert!(out.status.success(), "git init: {:?}", out); dir } fn cleanup(repo: &Path) { let _ = std::fs::remove_dir_all(repo); } fn git_commit(repo: &Path, msg: &str) { let out = Command::new("git") .args([ "-c", "user.name=T", "-c", "user.email=t@e", "commit", "-q", "-m", msg, ]) .current_dir(repo) .output() .expect("git commit failed"); assert!(out.status.success(), "git commit `{msg}`: {:?}", out); } fn write_file(repo: &Path, rel: &str, content: &str) { let p = repo.join(rel); std::fs::create_dir_all(p.parent().expect("parent")).expect("mkdir"); std::fs::write(p, content).expect("write"); } fn index_commit(repo: &Path, msg: &str) { let out = Command::new("git") .args(["add", "-A"]) .current_dir(repo) .output() .expect("git add failed"); assert!(out.status.success(), "git add: {:?}", out); git_commit(repo, msg); } async fn baby_of(repo: PathBuf) -> GitBaby { GitBaby::new(Arc::new(TestFacade { dir: repo })) } #[tokio::test] async fn tree_entries_with_latest_whole_tree() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); write_file(&repo, "b.txt", "bee"); index_commit(&repo, "first"); write_file(&repo, "a.txt", "two"); index_commit(&repo, "second"); write_file(&repo, "c/d.txt", "dee"); index_commit(&repo, "third"); let baby = baby_of(repo.clone()).await; let rows = baby .tree_entries_with_latest("HEAD", None) .await .expect("list whole tree"); let by_name: HashMap<&str, &gitbaby::tree::TreeEntryWithLatest> = rows.iter().map(|r| (r.entry.name.as_str(), r)).collect(); let a = by_name["a.txt"]; assert_eq!(a.entry.mode, TreeEntryMode::Blob); assert_eq!(a.latest.message.as_str(), "second"); let b = by_name["b.txt"]; assert_eq!(b.latest.message.as_str(), "first"); let c = by_name["c"]; assert_eq!(c.entry.mode, TreeEntryMode::Tree); assert_eq!(c.latest.message.as_str(), "third"); let d = by_name["c/d.txt"]; assert_eq!(d.latest.message.as_str(), "third"); cleanup(&repo); } #[tokio::test] async fn tree_entries_with_latest_subtree() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); index_commit(&repo, "first"); write_file(&repo, "c/d.txt", "dee"); index_commit(&repo, "third"); let baby = baby_of(repo.clone()).await; let rows = baby .tree_entries_with_latest("HEAD", Some("c")) .await .expect("list subtree"); assert_eq!(rows.len(), 2); assert_eq!(rows[0].entry.name.as_str(), "c"); assert_eq!(rows[0].entry.mode, TreeEntryMode::Tree); assert_eq!(rows[0].latest.message.as_str(), "third"); assert_eq!(rows[1].entry.name.as_str(), "c/d.txt"); assert_eq!(rows[1].latest.message.as_str(), "third"); cleanup(&repo); } #[tokio::test] async fn tree_entries_with_latest_rejects_bad_input() { let repo = temp_repo(); write_file(&repo, "a.txt", "one"); index_commit(&repo, "first"); let baby = baby_of(repo.clone()).await; assert!(baby.tree_entries_with_latest("", None).await.is_err()); assert!( baby.tree_entries_with_latest("HEAD", Some("..")) .await .is_err() ); cleanup(&repo); }