From 59bab6efee41a196b0d9d392167c536abbe068ba Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 10 May 2026 20:31:00 +0900 Subject: refactor(downloader): introduce top-level downloader module Move VCS downloaders and DownloadManager out of vcs/repository into a new top-level downloader module mirroring Composer\Downloader, and add stub types for the remaining Composer downloader hierarchy (file, archive variants, path, perforce, fossil, exceptions, interfaces) so future ports have a home. --- crates/mozart-core/src/downloader.rs | 47 ++++ .../src/downloader/archive_downloader.rs | 1 + .../src/downloader/change_report_interface.rs | 1 + .../mozart-core/src/downloader/download_manager.rs | 139 +++++++++++ .../src/downloader/downloader_interface.rs | 1 + .../src/downloader/dvcs_downloader_interface.rs | 1 + .../mozart-core/src/downloader/file_downloader.rs | 1 + .../src/downloader/filesystem_exception.rs | 1 + .../src/downloader/fossil_downloader.rs | 1 + .../mozart-core/src/downloader/git_downloader.rs | 272 +++++++++++++++++++++ .../mozart-core/src/downloader/gzip_downloader.rs | 1 + crates/mozart-core/src/downloader/hg_downloader.rs | 84 +++++++ .../downloader/max_file_size_exceeded_exception.rs | 1 + .../mozart-core/src/downloader/path_downloader.rs | 1 + .../src/downloader/perforce_downloader.rs | 1 + .../mozart-core/src/downloader/phar_downloader.rs | 1 + .../mozart-core/src/downloader/rar_downloader.rs | 1 + .../mozart-core/src/downloader/svn_downloader.rs | 85 +++++++ .../mozart-core/src/downloader/tar_downloader.rs | 1 + .../src/downloader/transport_exception.rs | 1 + .../downloader/vcs_capable_downloader_interface.rs | 1 + .../mozart-core/src/downloader/vcs_downloader.rs | 55 +++++ crates/mozart-core/src/downloader/xz_downloader.rs | 1 + .../mozart-core/src/downloader/zip_downloader.rs | 1 + crates/mozart-core/src/lib.rs | 1 + crates/mozart-core/src/repository.rs | 1 - .../mozart-core/src/repository/download_manager.rs | 143 ----------- .../repository/installer_executor/filesystem.rs | 18 +- crates/mozart-core/src/vcs.rs | 1 - crates/mozart-core/src/vcs/downloader/git.rs | 271 -------------------- crates/mozart-core/src/vcs/downloader/hg.rs | 84 ------- crates/mozart-core/src/vcs/downloader/mod.rs | 56 ----- crates/mozart-core/src/vcs/downloader/svn.rs | 84 ------- crates/mozart-core/tests/git_driver_test.rs | 3 +- crates/mozart/src/composer.rs | 2 +- crates/mozart/src/factory.rs | 2 +- 36 files changed, 713 insertions(+), 653 deletions(-) create mode 100644 crates/mozart-core/src/downloader.rs create mode 100644 crates/mozart-core/src/downloader/archive_downloader.rs create mode 100644 crates/mozart-core/src/downloader/change_report_interface.rs create mode 100644 crates/mozart-core/src/downloader/download_manager.rs create mode 100644 crates/mozart-core/src/downloader/downloader_interface.rs create mode 100644 crates/mozart-core/src/downloader/dvcs_downloader_interface.rs create mode 100644 crates/mozart-core/src/downloader/file_downloader.rs create mode 100644 crates/mozart-core/src/downloader/filesystem_exception.rs create mode 100644 crates/mozart-core/src/downloader/fossil_downloader.rs create mode 100644 crates/mozart-core/src/downloader/git_downloader.rs create mode 100644 crates/mozart-core/src/downloader/gzip_downloader.rs create mode 100644 crates/mozart-core/src/downloader/hg_downloader.rs create mode 100644 crates/mozart-core/src/downloader/max_file_size_exceeded_exception.rs create mode 100644 crates/mozart-core/src/downloader/path_downloader.rs create mode 100644 crates/mozart-core/src/downloader/perforce_downloader.rs create mode 100644 crates/mozart-core/src/downloader/phar_downloader.rs create mode 100644 crates/mozart-core/src/downloader/rar_downloader.rs create mode 100644 crates/mozart-core/src/downloader/svn_downloader.rs create mode 100644 crates/mozart-core/src/downloader/tar_downloader.rs create mode 100644 crates/mozart-core/src/downloader/transport_exception.rs create mode 100644 crates/mozart-core/src/downloader/vcs_capable_downloader_interface.rs create mode 100644 crates/mozart-core/src/downloader/vcs_downloader.rs create mode 100644 crates/mozart-core/src/downloader/xz_downloader.rs create mode 100644 crates/mozart-core/src/downloader/zip_downloader.rs delete mode 100644 crates/mozart-core/src/repository/download_manager.rs delete mode 100644 crates/mozart-core/src/vcs/downloader/git.rs delete mode 100644 crates/mozart-core/src/vcs/downloader/hg.rs delete mode 100644 crates/mozart-core/src/vcs/downloader/mod.rs delete mode 100644 crates/mozart-core/src/vcs/downloader/svn.rs diff --git a/crates/mozart-core/src/downloader.rs b/crates/mozart-core/src/downloader.rs new file mode 100644 index 0000000..f298085 --- /dev/null +++ b/crates/mozart-core/src/downloader.rs @@ -0,0 +1,47 @@ +mod archive_downloader; +mod change_report_interface; +mod download_manager; +mod downloader_interface; +mod dvcs_downloader_interface; +mod file_downloader; +mod filesystem_exception; +mod fossil_downloader; +mod git_downloader; +mod gzip_downloader; +mod hg_downloader; +mod max_file_size_exceeded_exception; +mod path_downloader; +mod perforce_downloader; +mod phar_downloader; +mod rar_downloader; +mod svn_downloader; +mod tar_downloader; +mod transport_exception; +mod vcs_capable_downloader_interface; +mod vcs_downloader; +mod xz_downloader; +mod zip_downloader; + +pub use archive_downloader::*; +pub use change_report_interface::*; +pub use download_manager::*; +pub use downloader_interface::*; +pub use dvcs_downloader_interface::*; +pub use file_downloader::*; +pub use filesystem_exception::*; +pub use fossil_downloader::*; +pub use git_downloader::*; +pub use gzip_downloader::*; +pub use hg_downloader::*; +pub use max_file_size_exceeded_exception::*; +pub use path_downloader::*; +pub use perforce_downloader::*; +pub use phar_downloader::*; +pub use rar_downloader::*; +pub use svn_downloader::*; +pub use tar_downloader::*; +pub use transport_exception::*; +pub use vcs_capable_downloader_interface::*; +pub use vcs_downloader::*; +pub use xz_downloader::*; +pub use zip_downloader::*; diff --git a/crates/mozart-core/src/downloader/archive_downloader.rs b/crates/mozart-core/src/downloader/archive_downloader.rs new file mode 100644 index 0000000..3023609 --- /dev/null +++ b/crates/mozart-core/src/downloader/archive_downloader.rs @@ -0,0 +1 @@ +pub struct ArchiveDownloader; diff --git a/crates/mozart-core/src/downloader/change_report_interface.rs b/crates/mozart-core/src/downloader/change_report_interface.rs new file mode 100644 index 0000000..3a3b483 --- /dev/null +++ b/crates/mozart-core/src/downloader/change_report_interface.rs @@ -0,0 +1 @@ +pub trait ChangeReportInterface {} diff --git a/crates/mozart-core/src/downloader/download_manager.rs b/crates/mozart-core/src/downloader/download_manager.rs new file mode 100644 index 0000000..c83bc64 --- /dev/null +++ b/crates/mozart-core/src/downloader/download_manager.rs @@ -0,0 +1,139 @@ +//! `DownloadManager` — pick the right [`VcsDownloader`] for a given +//! [`LocalPackage`]. Mirrors `Composer\Downloader\DownloadManager`. + +use crate::composer::{InstallationSource, LocalPackage}; +use crate::downloader::{GitDownloader, HgDownloader, SvnDownloader, VcsDownloader}; +use crate::vcs::process::ProcessExecutor; +use crate::vcs::util::git::GitUtil; +use crate::vcs::util::hg::HgUtil; +use crate::vcs::util::svn::SvnUtil; +use std::path::PathBuf; + +/// 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. + /// `/.cache/git`). + pub fn new(git_cache_dir: PathBuf) -> Self { + Self { git_cache_dir } + } + + pub fn get_downloader_for_package( + &self, + package: &LocalPackage, + ) -> Option> { + 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 crate::composer::PackageReference; + use serde_json::Value; + + fn pkg( + installation_source: Option, + 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.get_downloader_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.get_downloader_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.get_downloader_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.get_downloader_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.get_downloader_for_package(&p).is_none()); + } +} diff --git a/crates/mozart-core/src/downloader/downloader_interface.rs b/crates/mozart-core/src/downloader/downloader_interface.rs new file mode 100644 index 0000000..9c1b585 --- /dev/null +++ b/crates/mozart-core/src/downloader/downloader_interface.rs @@ -0,0 +1 @@ +pub trait DownloaderInterface {} diff --git a/crates/mozart-core/src/downloader/dvcs_downloader_interface.rs b/crates/mozart-core/src/downloader/dvcs_downloader_interface.rs new file mode 100644 index 0000000..3f576f5 --- /dev/null +++ b/crates/mozart-core/src/downloader/dvcs_downloader_interface.rs @@ -0,0 +1 @@ +pub trait DvcsDownloaderInterface {} diff --git a/crates/mozart-core/src/downloader/file_downloader.rs b/crates/mozart-core/src/downloader/file_downloader.rs new file mode 100644 index 0000000..81625c4 --- /dev/null +++ b/crates/mozart-core/src/downloader/file_downloader.rs @@ -0,0 +1 @@ +pub struct FileDownloader; diff --git a/crates/mozart-core/src/downloader/filesystem_exception.rs b/crates/mozart-core/src/downloader/filesystem_exception.rs new file mode 100644 index 0000000..f978219 --- /dev/null +++ b/crates/mozart-core/src/downloader/filesystem_exception.rs @@ -0,0 +1 @@ +pub struct FilesystemException; diff --git a/crates/mozart-core/src/downloader/fossil_downloader.rs b/crates/mozart-core/src/downloader/fossil_downloader.rs new file mode 100644 index 0000000..8df300c --- /dev/null +++ b/crates/mozart-core/src/downloader/fossil_downloader.rs @@ -0,0 +1 @@ +pub struct FossilDownloader; diff --git a/crates/mozart-core/src/downloader/git_downloader.rs b/crates/mozart-core/src/downloader/git_downloader.rs new file mode 100644 index 0000000..d4d8c44 --- /dev/null +++ b/crates/mozart-core/src/downloader/git_downloader.rs @@ -0,0 +1,272 @@ +use anyhow::Result; +use regex::Regex; +use std::path::Path; +use std::sync::LazyLock; + +use crate::downloader::VcsDownloader; +use crate::vcs::process::ProcessExecutor; +use crate::vcs::util::git::GitUtil; + +/// Match ` HEAD` lines in `git show-ref --head -d` output. +static HEAD_REF_RE: LazyLock = + LazyLock::new(|| Regex::new(r"(?im)^([a-f0-9]+) HEAD$").unwrap()); + +/// Git downloader using clone/checkout with optional mirror cache. +/// +/// Corresponds to Composer's `Downloader\GitDownloader`. +pub struct GitDownloader { + git_util: GitUtil, +} + +impl GitDownloader { + pub fn new(git_util: GitUtil) -> Self { + Self { git_util } + } +} + +impl VcsDownloader for GitDownloader { + fn download(&self, url: &str, _reference: &str, _target: &Path) -> Result<()> { + // Pre-sync the mirror so install can use --reference + self.git_util.sync_mirror(url)?; + Ok(()) + } + + fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> { + let target_str = target.to_string_lossy(); + let mirror_path = self.git_util.mirror_path(url); + + if mirror_path.join("HEAD").exists() { + // Clone with mirror reference for efficiency + let mirror_str = mirror_path.to_string_lossy().to_string(); + self.git_util.run_command( + &[ + "git", + "clone", + "--no-checkout", + "--dissociate", + "--reference", + &mirror_str, + "--", + url, + &target_str, + ], + url, + None, + )?; + } else { + self.git_util.run_command( + &["git", "clone", "--no-checkout", "--", url, &target_str], + url, + None, + )?; + } + + // Checkout the specific reference + let process = ProcessExecutor::new(); + process.execute_checked(&["git", "checkout", reference, "--force"], Some(target))?; + + Ok(()) + } + + fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> { + let process = ProcessExecutor::new(); + + // Update remote URL + process.execute_checked( + &["git", "remote", "set-url", "origin", "--", url], + Some(target), + )?; + + // Fetch latest + self.git_util + .run_command(&["git", "fetch", "origin"], url, Some(target))?; + + // Checkout new reference + process.execute_checked(&["git", "checkout", new_ref, "--force"], Some(target))?; + + Ok(()) + } + + fn remove(&self, target: &Path) -> Result<()> { + if target.exists() { + std::fs::remove_dir_all(target)?; + } + Ok(()) + } + + fn get_local_changes(&self, target: &Path) -> Result> { + if !target.join(".git").exists() { + return Ok(None); + } + let process = ProcessExecutor::new(); + let output = process.execute( + &["git", "status", "--porcelain", "--untracked-files=no"], + Some(target), + )?; + let trimmed = output.stdout.trim(); + if trimmed.is_empty() { + Ok(None) + } else { + Ok(Some(trimmed.to_string())) + } + } + + fn vcs_reference(&self, target: &Path) -> Result> { + if !target.join(".git").exists() { + return Ok(None); + } + let process = ProcessExecutor::new(); + let output = process.execute(&["git", "rev-parse", "HEAD"], Some(target))?; + if output.status != 0 { + return Ok(None); + } + let trimmed = output.stdout.trim(); + if trimmed.is_empty() { + Ok(None) + } else { + Ok(Some(trimmed.to_string())) + } + } + + fn unpushed_changes(&self, target: &Path) -> Result> { + if !target.join(".git").exists() { + return Ok(None); + } + let process = ProcessExecutor::new(); + + let mut refs = match collect_show_ref(&process, target)? { + Some(r) => r, + None => return Ok(None), + }; + + let head_ref = match HEAD_REF_RE + .captures(&refs) + .and_then(|c| c.get(1)) + .map(|m| m.as_str().to_string()) + { + Some(h) => h, + None => return Ok(None), + }; + + let candidate_branches = collect_local_branches(&refs, &head_ref); + if candidate_branches.is_empty() { + // not on a branch (detached / tag) — skip + return Ok(None); + } + + let mut branch = candidate_branches[0].clone(); + let mut unpushed_changes: Option = None; + let mut branch_not_found_error = false; + + for i in 0..=1 { + let mut remote_branches: Vec = Vec::new(); + + for candidate in &candidate_branches { + let matches = collect_remote_branches(&refs, candidate); + if !matches.is_empty() { + branch = candidate.clone(); + remote_branches = matches; + break; + } + } + + if remote_branches.is_empty() { + unpushed_changes = Some(format!( + "Branch {branch} could not be found on any remote and appears to be unpushed" + )); + branch_not_found_error = true; + } else { + if branch_not_found_error { + unpushed_changes = None; + } + for remote_branch in &remote_branches { + let range = format!("{remote_branch}...{branch}"); + let output = process.execute_checked( + &["git", "diff", "--name-status", &range, "--"], + Some(target), + )?; + let trimmed = output.stdout.trim().to_string(); + match unpushed_changes { + None => unpushed_changes = Some(trimmed), + Some(ref existing) if trimmed.len() < existing.len() => { + unpushed_changes = Some(trimmed); + } + _ => {} + } + } + } + + if unpushed_changes.as_deref().is_some_and(|s| !s.is_empty()) && i == 0 { + let _ = process.execute(&["git", "fetch", "--all"], Some(target))?; + refs = match collect_show_ref(&process, target)? { + Some(r) => r, + None => return Ok(unpushed_changes), + }; + } + + if unpushed_changes.as_deref().is_none_or(str::is_empty) { + break; + } + } + + Ok(unpushed_changes.filter(|s| !s.is_empty())) + } + + fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result { + let process = ProcessExecutor::new(); + let range = format!("{from}..{to}"); + let output = process.execute( + &["git", "log", &range, "--oneline", "--no-decorate"], + Some(target), + )?; + Ok(output.stdout) + } + + fn is_change_report(&self) -> bool { + true + } + + fn is_vcs_capable_downloader(&self) -> bool { + true + } + + fn is_dvcs_downloader(&self) -> bool { + true + } +} + +fn collect_show_ref(process: &ProcessExecutor, target: &Path) -> Result> { + let output = process.execute(&["git", "show-ref", "--head", "-d"], Some(target))?; + if output.status != 0 { + anyhow::bail!( + "Failed to execute git show-ref --head -d\n\n{}", + output.stderr.trim() + ); + } + Ok(Some(output.stdout.trim().to_string())) +} + +fn collect_local_branches(refs: &str, head_ref: &str) -> Vec { + let pattern = format!(r"(?im)^{} refs/heads/(.+)$", regex::escape(head_ref)); + let re = match Regex::new(&pattern) { + Ok(r) => r, + Err(_) => return Vec::new(), + }; + re.captures_iter(refs) + .filter_map(|c| c.get(1).map(|m| m.as_str().to_string())) + .collect() +} + +fn collect_remote_branches(refs: &str, candidate: &str) -> Vec { + let pattern = format!( + r"(?im)^[a-f0-9]+ refs/remotes/((?:[^/]+)/{})$", + regex::escape(candidate) + ); + let re = match Regex::new(&pattern) { + Ok(r) => r, + Err(_) => return Vec::new(), + }; + re.captures_iter(refs) + .filter_map(|c| c.get(1).map(|m| m.as_str().to_string())) + .collect() +} diff --git a/crates/mozart-core/src/downloader/gzip_downloader.rs b/crates/mozart-core/src/downloader/gzip_downloader.rs new file mode 100644 index 0000000..c329617 --- /dev/null +++ b/crates/mozart-core/src/downloader/gzip_downloader.rs @@ -0,0 +1 @@ +pub struct GzipDownloader; diff --git a/crates/mozart-core/src/downloader/hg_downloader.rs b/crates/mozart-core/src/downloader/hg_downloader.rs new file mode 100644 index 0000000..9fb918e --- /dev/null +++ b/crates/mozart-core/src/downloader/hg_downloader.rs @@ -0,0 +1,84 @@ +use anyhow::Result; +use std::path::Path; + +use crate::{downloader::VcsDownloader, vcs::util::hg::HgUtil}; + +/// Mercurial downloader using clone/pull/update. +pub struct HgDownloader { + hg_util: HgUtil, +} + +impl HgDownloader { + pub fn new(hg_util: HgUtil) -> Self { + Self { hg_util } + } +} + +impl VcsDownloader for HgDownloader { + fn download(&self, _url: &str, _reference: &str, _target: &Path) -> Result<()> { + Ok(()) + } + + fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> { + let target_str = target.to_string_lossy().to_string(); + self.hg_util + .execute(&["clone", "--", url, &target_str], None)?; + self.hg_util + .execute(&["update", "-r", reference], Some(target))?; + Ok(()) + } + + fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> { + self.hg_util.execute(&["pull", url], Some(target))?; + self.hg_util + .execute(&["update", "-r", new_ref], Some(target))?; + Ok(()) + } + + fn remove(&self, target: &Path) -> Result<()> { + if target.exists() { + std::fs::remove_dir_all(target)?; + } + Ok(()) + } + + fn get_local_changes(&self, target: &Path) -> Result> { + if !target.join(".hg").is_dir() { + return Ok(None); + } + let output = self.hg_util.execute(&["st"], Some(target))?; + let trimmed = output.stdout.trim(); + if trimmed.is_empty() { + Ok(None) + } else { + Ok(Some(trimmed.to_string())) + } + } + + fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result { + let range = format!("{from}:{to}"); + let output = self.hg_util.execute( + &[ + "log", + "-r", + &range, + "--template", + "{rev}:{node|short} {desc|firstline}\\n", + ], + Some(target), + )?; + Ok(output.stdout) + } + + fn is_change_report(&self) -> bool { + true + } + + fn is_vcs_capable_downloader(&self) -> bool { + true + } + + fn is_dvcs_downloader(&self) -> bool { + false + } +} diff --git a/crates/mozart-core/src/downloader/max_file_size_exceeded_exception.rs b/crates/mozart-core/src/downloader/max_file_size_exceeded_exception.rs new file mode 100644 index 0000000..6a0f90c --- /dev/null +++ b/crates/mozart-core/src/downloader/max_file_size_exceeded_exception.rs @@ -0,0 +1 @@ +pub struct MaxFileSizeExceededException; diff --git a/crates/mozart-core/src/downloader/path_downloader.rs b/crates/mozart-core/src/downloader/path_downloader.rs new file mode 100644 index 0000000..e9cb62d --- /dev/null +++ b/crates/mozart-core/src/downloader/path_downloader.rs @@ -0,0 +1 @@ +pub struct PathDownloader; diff --git a/crates/mozart-core/src/downloader/perforce_downloader.rs b/crates/mozart-core/src/downloader/perforce_downloader.rs new file mode 100644 index 0000000..688e241 --- /dev/null +++ b/crates/mozart-core/src/downloader/perforce_downloader.rs @@ -0,0 +1 @@ +pub struct PerforceDownloader; diff --git a/crates/mozart-core/src/downloader/phar_downloader.rs b/crates/mozart-core/src/downloader/phar_downloader.rs new file mode 100644 index 0000000..74aff0f --- /dev/null +++ b/crates/mozart-core/src/downloader/phar_downloader.rs @@ -0,0 +1 @@ +pub struct PharDownloader; diff --git a/crates/mozart-core/src/downloader/rar_downloader.rs b/crates/mozart-core/src/downloader/rar_downloader.rs new file mode 100644 index 0000000..6fef0a0 --- /dev/null +++ b/crates/mozart-core/src/downloader/rar_downloader.rs @@ -0,0 +1 @@ +pub struct RarDownloader; diff --git a/crates/mozart-core/src/downloader/svn_downloader.rs b/crates/mozart-core/src/downloader/svn_downloader.rs new file mode 100644 index 0000000..c78f9b7 --- /dev/null +++ b/crates/mozart-core/src/downloader/svn_downloader.rs @@ -0,0 +1,85 @@ +use anyhow::Result; +use regex::Regex; +use std::path::Path; +use std::sync::LazyLock; + +use crate::downloader::VcsDownloader; +use crate::vcs::util::svn::SvnUtil; + +/// Match any non-`X` status line (mirror of Composer's +/// `{^ *[^X ] +}m`). Ignores externals (`X` prefix). +static SVN_STATUS_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?m)^ *[^X ] +").unwrap()); + +/// SVN downloader using checkout/switch. +pub struct SvnDownloader { + svn_util: SvnUtil, +} + +impl SvnDownloader { + pub fn new(svn_util: SvnUtil) -> Self { + Self { svn_util } + } +} + +impl VcsDownloader for SvnDownloader { + fn download(&self, _url: &str, _reference: &str, _target: &Path) -> Result<()> { + // SVN doesn't need a pre-download step + Ok(()) + } + + fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> { + let target_str = target.to_string_lossy().to_string(); + let svn_url = format!("{url}@{reference}"); + self.svn_util + .execute(&["checkout", &svn_url, &target_str], None)?; + Ok(()) + } + + fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> { + let svn_url = format!("{url}@{new_ref}"); + self.svn_util + .execute(&["switch", "--ignore-ancestry", &svn_url], Some(target))?; + Ok(()) + } + + fn remove(&self, target: &Path) -> Result<()> { + if target.exists() { + std::fs::remove_dir_all(target)?; + } + Ok(()) + } + + fn get_local_changes(&self, target: &Path) -> Result> { + if !target.join(".svn").is_dir() { + return Ok(None); + } + let output = self + .svn_util + .execute(&["status", "--ignore-externals"], Some(target))?; + if SVN_STATUS_RE.is_match(&output.stdout) { + Ok(Some(output.stdout)) + } else { + Ok(None) + } + } + + fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result { + let range = format!("{from}:{to}"); + let output = self + .svn_util + .execute(&["log", "-r", &range], Some(target))?; + Ok(output.stdout) + } + + fn is_change_report(&self) -> bool { + true + } + + fn is_vcs_capable_downloader(&self) -> bool { + true + } + + fn is_dvcs_downloader(&self) -> bool { + false + } +} diff --git a/crates/mozart-core/src/downloader/tar_downloader.rs b/crates/mozart-core/src/downloader/tar_downloader.rs new file mode 100644 index 0000000..82f68d9 --- /dev/null +++ b/crates/mozart-core/src/downloader/tar_downloader.rs @@ -0,0 +1 @@ +pub struct TarDownloader; diff --git a/crates/mozart-core/src/downloader/transport_exception.rs b/crates/mozart-core/src/downloader/transport_exception.rs new file mode 100644 index 0000000..e6ca721 --- /dev/null +++ b/crates/mozart-core/src/downloader/transport_exception.rs @@ -0,0 +1 @@ +pub struct TransportException; diff --git a/crates/mozart-core/src/downloader/vcs_capable_downloader_interface.rs b/crates/mozart-core/src/downloader/vcs_capable_downloader_interface.rs new file mode 100644 index 0000000..121b95d --- /dev/null +++ b/crates/mozart-core/src/downloader/vcs_capable_downloader_interface.rs @@ -0,0 +1 @@ +pub trait VcsCapableDownloaderInterface {} diff --git a/crates/mozart-core/src/downloader/vcs_downloader.rs b/crates/mozart-core/src/downloader/vcs_downloader.rs new file mode 100644 index 0000000..ce56d41 --- /dev/null +++ b/crates/mozart-core/src/downloader/vcs_downloader.rs @@ -0,0 +1,55 @@ +/// The VCS downloader interface. +/// +/// Corresponds to Composer's `VcsDownloader` hierarchy. +pub trait VcsDownloader { + /// Prepare for installation (e.g., sync mirror cache). + fn download(&self, url: &str, reference: &str, target: &std::path::Path) -> anyhow::Result<()>; + + /// Install (clone/checkout) the source to the target directory. + fn install(&self, url: &str, reference: &str, target: &std::path::Path) -> anyhow::Result<()>; + + /// Update the source at target to a new reference. + fn update( + &self, + url: &str, + old_ref: &str, + new_ref: &str, + target: &std::path::Path, + ) -> anyhow::Result<()>; + + /// Remove the source from the target directory. + fn remove(&self, target: &std::path::Path) -> anyhow::Result<()>; + + /// Detect local changes in the working copy. + /// Returns `None` if clean, `Some(diff)` if modified. + /// Mirrors `Composer\Downloader\ChangeReportInterface::getLocalChanges`. + fn get_local_changes(&self, target: &std::path::Path) -> anyhow::Result>; + + /// Detect commits present locally but not on the tracking remote. + /// Returns `None` if there are no unpushed commits or the concept does + /// not apply (only `GitDownloader` implements this in Composer's + /// `DvcsDownloaderInterface`). + fn unpushed_changes(&self, _target: &std::path::Path) -> anyhow::Result> { + Ok(None) + } + + /// Resolve the working copy's current VCS reference (e.g. commit hash). + /// Returns `None` if no reference can be determined. Mirrors + /// `Composer\Downloader\VcsCapableDownloaderInterface::getVcsReference`. + fn vcs_reference(&self, _target: &std::path::Path) -> anyhow::Result> { + Ok(None) + } + + /// Get commit log between two references. + fn commit_logs(&self, from: &str, to: &str, target: &std::path::Path) + -> anyhow::Result; + + /// instanceof ChangeReportInterface + fn is_change_report(&self) -> bool; + + /// instanceof VcsCapableDownloaderInterface + fn is_vcs_capable_downloader(&self) -> bool; + + /// instanceof DvcsDownloaderInterface + fn is_dvcs_downloader(&self) -> bool; +} diff --git a/crates/mozart-core/src/downloader/xz_downloader.rs b/crates/mozart-core/src/downloader/xz_downloader.rs new file mode 100644 index 0000000..18de2ce --- /dev/null +++ b/crates/mozart-core/src/downloader/xz_downloader.rs @@ -0,0 +1 @@ +pub struct XzDownloader; diff --git a/crates/mozart-core/src/downloader/zip_downloader.rs b/crates/mozart-core/src/downloader/zip_downloader.rs new file mode 100644 index 0000000..401047a --- /dev/null +++ b/crates/mozart-core/src/downloader/zip_downloader.rs @@ -0,0 +1 @@ +pub struct ZipDownloader; diff --git a/crates/mozart-core/src/lib.rs b/crates/mozart-core/src/lib.rs index 72f5ae1..adf24a3 100644 --- a/crates/mozart-core/src/lib.rs +++ b/crates/mozart-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod config_source; pub mod config_validator; pub mod console; pub mod dependency_resolver; +pub mod downloader; pub mod exit_code; pub mod factory; pub mod http; diff --git a/crates/mozart-core/src/repository.rs b/crates/mozart-core/src/repository.rs index ce6de85..b7527dd 100644 --- a/crates/mozart-core/src/repository.rs +++ b/crates/mozart-core/src/repository.rs @@ -2,7 +2,6 @@ pub mod advisory; pub mod browse_repos; pub mod cache; pub mod composer_repo; -pub mod download_manager; pub mod downloader; pub mod inline_package; pub mod installed; diff --git a/crates/mozart-core/src/repository/download_manager.rs b/crates/mozart-core/src/repository/download_manager.rs deleted file mode 100644 index d422899..0000000 --- a/crates/mozart-core/src/repository/download_manager.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! `DownloadManager` — pick the right [`VcsDownloader`] for a given -//! [`LocalPackage`]. Mirrors `Composer\Downloader\DownloadManager`. - -use std::path::PathBuf; - -use crate::composer::{InstallationSource, LocalPackage}; -use crate::vcs::downloader::VcsDownloader; -use crate::vcs::downloader::git::GitDownloader; -use crate::vcs::downloader::hg::HgDownloader; -use crate::vcs::downloader::svn::SvnDownloader; -use crate::vcs::process::ProcessExecutor; -use crate::vcs::util::git::GitUtil; -use crate::vcs::util::hg::HgUtil; -use crate::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. - /// `/.cache/git`). - pub fn new(git_cache_dir: PathBuf) -> Self { - Self { git_cache_dir } - } - - pub fn get_downloader_for_package( - &self, - package: &LocalPackage, - ) -> Option> { - 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 crate::composer::PackageReference; - use serde_json::Value; - - fn pkg( - installation_source: Option, - 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.get_downloader_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.get_downloader_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.get_downloader_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.get_downloader_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.get_downloader_for_package(&p).is_none()); - } -} diff --git a/crates/mozart-core/src/repository/installer_executor/filesystem.rs b/crates/mozart-core/src/repository/installer_executor/filesystem.rs index 3f982e3..2b34e02 100644 --- a/crates/mozart-core/src/repository/installer_executor/filesystem.rs +++ b/crates/mozart-core/src/repository/installer_executor/filesystem.rs @@ -5,9 +5,10 @@ //! [`crate::vcs`], and removes vendor directories. Test code substitutes a //! recording-only executor instead (added in a later step). -use super::super::cache::Cache; -use super::super::downloader; -use super::{ExecuteContext, InstallerExecutor, PackageOperation}; +use crate::downloader::{GitDownloader, HgDownloader, SvnDownloader}; +use crate::repository::cache::Cache; +use crate::repository::downloader; +use crate::repository::installer_executor::{ExecuteContext, InstallerExecutor, PackageOperation}; use std::path::Path; pub struct FilesystemExecutor { @@ -135,6 +136,8 @@ fn install_from_source( vendor_dir: &Path, package_name: &str, ) -> anyhow::Result<()> { + use crate::downloader::VcsDownloader as _; + let target = vendor_dir.join(package_name); if target.exists() { std::fs::remove_dir_all(&target)?; @@ -145,23 +148,20 @@ fn install_from_source( let process = crate::vcs::process::ProcessExecutor::new(); let git_util = crate::vcs::util::git::GitUtil::new(process, vendor_dir.join(".cache").join("git")); - let downloader = crate::vcs::downloader::git::GitDownloader::new(git_util); - use crate::vcs::downloader::VcsDownloader as _; + let downloader = GitDownloader::new(git_util); downloader.download(url, reference, &target)?; downloader.install(url, reference, &target)?; } "svn" => { let process = crate::vcs::process::ProcessExecutor::new(); let svn_util = crate::vcs::util::svn::SvnUtil::new(process); - let downloader = crate::vcs::downloader::svn::SvnDownloader::new(svn_util); - use crate::vcs::downloader::VcsDownloader as _; + let downloader = SvnDownloader::new(svn_util); downloader.install(url, reference, &target)?; } "hg" => { let process = crate::vcs::process::ProcessExecutor::new(); let hg_util = crate::vcs::util::hg::HgUtil::new(process); - let downloader = crate::vcs::downloader::hg::HgDownloader::new(hg_util); - use crate::vcs::downloader::VcsDownloader as _; + let downloader = HgDownloader::new(hg_util); downloader.install(url, reference, &target)?; } _ => { diff --git a/crates/mozart-core/src/vcs.rs b/crates/mozart-core/src/vcs.rs index 137cc5b..534dd0f 100644 --- a/crates/mozart-core/src/vcs.rs +++ b/crates/mozart-core/src/vcs.rs @@ -1,4 +1,3 @@ -pub mod downloader; pub mod process; pub mod repository; pub mod util; diff --git a/crates/mozart-core/src/vcs/downloader/git.rs b/crates/mozart-core/src/vcs/downloader/git.rs deleted file mode 100644 index eb7a649..0000000 --- a/crates/mozart-core/src/vcs/downloader/git.rs +++ /dev/null @@ -1,271 +0,0 @@ -use super::super::process::ProcessExecutor; -use super::super::util::git::GitUtil; -use super::VcsDownloader; -use anyhow::Result; -use regex::Regex; -use std::path::Path; -use std::sync::LazyLock; - -/// Match ` HEAD` lines in `git show-ref --head -d` output. -static HEAD_REF_RE: LazyLock = - LazyLock::new(|| Regex::new(r"(?im)^([a-f0-9]+) HEAD$").unwrap()); - -/// Git downloader using clone/checkout with optional mirror cache. -/// -/// Corresponds to Composer's `Downloader\GitDownloader`. -pub struct GitDownloader { - git_util: GitUtil, -} - -impl GitDownloader { - pub fn new(git_util: GitUtil) -> Self { - Self { git_util } - } -} - -impl VcsDownloader for GitDownloader { - fn download(&self, url: &str, _reference: &str, _target: &Path) -> Result<()> { - // Pre-sync the mirror so install can use --reference - self.git_util.sync_mirror(url)?; - Ok(()) - } - - fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> { - let target_str = target.to_string_lossy(); - let mirror_path = self.git_util.mirror_path(url); - - if mirror_path.join("HEAD").exists() { - // Clone with mirror reference for efficiency - let mirror_str = mirror_path.to_string_lossy().to_string(); - self.git_util.run_command( - &[ - "git", - "clone", - "--no-checkout", - "--dissociate", - "--reference", - &mirror_str, - "--", - url, - &target_str, - ], - url, - None, - )?; - } else { - self.git_util.run_command( - &["git", "clone", "--no-checkout", "--", url, &target_str], - url, - None, - )?; - } - - // Checkout the specific reference - let process = ProcessExecutor::new(); - process.execute_checked(&["git", "checkout", reference, "--force"], Some(target))?; - - Ok(()) - } - - fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> { - let process = ProcessExecutor::new(); - - // Update remote URL - process.execute_checked( - &["git", "remote", "set-url", "origin", "--", url], - Some(target), - )?; - - // Fetch latest - self.git_util - .run_command(&["git", "fetch", "origin"], url, Some(target))?; - - // Checkout new reference - process.execute_checked(&["git", "checkout", new_ref, "--force"], Some(target))?; - - Ok(()) - } - - fn remove(&self, target: &Path) -> Result<()> { - if target.exists() { - std::fs::remove_dir_all(target)?; - } - Ok(()) - } - - fn get_local_changes(&self, target: &Path) -> Result> { - if !target.join(".git").exists() { - return Ok(None); - } - let process = ProcessExecutor::new(); - let output = process.execute( - &["git", "status", "--porcelain", "--untracked-files=no"], - Some(target), - )?; - let trimmed = output.stdout.trim(); - if trimmed.is_empty() { - Ok(None) - } else { - Ok(Some(trimmed.to_string())) - } - } - - fn vcs_reference(&self, target: &Path) -> Result> { - if !target.join(".git").exists() { - return Ok(None); - } - let process = ProcessExecutor::new(); - let output = process.execute(&["git", "rev-parse", "HEAD"], Some(target))?; - if output.status != 0 { - return Ok(None); - } - let trimmed = output.stdout.trim(); - if trimmed.is_empty() { - Ok(None) - } else { - Ok(Some(trimmed.to_string())) - } - } - - fn unpushed_changes(&self, target: &Path) -> Result> { - if !target.join(".git").exists() { - return Ok(None); - } - let process = ProcessExecutor::new(); - - let mut refs = match collect_show_ref(&process, target)? { - Some(r) => r, - None => return Ok(None), - }; - - let head_ref = match HEAD_REF_RE - .captures(&refs) - .and_then(|c| c.get(1)) - .map(|m| m.as_str().to_string()) - { - Some(h) => h, - None => return Ok(None), - }; - - let candidate_branches = collect_local_branches(&refs, &head_ref); - if candidate_branches.is_empty() { - // not on a branch (detached / tag) — skip - return Ok(None); - } - - let mut branch = candidate_branches[0].clone(); - let mut unpushed_changes: Option = None; - let mut branch_not_found_error = false; - - for i in 0..=1 { - let mut remote_branches: Vec = Vec::new(); - - for candidate in &candidate_branches { - let matches = collect_remote_branches(&refs, candidate); - if !matches.is_empty() { - branch = candidate.clone(); - remote_branches = matches; - break; - } - } - - if remote_branches.is_empty() { - unpushed_changes = Some(format!( - "Branch {branch} could not be found on any remote and appears to be unpushed" - )); - branch_not_found_error = true; - } else { - if branch_not_found_error { - unpushed_changes = None; - } - for remote_branch in &remote_branches { - let range = format!("{remote_branch}...{branch}"); - let output = process.execute_checked( - &["git", "diff", "--name-status", &range, "--"], - Some(target), - )?; - let trimmed = output.stdout.trim().to_string(); - match unpushed_changes { - None => unpushed_changes = Some(trimmed), - Some(ref existing) if trimmed.len() < existing.len() => { - unpushed_changes = Some(trimmed); - } - _ => {} - } - } - } - - if unpushed_changes.as_deref().is_some_and(|s| !s.is_empty()) && i == 0 { - let _ = process.execute(&["git", "fetch", "--all"], Some(target))?; - refs = match collect_show_ref(&process, target)? { - Some(r) => r, - None => return Ok(unpushed_changes), - }; - } - - if unpushed_changes.as_deref().is_none_or(str::is_empty) { - break; - } - } - - Ok(unpushed_changes.filter(|s| !s.is_empty())) - } - - fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result { - let process = ProcessExecutor::new(); - let range = format!("{from}..{to}"); - let output = process.execute( - &["git", "log", &range, "--oneline", "--no-decorate"], - Some(target), - )?; - Ok(output.stdout) - } - - fn is_change_report(&self) -> bool { - true - } - - fn is_vcs_capable_downloader(&self) -> bool { - true - } - - fn is_dvcs_downloader(&self) -> bool { - true - } -} - -fn collect_show_ref(process: &ProcessExecutor, target: &Path) -> Result> { - let output = process.execute(&["git", "show-ref", "--head", "-d"], Some(target))?; - if output.status != 0 { - anyhow::bail!( - "Failed to execute git show-ref --head -d\n\n{}", - output.stderr.trim() - ); - } - Ok(Some(output.stdout.trim().to_string())) -} - -fn collect_local_branches(refs: &str, head_ref: &str) -> Vec { - let pattern = format!(r"(?im)^{} refs/heads/(.+)$", regex::escape(head_ref)); - let re = match Regex::new(&pattern) { - Ok(r) => r, - Err(_) => return Vec::new(), - }; - re.captures_iter(refs) - .filter_map(|c| c.get(1).map(|m| m.as_str().to_string())) - .collect() -} - -fn collect_remote_branches(refs: &str, candidate: &str) -> Vec { - let pattern = format!( - r"(?im)^[a-f0-9]+ refs/remotes/((?:[^/]+)/{})$", - regex::escape(candidate) - ); - let re = match Regex::new(&pattern) { - Ok(r) => r, - Err(_) => return Vec::new(), - }; - re.captures_iter(refs) - .filter_map(|c| c.get(1).map(|m| m.as_str().to_string())) - .collect() -} diff --git a/crates/mozart-core/src/vcs/downloader/hg.rs b/crates/mozart-core/src/vcs/downloader/hg.rs deleted file mode 100644 index 33650f8..0000000 --- a/crates/mozart-core/src/vcs/downloader/hg.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::super::util::hg::HgUtil; -use super::VcsDownloader; -use anyhow::Result; -use std::path::Path; - -/// Mercurial downloader using clone/pull/update. -pub struct HgDownloader { - hg_util: HgUtil, -} - -impl HgDownloader { - pub fn new(hg_util: HgUtil) -> Self { - Self { hg_util } - } -} - -impl VcsDownloader for HgDownloader { - fn download(&self, _url: &str, _reference: &str, _target: &Path) -> Result<()> { - Ok(()) - } - - fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> { - let target_str = target.to_string_lossy().to_string(); - self.hg_util - .execute(&["clone", "--", url, &target_str], None)?; - self.hg_util - .execute(&["update", "-r", reference], Some(target))?; - Ok(()) - } - - fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> { - self.hg_util.execute(&["pull", url], Some(target))?; - self.hg_util - .execute(&["update", "-r", new_ref], Some(target))?; - Ok(()) - } - - fn remove(&self, target: &Path) -> Result<()> { - if target.exists() { - std::fs::remove_dir_all(target)?; - } - Ok(()) - } - - fn get_local_changes(&self, target: &Path) -> Result> { - if !target.join(".hg").is_dir() { - return Ok(None); - } - let output = self.hg_util.execute(&["st"], Some(target))?; - let trimmed = output.stdout.trim(); - if trimmed.is_empty() { - Ok(None) - } else { - Ok(Some(trimmed.to_string())) - } - } - - fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result { - let range = format!("{from}:{to}"); - let output = self.hg_util.execute( - &[ - "log", - "-r", - &range, - "--template", - "{rev}:{node|short} {desc|firstline}\\n", - ], - Some(target), - )?; - Ok(output.stdout) - } - - fn is_change_report(&self) -> bool { - true - } - - fn is_vcs_capable_downloader(&self) -> bool { - true - } - - fn is_dvcs_downloader(&self) -> bool { - false - } -} diff --git a/crates/mozart-core/src/vcs/downloader/mod.rs b/crates/mozart-core/src/vcs/downloader/mod.rs deleted file mode 100644 index 352f330..0000000 --- a/crates/mozart-core/src/vcs/downloader/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -pub mod git; -pub mod hg; -pub mod svn; - -use std::path::Path; - -use anyhow::Result; - -/// The VCS downloader interface. -/// -/// Corresponds to Composer's `VcsDownloader` hierarchy. -pub trait VcsDownloader { - /// Prepare for installation (e.g., sync mirror cache). - fn download(&self, url: &str, reference: &str, target: &Path) -> Result<()>; - - /// Install (clone/checkout) the source to the target directory. - fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()>; - - /// Update the source at target to a new reference. - fn update(&self, url: &str, old_ref: &str, new_ref: &str, target: &Path) -> Result<()>; - - /// Remove the source from the target directory. - fn remove(&self, target: &Path) -> Result<()>; - - /// Detect local changes in the working copy. - /// Returns `None` if clean, `Some(diff)` if modified. - /// Mirrors `Composer\Downloader\ChangeReportInterface::getLocalChanges`. - fn get_local_changes(&self, target: &Path) -> Result>; - - /// Detect commits present locally but not on the tracking remote. - /// Returns `None` if there are no unpushed commits or the concept does - /// not apply (only `GitDownloader` implements this in Composer's - /// `DvcsDownloaderInterface`). - fn unpushed_changes(&self, _target: &Path) -> Result> { - Ok(None) - } - - /// Resolve the working copy's current VCS reference (e.g. commit hash). - /// Returns `None` if no reference can be determined. Mirrors - /// `Composer\Downloader\VcsCapableDownloaderInterface::getVcsReference`. - fn vcs_reference(&self, _target: &Path) -> Result> { - Ok(None) - } - - /// Get commit log between two references. - fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result; - - /// instanceof ChangeReportInterface - fn is_change_report(&self) -> bool; - - /// instanceof VcsCapableDownloaderInterface - fn is_vcs_capable_downloader(&self) -> bool; - - /// instanceof DvcsDownloaderInterface - fn is_dvcs_downloader(&self) -> bool; -} diff --git a/crates/mozart-core/src/vcs/downloader/svn.rs b/crates/mozart-core/src/vcs/downloader/svn.rs deleted file mode 100644 index ea885ed..0000000 --- a/crates/mozart-core/src/vcs/downloader/svn.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::super::util::svn::SvnUtil; -use super::VcsDownloader; -use anyhow::Result; -use regex::Regex; -use std::path::Path; -use std::sync::LazyLock; - -/// Match any non-`X` status line (mirror of Composer's -/// `{^ *[^X ] +}m`). Ignores externals (`X` prefix). -static SVN_STATUS_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?m)^ *[^X ] +").unwrap()); - -/// SVN downloader using checkout/switch. -pub struct SvnDownloader { - svn_util: SvnUtil, -} - -impl SvnDownloader { - pub fn new(svn_util: SvnUtil) -> Self { - Self { svn_util } - } -} - -impl VcsDownloader for SvnDownloader { - fn download(&self, _url: &str, _reference: &str, _target: &Path) -> Result<()> { - // SVN doesn't need a pre-download step - Ok(()) - } - - fn install(&self, url: &str, reference: &str, target: &Path) -> Result<()> { - let target_str = target.to_string_lossy().to_string(); - let svn_url = format!("{url}@{reference}"); - self.svn_util - .execute(&["checkout", &svn_url, &target_str], None)?; - Ok(()) - } - - fn update(&self, url: &str, _old_ref: &str, new_ref: &str, target: &Path) -> Result<()> { - let svn_url = format!("{url}@{new_ref}"); - self.svn_util - .execute(&["switch", "--ignore-ancestry", &svn_url], Some(target))?; - Ok(()) - } - - fn remove(&self, target: &Path) -> Result<()> { - if target.exists() { - std::fs::remove_dir_all(target)?; - } - Ok(()) - } - - fn get_local_changes(&self, target: &Path) -> Result> { - if !target.join(".svn").is_dir() { - return Ok(None); - } - let output = self - .svn_util - .execute(&["status", "--ignore-externals"], Some(target))?; - if SVN_STATUS_RE.is_match(&output.stdout) { - Ok(Some(output.stdout)) - } else { - Ok(None) - } - } - - fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result { - let range = format!("{from}:{to}"); - let output = self - .svn_util - .execute(&["log", "-r", &range], Some(target))?; - Ok(output.stdout) - } - - fn is_change_report(&self) -> bool { - true - } - - fn is_vcs_capable_downloader(&self) -> bool { - true - } - - fn is_dvcs_downloader(&self) -> bool { - false - } -} diff --git a/crates/mozart-core/tests/git_driver_test.rs b/crates/mozart-core/tests/git_driver_test.rs index 0c1c344..2b2db57 100644 --- a/crates/mozart-core/tests/git_driver_test.rs +++ b/crates/mozart-core/tests/git_driver_test.rs @@ -1,6 +1,5 @@ +use mozart_core::downloader::{GitDownloader, VcsDownloader}; use mozart_core::repository::vcs::{DriverConfig, DriverType, create_driver, detect_driver}; -use mozart_core::vcs::downloader::VcsDownloader; -use mozart_core::vcs::downloader::git::GitDownloader; use mozart_core::vcs::process::ProcessExecutor; use mozart_core::vcs::repository::VcsRepository; use mozart_core::vcs::util::git::GitUtil; diff --git a/crates/mozart/src/composer.rs b/crates/mozart/src/composer.rs index 0484344..9fd8b77 100644 --- a/crates/mozart/src/composer.rs +++ b/crates/mozart/src/composer.rs @@ -16,8 +16,8 @@ use std::path::{Path, PathBuf}; use crate::factory::create_composer; use mozart_core::composer::{AutoloadGenerator, InstallationManager, Locker, RepositoryManager}; use mozart_core::config::Config; +use mozart_core::downloader::DownloadManager; use mozart_core::package::RootPackageData; -use mozart_core::repository::download_manager::DownloadManager; /// Project-level Composer state. Mirrors `Composer\PartialComposer` / /// `Composer\Composer` in PHP, exposing the subset of getters command diff --git a/crates/mozart/src/factory.rs b/crates/mozart/src/factory.rs index 29faa42..b88e088 100644 --- a/crates/mozart/src/factory.rs +++ b/crates/mozart/src/factory.rs @@ -15,9 +15,9 @@ use mozart_core::composer::{ Locker, PackageReference, RepositoryManager, }; use mozart_core::config::resolve_references; +use mozart_core::downloader::DownloadManager; use mozart_core::factory::create_config; use mozart_core::package::{RootPackageData, read_from_file}; -use mozart_core::repository::download_manager::DownloadManager; /// Rust port of `Factory::createComposer()`. /// -- cgit v1.3.1