use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use git2::{Repository, Signature};
pub fn cwd_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
pub fn make_temp_dir(prefix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before UNIX_EPOCH - clock misconfigured")
.as_nanos();
let dir = env::temp_dir().join(format!("{}-{}-{}", prefix, std::process::id(), nanos));
fs::create_dir_all(&dir)
.unwrap_or_else(|e| panic!("failed to create temp dir at {:?}: {}", dir, e));
dir
}
pub fn git(dir: &Path, args: &[&str]) {
if args.is_empty() {
panic!("git() called with empty args");
}
match args[0] {
"init" => {
Repository::init(dir).expect("failed to init repo");
}
"config" if args.len() >= 3 => {
let repo = Repository::open(dir).expect("failed to open repo");
let mut config = repo.config().expect("failed to get config");
config
.set_str(args[1], args[2])
.expect("failed to set config");
}
"add" if args.len() >= 2 => {
let repo = Repository::open(dir).expect("failed to open repo");
let mut index = repo.index().expect("failed to get index");
if args[1] == "." {
index
.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
.expect("failed to add all files");
} else {
for arg in &args[1..] {
index.add_path(Path::new(arg)).expect("failed to add file");
}
}
index.write().expect("failed to write index");
}
"commit" if args.len() >= 3 && args[1] == "-m" => {
let repo = Repository::open(dir).expect("failed to open repo");
let sig = Signature::now("Test User", "test@example.com")
.expect("failed to create signature");
let mut index = repo.index().expect("failed to get index");
let tree_oid = index.write_tree().expect("failed to write tree");
let tree = repo.find_tree(tree_oid).expect("failed to find tree");
let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
let parents: Vec<&git2::Commit> = parent.iter().collect();
repo.commit(Some("HEAD"), &sig, &sig, args[2], &tree, &parents)
.expect("failed to create commit");
}
"commit" if args.contains(&"--allow-empty") => {
let repo = Repository::open(dir).expect("failed to open repo");
let sig = Signature::now("Test User", "test@example.com")
.expect("failed to create signature");
let parent = repo
.head()
.ok()
.and_then(|h| h.peel_to_commit().ok())
.expect("no HEAD for empty commit");
let tree = parent.tree().expect("failed to get tree");
let msg = args
.iter()
.skip_while(|a| *a != &"-m")
.nth(1)
.unwrap_or(&"empty");
repo.commit(Some("HEAD"), &sig, &sig, msg, &tree, &[&parent])
.expect("failed to create commit");
}
"checkout" if args.len() >= 3 && args[1] == "-b" => {
let repo = Repository::open(dir).expect("failed to open repo");
let head = repo.head().expect("failed to get HEAD");
let commit = head.peel_to_commit().expect("failed to get commit");
repo.branch(args[2], &commit, false)
.expect("failed to create branch");
let refname = format!("refs/heads/{}", args[2]);
repo.set_head(&refname).expect("failed to set HEAD");
}
"checkout" if args.len() >= 2 => {
let repo = Repository::open(dir).expect("failed to open repo");
let refname = format!("refs/heads/{}", args[1]);
repo.set_head(&refname).expect("failed to set HEAD");
repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
.expect("failed to checkout");
}
_ => {
panic!(
"Unsupported git command in test: {:?}. Add git2 implementation to test_utils::git()",
args
);
}
}
}
#[cfg(feature = "jj")]
pub fn jj(dir: &Path, args: &[&str]) -> bool {
Command::new("jj")
.current_dir(dir)
.args(args)
.status()
.map(|s| s.success())
.unwrap_or(false)
}
pub struct RepoGuard {
_lock: MutexGuard<'static, ()>,
pub dir: PathBuf,
original: PathBuf,
}
impl RepoGuard {
pub fn new() -> Self {
let lock = match cwd_lock().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let original = env::current_dir().expect("failed to get cwd");
let dir = make_temp_dir("lumen-test");
let repo = Repository::init(&dir).expect("failed to init repo");
let mut config = repo.config().expect("failed to get config");
config
.set_str("user.email", "test@example.com")
.expect("failed to set email");
config
.set_str("user.name", "Test User")
.expect("failed to set name");
fs::write(dir.join("README.md"), "hello\n").expect("failed to write file");
let mut index = repo.index().expect("failed to get index");
index
.add_path(Path::new("README.md"))
.expect("failed to add file");
index.write().expect("failed to write index");
let tree_oid = index.write_tree().expect("failed to write tree");
let tree = repo.find_tree(tree_oid).expect("failed to find tree");
let sig =
Signature::now("Test User", "test@example.com").expect("failed to create signature");
repo.commit(
Some("HEAD"),
&sig,
&sig,
"init",
&tree,
&[], )
.expect("failed to create commit");
env::set_current_dir(&dir).expect("failed to set cwd");
Self {
_lock: lock,
dir,
original,
}
}
}
impl Drop for RepoGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.original);
let _ = fs::remove_dir_all(&self.dir);
}
}
#[cfg(feature = "jj")]
pub struct JjRepoGuard {
_lock: MutexGuard<'static, ()>,
pub dir: PathBuf,
original: PathBuf,
}
#[cfg(feature = "jj")]
impl JjRepoGuard {
pub fn new() -> Option<Self> {
let lock = match cwd_lock().lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
let original = env::current_dir().expect("failed to get cwd");
let dir = make_temp_dir("lumen-jj-test");
if !jj(&dir, &["git", "init"]) {
return None;
}
jj(
&dir,
&["config", "set", "--repo", "user.email", "test@example.com"],
);
jj(&dir, &["config", "set", "--repo", "user.name", "Test User"]);
fs::write(dir.join("README.md"), "hello\n").expect("failed to write file");
jj(&dir, &["status"]);
env::set_current_dir(&dir).expect("failed to set cwd");
Some(Self {
_lock: lock,
dir,
original,
})
}
}
#[cfg(feature = "jj")]
impl Drop for JjRepoGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.original);
let _ = fs::remove_dir_all(&self.dir);
}
}