use gix::bstr::ByteSlice as _; use gix::revision::plumbing::Spec as RevSpec; use gix::revision::walk::Sorting; use jacquard_common::types::did::validate_did; use tempfile::TempDir; use crate::xrpc::XrpcError; /// A [`gix::Repository`] backed by a temporary directory that is removed on drop. Derefs to /// `gix::Repository`, so it is used just like one; keeping it alive keeps the scratch dir alive. pub(crate) struct TempRepository { repo: gix::Repository, // Declared AFTER `repo` so `repo` drops first: any file handles into the scratch dir close // before the dir itself is removed (Rust drops struct fields in declaration order). _dir: TempDir, } impl TempRepository { fn new(repo: gix::Repository, dir: TempDir) -> Self { Self { repo, _dir: dir } } } impl std::ops::Deref for TempRepository { type Target = gix::Repository; fn deref(&self) -> &gix::Repository { &self.repo } } /// Build a throwaway bare repo whose `objects/info/alternates` points read-only at each of the /// given git repositories, so a single `gix::Repository` can see objects from all of them /// without ever mutating them. pub(crate) fn open_scratch( repo_base: &std::path::Path, dids: &[&str], ) -> Result { let mut seen: Vec<&str> = Vec::new(); let mut object_dirs = Vec::new(); for &did in dids { if seen.contains(&did) { continue; } // A DID has no `/`, so this is also what keeps `repo_base.join(did)` inside `repo_base`. validate_did(did) .map_err(|_| XrpcError::InvalidRequest("repo must be a DID".to_owned()))?; seen.push(did); let object_dir = std::fs::canonicalize(repo_base.join(did).join("objects")).map_err(|e| { XrpcError::RepoNotFound { detail: format!("repo not found: {did}: {e}"), } })?; object_dirs.push(object_dir); } if object_dirs.is_empty() { return Err(XrpcError::Internal( "open_scratch requires at least one repo".to_owned(), )); } let internal = |e: std::io::Error| XrpcError::Internal(e.to_string()); let scratch = tempfile::tempdir().map_err(internal)?; gix::init_bare(scratch.path()).map_err(|e| XrpcError::Internal(e.to_string()))?; let info_dir = scratch.path().join("objects").join("info"); std::fs::create_dir_all(&info_dir).map_err(internal)?; let alternates = object_dirs .iter() .map(|p| p.display().to_string()) .collect::>() .join("\n"); std::fs::write(info_dir.join("alternates"), format!("{alternates}\n")).map_err(internal)?; let repo = gix::open(scratch.path()) .map_err(|e| XrpcError::Internal(e.to_string()))? .with_object_memory(); Ok(TempRepository::new(repo, scratch)) } pub(crate) fn commit_log_walk<'repo>( repo: &'repo gix::Repository, ranges: &[Vec], all_refs: bool, ) -> anyhow::Result> { let mut tips = Vec::new(); let mut hidden = Vec::new(); if all_refs { for r in repo.references()?.all()? { let mut r = r.map_err(|e| anyhow::anyhow!(e))?; if let Ok(commit) = r.peel_to_commit() { tips.push(commit.id); } } } else { for range in ranges { let revspec = repo.rev_parse(range.as_bstr())?; let spec = revspec.detach(); match spec { RevSpec::Include(id) => tips.push(id), RevSpec::Range { from, to } => { tips.push(to); hidden.push(from); } _ => { anyhow::bail!("The spec isn't currently supported: {spec:?}") } } } } Ok(repo .rev_walk(tips) .sorting(Sorting::ByCommitTime(Default::default())) .with_hidden(hidden) .all()?) } #[cfg(test)] mod tests { use super::*; #[test] fn temp_repository_removes_scratch_dir_on_drop() { let dir = tempfile::tempdir().unwrap(); gix::init_bare(dir.path()).unwrap(); let repo = gix::open(dir.path()).unwrap().with_object_memory(); let temp = TempRepository::new(repo, dir); let path = temp.git_dir().to_path_buf(); assert!(path.exists()); // Derefs to gix::Repository. assert!(temp.object_hash() == gix::hash::Kind::Sha1); drop(temp); assert!(!path.exists(), "scratch dir should be gone after drop"); } }