aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-registry/src/download_manager.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-08 19:52:18 +0900
committernsfisis <nsfisis@gmail.com>2026-05-08 19:52:18 +0900
commit5cb8fc4e306970764e84bb850da2c56f844c3b12 (patch)
tree0d66f3129a26138fcfee9402616b24929c40a017 /crates/mozart-registry/src/download_manager.rs
parentd83b9ef48775aeb31ba1909b29d5470e6d0ddaaa (diff)
downloadphp-mozart-5cb8fc4e306970764e84bb850da2c56f844c3b12.tar.gz
php-mozart-5cb8fc4e306970764e84bb850da2c56f844c3b12.tar.zst
php-mozart-5cb8fc4e306970764e84bb850da2c56f844c3b12.zip
fix(status): align with Composer's StatusCommand pipeline
Replace the dist-hash tree-diff implementation with Composer's VCS-level status flow: three buckets (errors / unpushed_changes / vcs_version_changes) populated via ChangeReportInterface / DvcsDownloaderInterface / VcsCapableDownloaderInterface, and a bitfield exit code (1|2|4) instead of always 1. Supporting work: - mozart-semver: add normalize_branch (VersionParser::normalizeBranch). - mozart-vcs: extend VcsDownloader trait with unpushed_changes / vcs_reference; port GitDownloader::getUnpushedChanges (HEAD-ref discovery + git diff --name-status remote...branch + two-pass fetch); fix git status invocation to use --untracked-files=no (Composer parity); add hasMetadataRepository preconditions to git/hg/svn local_changes; port VersionGuesser (git/hg/svn dispatch — Fossil omitted, feature branch detection runs sequentially instead of via async promises). - mozart-core: extend LocalPackage with pretty_version, package_type, installation_source, source, dist, extra; add InstallationSource and PackageReference. factory.rs reads them from installed.json. - mozart-registry: new download_manager mirroring DownloadManager::getDownloaderForPackage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-registry/src/download_manager.rs')
-rw-r--r--crates/mozart-registry/src/download_manager.rs140
1 files changed, 140 insertions, 0 deletions
diff --git a/crates/mozart-registry/src/download_manager.rs b/crates/mozart-registry/src/download_manager.rs
new file mode 100644
index 0000000..3e05517
--- /dev/null
+++ b/crates/mozart-registry/src/download_manager.rs
@@ -0,0 +1,140 @@
+//! `DownloadManager` — pick the right [`VcsDownloader`] for a given
+//! [`LocalPackage`]. Mirrors `Composer\Downloader\DownloadManager`.
+
+use std::path::PathBuf;
+
+use mozart_core::composer::{InstallationSource, LocalPackage};
+use mozart_vcs::downloader::VcsDownloader;
+use mozart_vcs::downloader::git::GitDownloader;
+use mozart_vcs::downloader::hg::HgDownloader;
+use mozart_vcs::downloader::svn::SvnDownloader;
+use mozart_vcs::process::ProcessExecutor;
+use mozart_vcs::util::git::GitUtil;
+use mozart_vcs::util::hg::HgUtil;
+use mozart_vcs::util::svn::SvnUtil;
+
+/// Selects a `VcsDownloader` for a package based on its installation source
+/// and source type. Mirrors `DownloadManager::getDownloaderForPackage`:
+///
+/// - `metapackage` → `None`.
+/// - `installation-source: dist` → `None` (Composer would return a
+/// `FileDownloader`-family object that does not implement
+/// `ChangeReportInterface` / `DvcsDownloaderInterface`, so the status
+/// command's `instanceof` checks all become no-ops; returning `None`
+/// directly is the equivalent in our trait-object world).
+/// - `installation-source: source` → the matching VCS downloader by
+/// `source.type` (`git` / `hg` / `svn`).
+pub struct DownloadManager {
+ git_cache_dir: PathBuf,
+}
+
+impl DownloadManager {
+ /// `git_cache_dir`: where `GitUtil` should keep mirror clones (e.g.
+ /// `<vendor>/.cache/git`).
+ pub fn new(git_cache_dir: PathBuf) -> Self {
+ Self { git_cache_dir }
+ }
+
+ pub fn for_package(&self, package: &LocalPackage) -> Option<Box<dyn VcsDownloader>> {
+ if package.package_type() == Some("metapackage") {
+ return None;
+ }
+ match package.installation_source()? {
+ InstallationSource::Dist => None,
+ InstallationSource::Source => {
+ let kind = package.source()?.kind.as_str();
+ match kind {
+ "git" => {
+ let git_util =
+ GitUtil::new(ProcessExecutor::new(), self.git_cache_dir.clone());
+ Some(Box::new(GitDownloader::new(git_util)))
+ }
+ "hg" => {
+ let hg_util = HgUtil::new(ProcessExecutor::new());
+ Some(Box::new(HgDownloader::new(hg_util)))
+ }
+ "svn" => {
+ let svn_util = SvnUtil::new(ProcessExecutor::new());
+ Some(Box::new(SvnDownloader::new(svn_util)))
+ }
+ _ => None,
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use mozart_core::composer::PackageReference;
+ use serde_json::Value;
+
+ fn pkg(
+ installation_source: Option<InstallationSource>,
+ source_kind: Option<&str>,
+ ) -> LocalPackage {
+ let source = source_kind.map(|kind| PackageReference {
+ kind: kind.to_string(),
+ url: "https://example/repo".into(),
+ reference: Some("abc123".into()),
+ shasum: None,
+ });
+ LocalPackage::new(
+ "vendor/pkg".into(),
+ "1.0.0".into(),
+ None,
+ Some("library".into()),
+ installation_source,
+ source,
+ None,
+ Value::Null,
+ )
+ }
+
+ #[test]
+ fn metapackage_returns_none() {
+ let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache"));
+ let mut p = pkg(Some(InstallationSource::Source), Some("git"));
+ // override type
+ p = LocalPackage::new(
+ "vendor/pkg".into(),
+ "1.0.0".into(),
+ None,
+ Some("metapackage".into()),
+ p.installation_source(),
+ p.source().cloned(),
+ None,
+ Value::Null,
+ );
+ assert!(dm.for_package(&p).is_none());
+ }
+
+ #[test]
+ fn dist_install_returns_none() {
+ let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache"));
+ let p = pkg(Some(InstallationSource::Dist), Some("git"));
+ assert!(dm.for_package(&p).is_none());
+ }
+
+ #[test]
+ fn source_install_with_git_returns_some() {
+ let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache"));
+ let p = pkg(Some(InstallationSource::Source), Some("git"));
+ assert!(dm.for_package(&p).is_some());
+ }
+
+ #[test]
+ fn unknown_source_kind_returns_none() {
+ let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache"));
+ let p = pkg(Some(InstallationSource::Source), Some("perforce"));
+ assert!(dm.for_package(&p).is_none());
+ }
+
+ #[test]
+ fn missing_installation_source_returns_none() {
+ let dm = DownloadManager::new(PathBuf::from("/tmp/mz-test-cache"));
+ let p = pkg(None, Some("git"));
+ assert!(dm.for_package(&p).is_none());
+ }
+}