diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-09 19:59:58 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-09 19:59:58 +0900 |
| commit | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (patch) | |
| tree | ddccef3355d76f759b3cf43af0fcc3c8b79eaa6d | |
| parent | f9671f2dcde92d5c037595d0d3f01396a8190970 (diff) | |
| download | php-mozart-72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4.tar.gz php-mozart-72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4.tar.zst php-mozart-72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4.zip | |
refactor(vcs): mirror Composer interfaces; rename get_local_changes
- Rename `local_changes` → `get_local_changes` to match Composer's
`getLocalChanges`
- Add `is_change_report`, `is_vcs_capable_downloader`,
`is_dvcs_downloader` trait methods to replace PHP `instanceof` checks
- Add `VersionParser` stub to keep `VersionGuesser::new` signature
compatible with Composer's constructor
- Add `ArrayDumper` in status.rs mirroring
`Composer\Package\Dumper\ArrayDumper`; expand `build_package_config`
to include all fields that `VersionGuesser` inspects
| -rw-r--r-- | crates/mozart-vcs/src/downloader/git.rs | 14 | ||||
| -rw-r--r-- | crates/mozart-vcs/src/downloader/hg.rs | 14 | ||||
| -rw-r--r-- | crates/mozart-vcs/src/downloader/mod.rs | 11 | ||||
| -rw-r--r-- | crates/mozart-vcs/src/downloader/svn.rs | 14 | ||||
| -rw-r--r-- | crates/mozart-vcs/src/version_guesser.rs | 26 | ||||
| -rw-r--r-- | crates/mozart-vcs/tests/git_driver_test.rs | 6 | ||||
| -rw-r--r-- | crates/mozart/src/commands.rs | 4 | ||||
| -rw-r--r-- | crates/mozart/src/commands/status.rs | 145 |
8 files changed, 175 insertions, 59 deletions
diff --git a/crates/mozart-vcs/src/downloader/git.rs b/crates/mozart-vcs/src/downloader/git.rs index 0c78f89..814d67e 100644 --- a/crates/mozart-vcs/src/downloader/git.rs +++ b/crates/mozart-vcs/src/downloader/git.rs @@ -96,7 +96,7 @@ impl VcsDownloader for GitDownloader { Ok(()) } - fn local_changes(&self, target: &Path) -> Result<Option<String>> { + fn get_local_changes(&self, target: &Path) -> Result<Option<String>> { if !target.join(".git").exists() { return Ok(None); } @@ -223,6 +223,18 @@ impl VcsDownloader for GitDownloader { )?; 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<Option<String>> { diff --git a/crates/mozart-vcs/src/downloader/hg.rs b/crates/mozart-vcs/src/downloader/hg.rs index 926cfa8..3230404 100644 --- a/crates/mozart-vcs/src/downloader/hg.rs +++ b/crates/mozart-vcs/src/downloader/hg.rs @@ -45,7 +45,7 @@ impl VcsDownloader for HgDownloader { Ok(()) } - fn local_changes(&self, target: &Path) -> Result<Option<String>> { + fn get_local_changes(&self, target: &Path) -> Result<Option<String>> { if !target.join(".hg").is_dir() { return Ok(None); } @@ -72,4 +72,16 @@ impl VcsDownloader for HgDownloader { )?; 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-vcs/src/downloader/mod.rs b/crates/mozart-vcs/src/downloader/mod.rs index 8948921..352f330 100644 --- a/crates/mozart-vcs/src/downloader/mod.rs +++ b/crates/mozart-vcs/src/downloader/mod.rs @@ -25,7 +25,7 @@ pub trait VcsDownloader { /// Detect local changes in the working copy. /// Returns `None` if clean, `Some(diff)` if modified. /// Mirrors `Composer\Downloader\ChangeReportInterface::getLocalChanges`. - fn local_changes(&self, target: &Path) -> Result<Option<String>>; + fn get_local_changes(&self, target: &Path) -> Result<Option<String>>; /// Detect commits present locally but not on the tracking remote. /// Returns `None` if there are no unpushed commits or the concept does @@ -44,4 +44,13 @@ pub trait VcsDownloader { /// Get commit log between two references. fn commit_logs(&self, from: &str, to: &str, target: &Path) -> Result<String>; + + /// 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-vcs/src/downloader/svn.rs b/crates/mozart-vcs/src/downloader/svn.rs index 533e15a..87b59da 100644 --- a/crates/mozart-vcs/src/downloader/svn.rs +++ b/crates/mozart-vcs/src/downloader/svn.rs @@ -51,7 +51,7 @@ impl VcsDownloader for SvnDownloader { Ok(()) } - fn local_changes(&self, target: &Path) -> Result<Option<String>> { + fn get_local_changes(&self, target: &Path) -> Result<Option<String>> { if !target.join(".svn").is_dir() { return Ok(None); } @@ -72,4 +72,16 @@ impl VcsDownloader for SvnDownloader { .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-vcs/src/version_guesser.rs b/crates/mozart-vcs/src/version_guesser.rs index e70eb4e..038e332 100644 --- a/crates/mozart-vcs/src/version_guesser.rs +++ b/crates/mozart-vcs/src/version_guesser.rs @@ -20,6 +20,25 @@ use crate::process::ProcessExecutor; const DEFAULT_BRANCH_ALIAS: &str = "9999999-dev"; +/// Mirrors `Composer\Package\Version\VersionParser` (itself a thin wrapper +/// around `Composer\Semver\VersionParser`). In Rust, semver parsing is +/// handled by `mozart_semver` directly, so this type carries no state; +/// it exists to keep `VersionGuesser::new` signature compatible with the +/// PHP constructor. +pub struct VersionParser; + +impl Default for VersionParser { + fn default() -> Self { + Self::new() + } +} + +impl VersionParser { + pub fn new() -> Self { + Self + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct GuessedVersion { pub version: String, @@ -35,12 +54,15 @@ pub struct VersionGuesser { impl Default for VersionGuesser { fn default() -> Self { - Self::new() + Self::new(VersionParser::new()) } } impl VersionGuesser { - pub fn new() -> Self { + /// Mirrors `Composer\Package\Version\VersionGuesser::__construct`. + /// `_version_parser` is accepted for API parity but unused — Rust relies + /// on `mozart_semver` directly. + pub fn new(_version_parser: VersionParser) -> Self { Self { process: ProcessExecutor::new(), } diff --git a/crates/mozart-vcs/tests/git_driver_test.rs b/crates/mozart-vcs/tests/git_driver_test.rs index 2654665..dd72ad6 100644 --- a/crates/mozart-vcs/tests/git_driver_test.rs +++ b/crates/mozart-vcs/tests/git_driver_test.rs @@ -165,13 +165,13 @@ fn test_git_downloader() { assert!(target.join("composer.json").exists()); // Check no local changes - let changes = downloader.local_changes(&target).unwrap(); + let changes = downloader.get_local_changes(&target).unwrap(); assert!(changes.is_none(), "Expected no changes, got: {:?}", changes); // Untracked files alone must NOT count as local changes (matches // Composer's `git status --porcelain --untracked-files=no`). std::fs::write(target.join("untracked.txt"), "untracked").unwrap(); - let changes = downloader.local_changes(&target).unwrap(); + let changes = downloader.get_local_changes(&target).unwrap(); assert!( changes.is_none(), "Untracked files should be ignored, got: {:?}", @@ -180,7 +180,7 @@ fn test_git_downloader() { // Modifying a tracked file is a local change. std::fs::write(target.join("composer.json"), "{\"name\":\"changed\"}\n").unwrap(); - let changes = downloader.local_changes(&target).unwrap(); + let changes = downloader.get_local_changes(&target).unwrap(); assert!(changes.is_some()); assert!(changes.unwrap().contains("composer.json")); diff --git a/crates/mozart/src/commands.rs b/crates/mozart/src/commands.rs index f9b8880..d0139d5 100644 --- a/crates/mozart/src/commands.rs +++ b/crates/mozart/src/commands.rs @@ -96,6 +96,10 @@ impl Cli { .map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e)), } } + + fn is_verbose(&self) -> bool { + self.verbose > 0 + } } #[derive(clap::Subcommand)] diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs index 60185cb..d15ffb3 100644 --- a/crates/mozart/src/commands/status.rs +++ b/crates/mozart/src/commands/status.rs @@ -1,12 +1,11 @@ use crate::composer::Composer; use clap::Args; -use indexmap::IndexMap; use mozart_core::composer::{InstallationSource, LocalPackage}; use mozart_core::console::Console; use mozart_core::console_writeln; use mozart_core::console_writeln_error; use mozart_core::exit_code; -use mozart_vcs::version_guesser::VersionGuesser; +use mozart_vcs::version_guesser::{VersionGuesser, VersionParser}; #[derive(Args)] pub struct StatusArgs {} @@ -33,11 +32,13 @@ pub async fn execute( let dm = composer.download_manager(); let im = composer.installation_manager(); - let mut errors = IndexMap::new(); - let mut unpushed_changes = IndexMap::new(); - let mut vcs_version_changes = IndexMap::new(); + let mut errors = Vec::new(); + let mut unpushed_changes = Vec::new(); + let mut vcs_version_changes = Vec::new(); - let guesser = VersionGuesser::new(); + let parser = VersionParser::new(); + let guesser = VersionGuesser::new(parser); + let dumper = ArrayDumper::new(); for package in installed_repo.get_canonical_packages() { let Some(downloader) = dm.get_downloader_for_package(package) else { @@ -46,57 +47,60 @@ pub async fn execute( let Some(target_dir) = im.get_install_path(package) else { continue; }; - let target_key = target_dir.display().to_string(); + let target_dir_key = target_dir.display().to_string(); - // ChangeReportInterface — Composer mirrors the symlink branch and - // the local-changes branch unconditionally; the latter overrides - // the former when both fire. - if std::fs::symlink_metadata(&target_dir) - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false) - { - errors.insert( - target_key.clone(), - format!("{target_key} is a symbolic link."), - ); - } - if let Some(changes) = downloader.local_changes(&target_dir)? { - errors.insert(target_key.clone(), changes); + if downloader.is_change_report() { + if std::fs::symlink_metadata(&target_dir) + .map(|m| m.file_type().is_symlink()) + .unwrap_or(false) + { + errors.push(( + target_dir_key.clone(), + format!("{target_dir_key} is a symbolic link."), + )); + } + if let Some(changes) = downloader.get_local_changes(&target_dir)? { + errors.push((target_dir_key.clone(), changes)); + } } - // VcsCapableDownloaderInterface - if downloader.vcs_reference(&target_dir)?.is_some() { + if downloader.is_vcs_capable_downloader() + && downloader.vcs_reference(&target_dir)?.is_some() + { let previous_ref = match package.installation_source() { Some(InstallationSource::Source) => package.source_reference(), Some(InstallationSource::Dist) => package.dist_reference(), _ => None, }; - let pkg_config = build_package_config(package); - let current_version = guesser.guess_version(&pkg_config, &target_dir); + + let current_version = guesser.guess_version(&dumper.dump(package), &target_dir); + if let (Some(previous_ref), Some(current_version)) = (previous_ref, current_version) { - let cur_commit = current_version.commit.as_deref().unwrap_or(""); - let cur_pretty = current_version.pretty_version.as_deref().unwrap_or(""); - if cur_commit != previous_ref && cur_pretty != previous_ref { - vcs_version_changes.insert( - target_key.clone(), + let current_commit = current_version.commit.as_deref().unwrap_or(""); + let current_pretty_version = + current_version.pretty_version.as_deref().unwrap_or(""); + if current_commit != previous_ref && current_pretty_version != previous_ref { + vcs_version_changes.push(( + target_dir_key.clone(), VcsVerChange { previous: VerRef { version: package.pretty_version().to_string(), reference: previous_ref.to_string(), }, current: VerRef { - version: cur_pretty.to_string(), - reference: cur_commit.to_string(), + version: current_pretty_version.to_string(), + reference: current_commit.to_string(), }, }, - ); + )); } } } - // DvcsDownloaderInterface - if let Some(unpushed) = downloader.unpushed_changes(&target_dir)? { - unpushed_changes.insert(target_key.clone(), unpushed); + if downloader.is_dvcs_downloader() + && let Some(unpushed) = downloader.unpushed_changes(&target_dir)? + { + unpushed_changes.push((target_dir_key.clone(), unpushed)); } } @@ -105,9 +109,6 @@ pub async fn execute( return Ok(()); } - let verbose = cli.verbose > 0; - let very_verbose = cli.verbose >= 2; - if !errors.is_empty() { console_writeln_error!( console, @@ -115,7 +116,7 @@ pub async fn execute( ); for (path, changes) in &errors { - if verbose { + if cli.is_verbose() { console_writeln!(console, "<info>{path}</info>:"); console_writeln!(console, "{}", &indent_block(changes)); } else { @@ -131,7 +132,7 @@ pub async fn execute( ); for (path, changes) in &unpushed_changes { - if verbose { + if cli.is_verbose() { console_writeln!(console, "<info>{path}</info>:"); console_writeln!(console, "{}", &indent_block(changes)); } else { @@ -147,7 +148,7 @@ pub async fn execute( ); for (path, change) in &vcs_version_changes { - if verbose { + if cli.is_verbose() { let mut prev = if change.previous.version.is_empty() { change.previous.reference.clone() } else { @@ -158,7 +159,7 @@ pub async fn execute( } else { change.current.version.clone() }; - if very_verbose { + if console.is_very_verbose() { prev.push_str(&format!(" ({})", change.previous.reference)); curr.push_str(&format!(" ({})", change.current.reference)); } @@ -173,7 +174,7 @@ pub async fn execute( } } - if !verbose { + if !cli.is_verbose() { console_writeln_error!(console, "Use --verbose (-v) to see a list of files"); } @@ -197,11 +198,55 @@ fn indent_block(s: &str) -> String { .join("\n") } -/// Build the `package_config` shape that `VersionGuesser` reads. The PHP -/// equivalent is `ArrayDumper::dump($package)`; we only need the fields -/// that `VersionGuesser` actually inspects. +/// Mirrors `Composer\Package\Dumper\ArrayDumper`. Serialises a `LocalPackage` +/// into the JSON shape that `VersionGuesser::guess_version` expects. +struct ArrayDumper; + +impl ArrayDumper { + fn new() -> Self { + Self + } + + fn dump(&self, package: &LocalPackage) -> serde_json::Value { + build_package_config(package) + } +} + +/// Serialises a `LocalPackage` to the JSON shape consumed by +/// `VersionGuesser::guess_version`. Mirrors `ArrayDumper::dump($package)` — +/// we include all fields that `VersionGuesser` inspects. fn build_package_config(package: &LocalPackage) -> serde_json::Value { - serde_json::json!({ - "extra": package.extra(), - }) + let mut obj = serde_json::Map::new(); + obj.insert("name".into(), package.pretty_name().into()); + obj.insert("version".into(), package.pretty_version().into()); + if let Some(t) = package.package_type() { + obj.insert("type".into(), t.into()); + } + obj.insert("extra".into(), package.extra().clone()); + if let Some(src) = package.source() { + let mut s = serde_json::Map::new(); + s.insert("type".into(), src.kind.clone().into()); + s.insert("url".into(), src.url.clone().into()); + if let Some(r) = &src.reference { + s.insert("reference".into(), r.clone().into()); + } + obj.insert("source".into(), serde_json::Value::Object(s)); + } + if let Some(dist) = package.dist() { + let mut d = serde_json::Map::new(); + d.insert("type".into(), dist.kind.clone().into()); + d.insert("url".into(), dist.url.clone().into()); + if let Some(r) = &dist.reference { + d.insert("reference".into(), r.clone().into()); + } + obj.insert("dist".into(), serde_json::Value::Object(d)); + } + if let Some(is) = package.installation_source() { + let s = match is { + InstallationSource::Source => "source", + InstallationSource::Dist => "dist", + }; + obj.insert("installation-source".into(), s.into()); + } + serde_json::Value::Object(obj) } |
