aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-05 13:12:56 +0900
committernsfisis <nsfisis@gmail.com>2026-05-05 13:12:56 +0900
commita10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab (patch)
tree5dde666652768c4cbf3294e2ce17d78daa18b0c2 /crates/mozart-registry/src
parent78a627f9b839e902faec6e5f7fee4ec19fc0e4b8 (diff)
downloadphp-mozart-a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab.tar.gz
php-mozart-a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab.tar.zst
php-mozart-a10adb3b9ff3e1fe7ae0b8dc6440f171dde5f0ab.zip
feat(cache): include cache-vcs-dir in clear-cache command
Composer's clear-cache deletes cache-vcs-dir alongside repo/files caches, and GCs it via gcVcsCache (TTL-based, top-level subdir deletion, no size cap). Mozart was silently leaving VCS mirrors on disk forever — every clear-cache run grew the cache monotonically. Add cache_vcs_dir to CacheConfig (default {cache-dir}/vcs, env override COMPOSER_CACHE_VCS_DIR), wire it into clear-cache, and add Cache::gc_vcs mirroring Composer's gcVcsCache semantics.
Diffstat (limited to 'crates/mozart-registry/src')
-rw-r--r--crates/mozart-registry/src/cache.rs70
1 files changed, 70 insertions, 0 deletions
diff --git a/crates/mozart-registry/src/cache.rs b/crates/mozart-registry/src/cache.rs
index ac4b507..9b7d165 100644
--- a/crates/mozart-registry/src/cache.rs
+++ b/crates/mozart-registry/src/cache.rs
@@ -5,6 +5,7 @@
//! ~/.cache/mozart/ (or $COMPOSER_CACHE_DIR)
//! files/ dist archives (key: vendor~package~reference.ext)
//! repo/ API responses (key: provider-vendor~package.json)
+//! vcs/ VCS mirrors (one subdir per sanitized URL)
//! ```
use std::fs;
@@ -23,6 +24,8 @@ pub struct CacheConfig {
pub cache_files_dir: PathBuf,
/// Directory for API responses.
pub cache_repo_dir: PathBuf,
+ /// Directory for VCS mirrors (one subdirectory per sanitized URL).
+ pub cache_vcs_dir: PathBuf,
/// TTL in seconds for repo entries (default: 15,552,000 = 6 months).
pub cache_ttl: u64,
/// TTL in seconds for files entries (falls back to `cache_ttl`).
@@ -62,10 +65,14 @@ pub fn build_cache_config(cli_no_cache: bool) -> CacheConfig {
let cache_files_dir = cache_dir.join("files");
let cache_repo_dir = cache_dir.join("repo");
+ let cache_vcs_dir = std::env::var("COMPOSER_CACHE_VCS_DIR")
+ .map(PathBuf::from)
+ .unwrap_or_else(|_| cache_dir.join("vcs"));
CacheConfig {
cache_files_dir,
cache_repo_dir,
+ cache_vcs_dir,
cache_ttl: CacheConfig::DEFAULT_TTL,
cache_files_ttl: CacheConfig::DEFAULT_TTL,
cache_files_maxsize: CacheConfig::DEFAULT_FILES_MAXSIZE,
@@ -246,6 +253,42 @@ impl Cache {
Ok(())
}
+ /// Run garbage collection on a VCS cache bucket.
+ ///
+ /// Each top-level subdirectory is one bare mirror keyed by sanitized URL.
+ /// Deletes entire subdirectories whose mtime is older than `ttl_seconds`.
+ /// Mirrors Composer's `Cache::gcVcsCache`.
+ pub fn gc_vcs(&self, ttl_seconds: u64) -> anyhow::Result<()> {
+ if !self.enabled || !self.root.exists() {
+ return Ok(());
+ }
+
+ let now = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_secs();
+
+ for entry in fs::read_dir(&self.root)? {
+ let entry = entry?;
+ let path = entry.path();
+ let metadata = entry.metadata()?;
+ if !metadata.is_dir() {
+ continue;
+ }
+ let mtime = metadata
+ .modified()
+ .ok()
+ .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
+ .map(|d| d.as_secs())
+ .unwrap_or(0);
+ if now.saturating_sub(mtime) > ttl_seconds {
+ let _ = fs::remove_dir_all(&path);
+ }
+ }
+
+ Ok(())
+ }
+
/// Return the age in seconds of a cached entry based on its mtime,
/// or `None` if the entry doesn't exist or mtime can't be read.
pub fn age(&self, key: &str) -> Option<u64> {
@@ -461,6 +504,33 @@ mod tests {
);
}
+ // ──────────── gc_vcs (top-level subdir TTL deletion) ────────────
+
+ #[test]
+ fn test_gc_vcs_removes_old_subdirs() {
+ let dir = tempdir().unwrap();
+ let cache = Cache::new(dir.path().to_path_buf(), true);
+
+ let old_mirror = dir.path().join("old-mirror");
+ let new_mirror = dir.path().join("new-mirror");
+ fs::create_dir_all(&old_mirror).unwrap();
+ fs::write(old_mirror.join("HEAD"), "ref: refs/heads/main\n").unwrap();
+ fs::create_dir_all(&new_mirror).unwrap();
+ fs::write(new_mirror.join("HEAD"), "ref: refs/heads/main\n").unwrap();
+
+ let two_hours_ago = SystemTime::now() - Duration::from_secs(7200);
+ filetime::set_file_mtime(
+ &old_mirror,
+ filetime::FileTime::from_system_time(two_hours_ago),
+ )
+ .unwrap();
+
+ cache.gc_vcs(3600).unwrap();
+
+ assert!(!old_mirror.exists(), "expired mirror should be removed");
+ assert!(new_mirror.exists(), "fresh mirror should remain");
+ }
+
// ──────────── age ────────────
#[test]