//! Fixtures: a temporary store of bare repositories built with git2, and //! an App configured to serve it. use std::{fs, path::Path, sync::Arc}; use clap::Parser; use git2::{Repository, RepositoryInitOptions, Signature}; use mugit::{App, Config}; use tempfile::TempDir; pub const PUBLIC_URL: &str = "http://git.test"; pub struct Fixture { pub dir: TempDir, pub app: Arc, } impl Fixture { pub fn new() -> Self { Self::with_page_size(50) } pub fn with_page_size(page_size: u32) -> Self { Self::with_args(&["--page-size", &page_size.to_string()]) } /// A fixture whose app is configured with further command line /// arguments on top of the usual ones. pub fn with_args(args: &[&str]) -> Self { let dir = tempfile::tempdir().expect("temp dir"); let usual = [ "mugit", "--repos", dir.path().to_str().unwrap(), "--listen", "127.0.0.1:0", "--public-url", PUBLIC_URL, "--site-name", "Test Site", ]; let config = Config::parse_from(usual.iter().chain(args)); let app = App::new(config).expect("app"); Fixture { dir, app } } /// Creates a bare repository with one commit containing `files`, /// on branch `main`. File names may contain `/` to build directories. pub fn repo(&self, name: &str, public: bool, description: &str, files: &[(&str, &str)]) { let byte_files: Vec<(&str, &[u8])> = files.iter().map(|(f, c)| (*f, c.as_bytes())).collect(); self.repo_bytes(name, public, description, &byte_files); } /// As `repo`, with arbitrary file contents. pub fn repo_bytes(&self, name: &str, public: bool, description: &str, files: &[(&str, &[u8])]) { let path = self.dir.path().join(format!("{name}.git")); let mut options = RepositoryInitOptions::new(); options.bare(true).initial_head("main"); let repo = Repository::init_opts(&path, &options).expect("init bare repo"); if !files.is_empty() { let tree = repo.find_tree(build_tree(&repo, files)).expect("find tree"); let sig = Signature::now("Test", "test@example.com").expect("signature"); repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[]) .expect("commit"); } fs::write(path.join("description"), format!("{description}\n")).expect("description"); set_public(&path, public); } } // Writes a tree for files whose names may contain directory separators. fn build_tree(repo: &Repository, files: &[(&str, &[u8])]) -> git2::Oid { let mut builder = repo.treebuilder(None).expect("tree builder"); let mut subdirs: Vec<(&str, Vec<(&str, &[u8])>)> = Vec::new(); for (file, content) in files { match file.split_once('/') { None => { let oid = repo.blob(content).expect("blob"); builder.insert(file, oid, 0o100644).expect("tree entry"); } Some((dir, rest)) => match subdirs.iter_mut().find(|(d, _)| *d == dir) { Some((_, entries)) => entries.push((rest, content)), None => subdirs.push((dir, vec![(rest, content)])), }, } } for (dir, entries) in subdirs { let oid = build_tree(repo, &entries); builder.insert(dir, oid, 0o040000).expect("subtree entry"); } builder.write().expect("tree") } pub fn set_public(repo_path: &Path, public: bool) { let marker = repo_path.join(mugit::repos::EXPORT_MARKER); if public { fs::write(&marker, "").expect("export marker"); } else if marker.exists() { fs::remove_file(&marker).expect("remove export marker"); } }