use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use super::index::{PackageEntry, PackageIndex};
pub struct RegistryCache {
name: String,
url: String,
}
impl RegistryCache {
pub fn new(name: impl Into<String>, url: impl Into<String>) -> Self {
Self {
name: name.into(),
url: url.into(),
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn local_path(&self) -> PathBuf {
ccgo_home().join("registries").join(&self.name)
}
pub fn ensure_synced(&self, verbose: bool) -> Result<()> {
let path = self.local_path();
std::fs::create_dir_all(path.parent().expect("registries path always has a parent"))
.with_context(|| format!("failed to create registries dir for '{}'", self.name))?;
if path.join(".git").is_dir() {
self.run_git(&["fetch", "--quiet", "origin"], &path, verbose)?;
self.run_git(
&["reset", "--hard", "--quiet", "FETCH_HEAD"],
&path,
verbose,
)
.with_context(|| format!("failed to reset registry '{}' to FETCH_HEAD", self.name))
} else {
self.run_git(
&[
"clone",
"--quiet",
&self.url,
path.to_str().expect("local registry path is UTF-8"),
],
Path::new("."),
verbose,
)
.with_context(|| format!("failed to clone registry '{}' from {}", self.name, self.url))
}
}
pub fn lookup(&self, package: &str) -> Result<Option<PackageEntry>> {
let rel = PackageIndex::package_index_path(package);
let abs = self.local_path().join(&rel);
match std::fs::read_to_string(&abs) {
Ok(s) => {
let entry: PackageEntry = serde_json::from_str(&s)
.with_context(|| format!("failed to parse {}", abs.display()))?;
Ok(Some(entry))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e).with_context(|| format!("failed to read {}", abs.display())),
}
}
fn run_git(&self, args: &[&str], cwd: &Path, verbose: bool) -> Result<()> {
let mut cmd = std::process::Command::new("git");
cmd.args(args).current_dir(cwd);
if verbose {
eprintln!("[registry:{}] git {}", self.name, args.join(" "));
}
let status = cmd
.status()
.with_context(|| format!("failed to spawn git in {}", cwd.display()))?;
if !status.success() {
anyhow::bail!(
"git {} exited with code {:?}",
args.join(" "),
status.code()
);
}
Ok(())
}
}
fn ccgo_home() -> PathBuf {
if let Ok(h) = std::env::var("CCGO_HOME") {
return PathBuf::from(h);
}
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.expect("HOME or USERPROFILE must be set");
PathBuf::from(home).join(".ccgo")
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn run_git(args: &[&str], cwd: &Path) {
let status = Command::new("git")
.args(args)
.current_dir(cwd)
.status()
.unwrap();
assert!(status.success(), "git {} failed", args.join(" "));
}
fn make_synthetic_index(parent: &Path, package_json: Option<&str>) -> PathBuf {
let index_dir = parent.join("upstream-index");
let leaf_rel = PackageIndex::package_index_path("leaf");
let leaf_abs = index_dir.join(&leaf_rel);
std::fs::create_dir_all(
leaf_abs
.parent()
.expect("package_index_path always has a parent dir"),
)
.unwrap();
run_git(&["init", "-q"], &index_dir);
run_git(&["config", "user.email", "test@example.com"], &index_dir);
run_git(&["config", "user.name", "ccgo-test"], &index_dir);
let json = package_json.unwrap_or(
r#"{"name":"leaf","description":"x","repository":"x","license":"MIT","platforms":[],"versions":[{"version":"1.0.0","tag":"v1.0.0"}]}"#,
);
std::fs::write(&leaf_abs, json).unwrap();
run_git(&["add", "-A"], &index_dir);
run_git(&["commit", "-q", "-m", "init", "--no-verify"], &index_dir);
index_dir
}
#[test]
#[serial_test::serial]
fn ensure_synced_clones_when_absent() {
let tmp = tempfile::TempDir::new().unwrap();
std::env::set_var("CCGO_HOME", tmp.path());
let upstream = make_synthetic_index(tmp.path(), None);
let url = format!("file://{}", upstream.display());
let cache = RegistryCache::new("test", url);
cache.ensure_synced(false).unwrap();
assert!(cache.local_path().join(".git").is_dir());
let leaf_rel = PackageIndex::package_index_path("leaf");
assert!(cache.local_path().join(&leaf_rel).is_file());
}
#[test]
#[serial_test::serial]
fn ensure_synced_pulls_when_already_cloned() {
let tmp = tempfile::TempDir::new().unwrap();
std::env::set_var("CCGO_HOME", tmp.path());
let upstream = make_synthetic_index(tmp.path(), None);
let url = format!("file://{}", upstream.display());
let cache = RegistryCache::new("test", url);
cache.ensure_synced(false).unwrap();
cache.ensure_synced(false).unwrap();
}
#[test]
#[serial_test::serial]
fn lookup_returns_some_for_existing_package() {
let tmp = tempfile::TempDir::new().unwrap();
std::env::set_var("CCGO_HOME", tmp.path());
let upstream = make_synthetic_index(tmp.path(), None);
let url = format!("file://{}", upstream.display());
let cache = RegistryCache::new("test", url);
cache.ensure_synced(false).unwrap();
let entry = cache.lookup("leaf").unwrap().expect("expected leaf entry");
assert_eq!(entry.name, "leaf");
assert_eq!(entry.versions.len(), 1);
}
#[test]
#[serial_test::serial]
fn lookup_returns_none_for_missing_package() {
let tmp = tempfile::TempDir::new().unwrap();
std::env::set_var("CCGO_HOME", tmp.path());
let upstream = make_synthetic_index(tmp.path(), None);
let url = format!("file://{}", upstream.display());
let cache = RegistryCache::new("test", url);
cache.ensure_synced(false).unwrap();
assert!(cache.lookup("never-existed").unwrap().is_none());
}
#[test]
#[serial_test::serial]
fn lookup_returns_none_when_cache_not_yet_synced() {
let tmp = tempfile::TempDir::new().unwrap();
std::env::set_var("CCGO_HOME", tmp.path());
let cache = RegistryCache::new("test", "file:///never-cloned");
assert!(cache.lookup("leaf").unwrap().is_none());
}
}