From 5cb8fc4e306970764e84bb850da2c56f844c3b12 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 8 May 2026 19:52:18 +0900 Subject: fix(status): align with Composer's StatusCommand pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/mozart-core/src/composer.rs | 100 +++++++++++++++++++++++++++++++++++-- crates/mozart-core/src/factory.rs | 57 ++++++++++++++++++++- 2 files changed, 152 insertions(+), 5 deletions(-) (limited to 'crates/mozart-core') diff --git a/crates/mozart-core/src/composer.rs b/crates/mozart-core/src/composer.rs index 6fe022a..66bae92 100644 --- a/crates/mozart-core/src/composer.rs +++ b/crates/mozart-core/src/composer.rs @@ -92,20 +92,72 @@ pub struct Composer { locker: Locker, } -/// Subset of `Composer\Package\PackageInterface` needed by the -/// installation manager. Today only the fields referenced by -/// `LibraryInstaller::getInstallPath` (`prettyName`, `targetDir`). +/// Which source the package was installed from. Mirrors +/// `PackageInterface::getInstallationSource` ("source" | "dist"). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstallationSource { + Source, + Dist, +} + +impl InstallationSource { + /// Parse the `installation-source` field from `installed.json`. + pub fn parse(s: &str) -> Option { + match s { + "source" => Some(InstallationSource::Source), + "dist" => Some(InstallationSource::Dist), + _ => None, + } + } +} + +/// Source/dist descriptor — mirrors the nested `source`/`dist` objects in +/// `installed.json`. +#[derive(Debug, Clone)] +pub struct PackageReference { + pub kind: String, + pub url: String, + pub reference: Option, + pub shasum: Option, +} + +/// Subset of `Composer\Package\PackageInterface` carried through Mozart's +/// `LocalRepository`. Holds the fields needed by both the installation +/// manager (`prettyName`, `targetDir`) and the status command +/// (installation source, source/dist refs, version, extra). #[derive(Debug, Clone)] pub struct LocalPackage { pretty_name: String, + pretty_version: String, target_dir: Option, + package_type: Option, + installation_source: Option, + source: Option, + dist: Option, + extra: serde_json::Value, } impl LocalPackage { - pub fn new(pretty_name: String, target_dir: Option) -> Self { + #[allow(clippy::too_many_arguments)] + pub fn new( + pretty_name: String, + pretty_version: String, + target_dir: Option, + package_type: Option, + installation_source: Option, + source: Option, + dist: Option, + extra: serde_json::Value, + ) -> Self { Self { pretty_name, + pretty_version, target_dir, + package_type, + installation_source, + source, + dist, + extra, } } @@ -115,11 +167,51 @@ impl LocalPackage { &self.pretty_name } + /// Original-case version string (e.g. `v1.0.0`). Mirrors + /// `PackageInterface::getPrettyVersion`. + pub fn pretty_version(&self) -> &str { + &self.pretty_version + } + /// Optional sub-directory inside the install path that holds the /// package code. Mirrors `PackageInterface::getTargetDir`. pub fn target_dir(&self) -> Option<&str> { self.target_dir.as_deref() } + + /// Mirrors `PackageInterface::getType`. + pub fn package_type(&self) -> Option<&str> { + self.package_type.as_deref() + } + + /// Mirrors `PackageInterface::getInstallationSource`. + pub fn installation_source(&self) -> Option { + self.installation_source + } + + pub fn source(&self) -> Option<&PackageReference> { + self.source.as_ref() + } + + pub fn dist(&self) -> Option<&PackageReference> { + self.dist.as_ref() + } + + /// Mirrors `PackageInterface::getSourceReference`. + pub fn source_reference(&self) -> Option<&str> { + self.source.as_ref().and_then(|r| r.reference.as_deref()) + } + + /// Mirrors `PackageInterface::getDistReference`. + pub fn dist_reference(&self) -> Option<&str> { + self.dist.as_ref().and_then(|r| r.reference.as_deref()) + } + + /// Raw `extra` field — used by VersionGuesser to read + /// `branch-alias`, `non-feature-branches`, etc. + pub fn extra(&self) -> &serde_json::Value { + &self.extra + } } /// In-memory mirror of `Composer\Repository\InstalledFilesystemRepository` diff --git a/crates/mozart-core/src/factory.rs b/crates/mozart-core/src/factory.rs index 92aa70e..c9d346b 100644 --- a/crates/mozart-core/src/factory.rs +++ b/crates/mozart-core/src/factory.rs @@ -276,15 +276,70 @@ fn read_local_packages(vendor_dir: &Path) -> anyhow::Result> { .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); + let pretty_version = entry + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); let target_dir = entry .get("target-dir") .and_then(|v| v.as_str()) .map(|s| s.to_string()); - out.push(LocalPackage::new(pretty_name, target_dir)); + let package_type = entry + .get("type") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let installation_source = entry + .get("installation-source") + .and_then(|v| v.as_str()) + .and_then(crate::composer::InstallationSource::parse); + let source = read_package_reference(entry.get("source")); + let dist = read_package_reference(entry.get("dist")); + let extra = entry + .get("extra") + .cloned() + .unwrap_or(serde_json::Value::Null); + out.push(LocalPackage::new( + pretty_name, + pretty_version, + target_dir, + package_type, + installation_source, + source, + dist, + extra, + )); } Ok(out) } +fn read_package_reference( + value: Option<&serde_json::Value>, +) -> Option { + let v = value?; + let kind = v.get("type").and_then(|x| x.as_str())?.to_string(); + let url = v + .get("url") + .and_then(|x| x.as_str()) + .unwrap_or("") + .to_string(); + let reference = v + .get("reference") + .and_then(|x| x.as_str()) + .map(|s| s.to_string()); + let shasum = v + .get("shasum") + .and_then(|x| x.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + Some(crate::composer::PackageReference { + kind, + url, + reference, + shasum, + }) +} + #[cfg(test)] mod tests { use super::*; -- cgit v1.3.1