use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
use super::lockfile::RepoEntry;
pub fn materialize(repo: &RepoEntry, lock_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
if let Some(rel) = &repo.path {
let dir = lock_dir.join(rel);
if !dir.is_dir() {
bail!(
"repo {}: local fixture {} is not a directory",
repo.name,
dir.display()
);
}
return Ok(dir);
}
let url = repo
.url
.as_deref()
.context("remote repo entry without url (should be unreachable)")?;
let commit = repo
.commit
.as_deref()
.context("remote repo entry without commit (should be unreachable)")?;
std::fs::create_dir_all(cache_dir)
.with_context(|| format!("creating cache dir {}", cache_dir.display()))?;
let dest = cache_dir.join(&repo.name);
if !dest.join(".git").is_dir() {
git(&["clone", "--quiet", url, &dest.to_string_lossy()], None)
.with_context(|| format!("cloning {url} for repo {}", repo.name))?;
}
if git(&["checkout", "--quiet", commit], Some(&dest)).is_err() {
git(&["fetch", "--quiet", "--all", "--tags"], Some(&dest))
.with_context(|| format!("fetching {url} for repo {}", repo.name))?;
git(&["checkout", "--quiet", commit], Some(&dest)).with_context(|| {
format!("checking out pinned commit {commit} in repo {}", repo.name)
})?;
}
let head = git(&["rev-parse", "HEAD"], Some(&dest))?;
let head = head.trim();
if head != commit && !head.starts_with(commit) {
bail!(
"repo {}: checked-out HEAD {head} does not match pinned commit {commit}",
repo.name
);
}
Ok(dest)
}
fn git(args: &[&str], dir: Option<&Path>) -> Result<String> {
let mut cmd = Command::new("git");
if let Some(d) = dir {
cmd.current_dir(d);
}
cmd.args(args);
let out = cmd
.output()
.with_context(|| format!("spawning git {}", args.join(" ")))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
bail!("git {} failed: {}", args.join(" "), stderr.trim());
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_fixture_resolves_against_lock_dir() {
let root = tempfile::tempdir().unwrap();
std::fs::create_dir_all(root.path().join("repos/qa")).unwrap();
let entry = RepoEntry {
name: "qa".into(),
url: None,
commit: None,
path: Some("repos/qa".into()),
suite: "qa.ndjson".into(),
};
let got = materialize(&entry, root.path(), &root.path().join("cache")).unwrap();
assert_eq!(got, root.path().join("repos/qa"));
}
#[test]
fn missing_local_fixture_errors() {
let root = tempfile::tempdir().unwrap();
let entry = RepoEntry {
name: "qa".into(),
url: None,
commit: None,
path: Some("nope".into()),
suite: "qa.ndjson".into(),
};
assert!(materialize(&entry, root.path(), &root.path().join("cache")).is_err());
}
#[cfg(unix)]
#[test]
fn clones_and_checks_out_a_local_git_repo() {
let root = tempfile::tempdir().unwrap();
let origin = root.path().join("origin");
std::fs::create_dir_all(&origin).unwrap();
let run = |args: &[&str]| {
assert!(
Command::new("git")
.current_dir(&origin)
.args(args)
.output()
.unwrap()
.status
.success(),
"git {args:?} failed"
);
};
run(&["init", "--quiet"]);
run(&["config", "user.email", "t@t"]);
run(&["config", "user.name", "t"]);
std::fs::write(origin.join("file.txt"), "hello").unwrap();
run(&["add", "."]);
run(&["commit", "--quiet", "-m", "init"]);
let commit = String::from_utf8_lossy(
&Command::new("git")
.current_dir(&origin)
.args(["rev-parse", "HEAD"])
.output()
.unwrap()
.stdout,
)
.trim()
.to_string();
let entry = RepoEntry {
name: "fix".into(),
url: Some(origin.to_string_lossy().into_owned()),
commit: Some(commit),
path: None,
suite: "s.ndjson".into(),
};
let cache = root.path().join("cache");
let dest = materialize(&entry, root.path(), &cache).unwrap();
assert!(dest.join("file.txt").exists());
let dest2 = materialize(&entry, root.path(), &cache).unwrap();
assert_eq!(dest, dest2);
}
#[cfg(unix)]
#[test]
fn wrong_pinned_commit_errors() {
let root = tempfile::tempdir().unwrap();
let origin = root.path().join("origin");
std::fs::create_dir_all(&origin).unwrap();
for args in [
vec!["init", "--quiet"],
vec!["config", "user.email", "t@t"],
vec!["config", "user.name", "t"],
] {
Command::new("git")
.current_dir(&origin)
.args(&args)
.output()
.unwrap();
}
std::fs::write(origin.join("f"), "x").unwrap();
for args in [vec!["add", "."], vec!["commit", "--quiet", "-m", "i"]] {
Command::new("git")
.current_dir(&origin)
.args(&args)
.output()
.unwrap();
}
let entry = RepoEntry {
name: "fix".into(),
url: Some(origin.to_string_lossy().into_owned()),
commit: Some("0000000000000000000000000000000000000000".into()),
path: None,
suite: "s.ndjson".into(),
};
assert!(materialize(&entry, root.path(), &root.path().join("cache")).is_err());
}
}