diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-08 19:52:18 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-08 19:52:18 +0900 |
| commit | 5cb8fc4e306970764e84bb850da2c56f844c3b12 (patch) | |
| tree | 0d66f3129a26138fcfee9402616b24929c40a017 /crates/mozart/src | |
| parent | d83b9ef48775aeb31ba1909b29d5470e6d0ddaaa (diff) | |
| download | php-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/src')
| -rw-r--r-- | crates/mozart/src/commands/status.rs | 713 |
1 files changed, 156 insertions, 557 deletions
diff --git a/crates/mozart/src/commands/status.rs b/crates/mozart/src/commands/status.rs index 60db8ac..8647078 100644 --- a/crates/mozart/src/commands/status.rs +++ b/crates/mozart/src/commands/status.rs @@ -1,610 +1,209 @@ use clap::Args; use indexmap::IndexMap; +use mozart_core::composer::{Composer, InstallationSource, LocalPackage}; +use mozart_core::console::Console; +use mozart_core::console_format; use mozart_core::console_writeln; -use sha1::{Digest, Sha1}; -use std::path::{Path, PathBuf}; +use mozart_core::console_writeln_error; +use mozart_core::exit_code; +use mozart_registry::download_manager::DownloadManager; +use mozart_vcs::version_guesser::VersionGuesser; #[derive(Args)] pub struct StatusArgs {} -/// Information extracted from a package's dist field. -struct DistInfo { - dist_type: String, - url: String, - shasum: Option<String>, +struct VcsVerChange { + previous: VerRef, + current: VerRef, } -/// The kind of change detected for a file. -#[derive(Debug, PartialEq)] -enum ChangeKind { - /// File was modified (exists in both, different hash). - Modified, - /// File was added in the installed copy (not in original archive). - Added, - /// File was removed from the installed copy (in original archive, not installed). - Removed, -} - -/// A single file change within a package. -struct FileChange { - kind: ChangeKind, - path: String, -} - -/// Changes detected for one package. -struct PackageStatus { - install_path: String, - note: Option<String>, - changes: Vec<FileChange>, +struct VerRef { + version: String, + reference: String, } pub async fn execute( _args: &StatusArgs, cli: &super::Cli, - console: &mozart_core::console::Console, + console: &Console, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; + let composer = Composer::require(&working_dir)?; + let installed_repo = composer.repository_manager().local_repository(); + let im = composer.installation_manager(); + let dm = DownloadManager::new(im.vendor_dir().join(".cache").join("git")); + let guesser = VersionGuesser::new(); - let vendor_dir = working_dir.join("vendor"); - let installed = mozart_registry::installed::InstalledPackages::read(&vendor_dir)?; - - if installed.packages.is_empty() { - console.info("No packages installed."); - return Ok(()); - } - - let cache_config = mozart_registry::cache::build_cache_config(cli.no_cache); - let files_cache = mozart_registry::cache::Cache::files(&cache_config); + let mut errors: IndexMap<String, String> = IndexMap::new(); + let mut unpushed_changes: IndexMap<String, String> = IndexMap::new(); + let mut vcs_version_changes: IndexMap<String, VcsVerChange> = IndexMap::new(); - let show_files = cli.verbose > 0; - - let mut modified_packages: Vec<PackageStatus> = Vec::new(); - - for pkg in &installed.packages { - let dist = match extract_dist_info(pkg) { - Some(d) => d, - None => { - if cli.verbose > 1 { - console.verbose(&format!(" Skipping {} — no dist info available", pkg.name)); - } - continue; - } + for package in installed_repo.canonical_packages() { + let Some(downloader) = dm.for_package(package) else { + continue; }; - - // Resolve install path - let install_path = resolve_install_path(pkg, &vendor_dir); - if !install_path.exists() { - if cli.verbose > 0 { - console.verbose(&format!( - " Skipping {} — install path does not exist: {}", - pkg.name, - install_path.display() - )); - } + let Some(target_dir) = im.get_install_path(package) else { continue; - } + }; + let target_key = target_dir.display().to_string(); - // Check if the install path is a symlink — skip archive comparison - if install_path.is_symlink() { - let note = format!("{} is a symbolic link.", install_path.display()); - modified_packages.push(PackageStatus { - install_path: install_path.to_string_lossy().into_owned(), - note: Some(note), - changes: Vec::new(), - }); - continue; + // 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 cli.verbose > 0 { - console.verbose(&format!(" Checking {} ...", pkg.name)); + if let Some(changes) = downloader.local_changes(&target_dir)? { + errors.insert(target_key.clone(), changes); } - // Download original archive to a temp dir - let tmp_dir = make_temp_dir(&pkg.name)?; - let downloaded = mozart_registry::downloader::download_dist( - &dist.url, - dist.shasum.as_deref(), - None, - &files_cache, - ) - .await; - - let bytes = match downloaded { - Ok(b) => b, - Err(e) => { - console.info(&format!( - " Warning: could not download dist for {}: {}", - pkg.name, e - )); - let _ = std::fs::remove_dir_all(&tmp_dir); - continue; - } - }; - - // Extract archive to temp dir - let extract_result = match dist.dist_type.as_str() { - "zip" => mozart_registry::downloader::extract_zip(&bytes, &tmp_dir), - "tar" | "tar.gz" | "tgz" => { - mozart_registry::downloader::extract_tar_gz(&bytes, &tmp_dir) - } - other => { - console.info(&format!( - " Warning: unsupported dist type '{}' for {}", - other, pkg.name - )); - let _ = std::fs::remove_dir_all(&tmp_dir); - continue; + // VcsCapableDownloaderInterface + if 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); + 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(), + 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(), + }, + }, + ); + } } - }; - - if let Err(e) = extract_result { - console.info(&format!( - " Warning: could not extract dist for {}: {}", - pkg.name, e - )); - let _ = std::fs::remove_dir_all(&tmp_dir); - continue; } - // Hash both directories - let original_hashes = hash_directory(&tmp_dir)?; - let installed_hashes = hash_directory(&install_path)?; - let _ = std::fs::remove_dir_all(&tmp_dir); - - // Compute diff - let changes = compute_diff(&original_hashes, &installed_hashes); - - if !changes.is_empty() { - modified_packages.push(PackageStatus { - install_path: install_path.to_string_lossy().into_owned(), - note: None, - changes, - }); + // DvcsDownloaderInterface + if let Some(unpushed) = downloader.unpushed_changes(&target_dir)? { + unpushed_changes.insert(target_key.clone(), unpushed); } } - if modified_packages.is_empty() { - console.info("No local changes"); + if errors.is_empty() && unpushed_changes.is_empty() && vcs_version_changes.is_empty() { + console_writeln_error!(console, &console_format!("<info>No local changes</info>")); return Ok(()); } - console.info("You have changes in the following dependencies:\n"); - - for pkg_status in &modified_packages { - if let Some(ref note) = pkg_status.note { - console_writeln!(console, note); - } else { - console_writeln!(console, &pkg_status.install_path); - - if show_files { - let mut sorted_changes: Vec<&FileChange> = pkg_status.changes.iter().collect(); - sorted_changes.sort_by_key(|c| c.path.as_str()); + let verbose = cli.verbose > 0; + let very_verbose = cli.verbose >= 2; - for change in sorted_changes { - let prefix = match change.kind { - ChangeKind::Modified => 'M', - ChangeKind::Added => '+', - ChangeKind::Removed => '-', - }; - console_writeln!(console, &format!(" {} {}", prefix, change.path),); - } + if !errors.is_empty() { + console_writeln_error!( + console, + &console_format!("<error>You have changes in the following dependencies:</error>") + ); + for (path, changes) in &errors { + if verbose { + console_writeln!(console, &console_format!("<info>{path}</info>:")); + console_writeln!(console, &indent_block(changes)); + } else { + console_writeln!(console, path); } } } - // Hint about --verbose if not already showing files and there are modified packages - if !show_files { - console.info("Use --verbose (-v) to see a list of files"); - } - - // Exit with code 1 if modifications found - Err(mozart_core::exit_code::bail_silent(1)) -} - -/// Extract dist info from an installed package entry. -fn extract_dist_info(pkg: &mozart_registry::installed::InstalledPackageEntry) -> Option<DistInfo> { - // Try the strongly-typed `dist` field first - let dist_val = pkg.dist.as_ref().or_else(|| pkg.extra_fields.get("dist"))?; - - let dist_type = dist_val.get("type").and_then(|v| v.as_str())?.to_string(); - let url = dist_val.get("url").and_then(|v| v.as_str())?.to_string(); - let shasum = dist_val - .get("shasum") - .and_then(|v| v.as_str()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - - if url.is_empty() { - return None; + if !unpushed_changes.is_empty() { + console_writeln_error!( + console, + &console_format!( + "<warning>You have unpushed changes on the current branch in the following dependencies:</warning>" + ) + ); + for (path, changes) in &unpushed_changes { + if verbose { + console_writeln!(console, &console_format!("<info>{path}</info>:")); + console_writeln!(console, &indent_block(changes)); + } else { + console_writeln!(console, path); + } + } } - Some(DistInfo { - dist_type, - url, - shasum, - }) -} - -/// Resolve the on-disk install path for a package. -/// -/// Prefers the `install-path` field from installed.json when available, -/// since it is a path relative to `vendor/composer/`. Falls back to -/// `vendor/<package-name>`. -fn resolve_install_path( - pkg: &mozart_registry::installed::InstalledPackageEntry, - vendor_dir: &Path, -) -> PathBuf { - if let Some(ref rel) = pkg.install_path { - // install-path is relative to vendor/composer/ - let base = vendor_dir.join("composer"); - let resolved = base.join(rel); - // Normalize out ".." segments using canonicalize-like logic - let resolved_str = resolved.to_string_lossy().into_owned(); - let mut components: Vec<&str> = Vec::new(); - for part in resolved_str.split('/') { - match part { - ".." => { - components.pop(); + if !vcs_version_changes.is_empty() { + console_writeln_error!( + console, + &console_format!( + "<warning>You have version variations in the following dependencies:</warning>" + ) + ); + for (path, change) in &vcs_version_changes { + if verbose { + let mut prev = if change.previous.version.is_empty() { + change.previous.reference.clone() + } else { + change.previous.version.clone() + }; + let mut curr = if change.current.version.is_empty() { + change.current.reference.clone() + } else { + change.current.version.clone() + }; + if very_verbose { + prev.push_str(&format!(" ({})", change.previous.reference)); + curr.push_str(&format!(" ({})", change.current.reference)); } - "." | "" => {} - p => components.push(p), + console_writeln!(console, &console_format!("<info>{path}</info>:")); + console_writeln!( + console, + &console_format!( + " From <comment>{prev}</comment> to <comment>{curr}</comment>" + ) + ); + } else { + console_writeln!(console, path); } } - PathBuf::from("/".to_string() + &components.join("/")) - } else { - vendor_dir.join(&pkg.name) } -} -/// Create a unique temporary directory for extracting a package archive. -/// -/// The directory is placed under the system temp dir and named using the -/// package name (with `/` replaced) and a timestamp-derived suffix so that -/// concurrent runs are unlikely to collide. The caller is responsible for -/// removing the directory when done. -fn make_temp_dir(package_name: &str) -> anyhow::Result<PathBuf> { - use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .subsec_nanos(); - let safe_name = package_name.replace('/', "_"); - let dir = std::env::temp_dir().join(format!("mozart_status_{}_{}", safe_name, nanos)); - std::fs::create_dir_all(&dir)?; - Ok(dir) -} - -/// Recursively hash all files in a directory. -/// -/// Returns a map from relative path string to SHA-1 hex digest. -fn hash_directory(dir: &Path) -> anyhow::Result<IndexMap<String, String>> { - let mut map = IndexMap::new(); - hash_dir_recursive(dir, dir, &mut map)?; - Ok(map) -} - -fn hash_dir_recursive( - root: &Path, - current: &Path, - map: &mut IndexMap<String, String>, -) -> anyhow::Result<()> { - let entries = match std::fs::read_dir(current) { - Ok(e) => e, - Err(_) => return Ok(()), - }; - - for entry in entries { - let entry = entry?; - let path = entry.path(); - let metadata = entry.metadata()?; - - if metadata.is_dir() { - hash_dir_recursive(root, &path, map)?; - } else if metadata.is_file() { - let relative = path - .strip_prefix(root) - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_default(); - - let contents = std::fs::read(&path)?; - let mut hasher = Sha1::new(); - hasher.update(&contents); - let hex = format!("{:x}", hasher.finalize()); - - map.insert(relative, hex); - } - // Symlinks are skipped + if !verbose { + console_writeln_error!(console, "Use --verbose (-v) to see a list of files"); } + let code = (if !errors.is_empty() { 1 } else { 0 }) + | (if !unpushed_changes.is_empty() { 2 } else { 0 }) + | (if !vcs_version_changes.is_empty() { + 4 + } else { + 0 + }); + if code != 0 { + return Err(exit_code::bail_silent(code)); + } Ok(()) } -/// Compare two hash maps (original vs installed) and return a list of changes. -fn compute_diff( - original: &IndexMap<String, String>, - installed: &IndexMap<String, String>, -) -> Vec<FileChange> { - let mut changes: Vec<FileChange> = Vec::new(); - - // Files in original: check for modifications and removals - for (path, orig_hash) in original { - match installed.get(path) { - Some(inst_hash) if inst_hash != orig_hash => { - changes.push(FileChange { - kind: ChangeKind::Modified, - path: path.clone(), - }); - } - Some(_) => {} // unchanged - None => { - changes.push(FileChange { - kind: ChangeKind::Removed, - path: path.clone(), - }); - } - } - } - - // Files in installed but not in original: added - for path in installed.keys() { - if !original.contains_key(path) { - changes.push(FileChange { - kind: ChangeKind::Added, - path: path.clone(), - }); - } - } - - changes +fn indent_block(s: &str) -> String { + s.split('\n') + .map(|line| format!(" {}", line.trim_start())) + .collect::<Vec<_>>() + .join("\n") } -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::tempdir; - - #[test] - fn test_hash_directory() { - let dir = tempdir().unwrap(); - - fs::write(dir.path().join("file.txt"), b"hello").unwrap(); - fs::create_dir(dir.path().join("sub")).unwrap(); - fs::write(dir.path().join("sub/nested.txt"), b"world").unwrap(); - - let hashes = hash_directory(dir.path()).unwrap(); - assert_eq!(hashes.len(), 2); - assert!(hashes.contains_key("file.txt")); - assert!(hashes.contains_key("sub/nested.txt")); - - // Same content → same hash - let dir2 = tempdir().unwrap(); - fs::write(dir2.path().join("file.txt"), b"hello").unwrap(); - fs::create_dir(dir2.path().join("sub")).unwrap(); - fs::write(dir2.path().join("sub/nested.txt"), b"world").unwrap(); - - let hashes2 = hash_directory(dir2.path()).unwrap(); - assert_eq!(hashes["file.txt"], hashes2["file.txt"]); - assert_eq!(hashes["sub/nested.txt"], hashes2["sub/nested.txt"]); - - // Different content → different hash - let dir3 = tempdir().unwrap(); - fs::write(dir3.path().join("file.txt"), b"different").unwrap(); - let hashes3 = hash_directory(dir3.path()).unwrap(); - assert_ne!(hashes["file.txt"], hashes3["file.txt"]); - } - - #[test] - fn test_compute_diff_no_changes() { - let mut map: IndexMap<String, String> = IndexMap::new(); - map.insert("src/Foo.php".to_string(), "abc123".to_string()); - map.insert("src/Bar.php".to_string(), "def456".to_string()); - - let changes = compute_diff(&map, &map); - assert!(changes.is_empty()); - } - - #[test] - fn test_compute_diff_modified() { - let mut original: IndexMap<String, String> = IndexMap::new(); - original.insert("src/Foo.php".to_string(), "abc123".to_string()); - - let mut installed: IndexMap<String, String> = IndexMap::new(); - installed.insert("src/Foo.php".to_string(), "xyz999".to_string()); - - let changes = compute_diff(&original, &installed); - assert_eq!(changes.len(), 1); - assert_eq!(changes[0].kind, ChangeKind::Modified); - assert_eq!(changes[0].path, "src/Foo.php"); - } - - #[test] - fn test_compute_diff_added() { - let original: IndexMap<String, String> = IndexMap::new(); - - let mut installed: IndexMap<String, String> = IndexMap::new(); - installed.insert("src/NewFile.php".to_string(), "aabbcc".to_string()); - - let changes = compute_diff(&original, &installed); - assert_eq!(changes.len(), 1); - assert_eq!(changes[0].kind, ChangeKind::Added); - assert_eq!(changes[0].path, "src/NewFile.php"); - } - - #[test] - fn test_compute_diff_removed() { - let mut original: IndexMap<String, String> = IndexMap::new(); - original.insert("src/OldFile.php".to_string(), "112233".to_string()); - - let installed: IndexMap<String, String> = IndexMap::new(); - - let changes = compute_diff(&original, &installed); - assert_eq!(changes.len(), 1); - assert_eq!(changes[0].kind, ChangeKind::Removed); - assert_eq!(changes[0].path, "src/OldFile.php"); - } - - #[test] - fn test_compute_diff_mixed() { - let mut original: IndexMap<String, String> = IndexMap::new(); - original.insert("src/Unchanged.php".to_string(), "same".to_string()); - original.insert("src/Modified.php".to_string(), "old".to_string()); - original.insert("src/Removed.php".to_string(), "gone".to_string()); - - let mut installed: IndexMap<String, String> = IndexMap::new(); - installed.insert("src/Unchanged.php".to_string(), "same".to_string()); - installed.insert("src/Modified.php".to_string(), "new".to_string()); - installed.insert("src/Added.php".to_string(), "extra".to_string()); - - let changes = compute_diff(&original, &installed); - assert_eq!(changes.len(), 3); - - let modified: Vec<_> = changes - .iter() - .filter(|c| c.kind == ChangeKind::Modified) - .collect(); - let added: Vec<_> = changes - .iter() - .filter(|c| c.kind == ChangeKind::Added) - .collect(); - let removed: Vec<_> = changes - .iter() - .filter(|c| c.kind == ChangeKind::Removed) - .collect(); - - assert_eq!(modified.len(), 1); - assert_eq!(modified[0].path, "src/Modified.php"); - - assert_eq!(added.len(), 1); - assert_eq!(added[0].path, "src/Added.php"); - - assert_eq!(removed.len(), 1); - assert_eq!(removed[0].path, "src/Removed.php"); - } - - #[test] - fn test_extract_dist_info_from_dist_field() { - use std::collections::BTreeMap; - - let pkg = mozart_registry::installed::InstalledPackageEntry { - name: "vendor/pkg".to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: Some(serde_json::json!({ - "type": "zip", - "url": "https://example.com/pkg.zip", - "reference": "abc123", - "shasum": "deadbeef" - })), - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: BTreeMap::new(), - }; - - let info = extract_dist_info(&pkg).unwrap(); - assert_eq!(info.dist_type, "zip"); - assert_eq!(info.url, "https://example.com/pkg.zip"); - assert_eq!(info.shasum.as_deref(), Some("deadbeef")); - } - - #[test] - fn test_extract_dist_info_no_url() { - use std::collections::BTreeMap; - - let pkg = mozart_registry::installed::InstalledPackageEntry { - name: "vendor/pkg".to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: Some(serde_json::json!({ - "type": "zip", - "url": "", - "shasum": "" - })), - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: BTreeMap::new(), - }; - - assert!(extract_dist_info(&pkg).is_none()); - } - - #[test] - fn test_extract_dist_info_absent() { - use std::collections::BTreeMap; - - let pkg = mozart_registry::installed::InstalledPackageEntry { - name: "vendor/pkg".to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: BTreeMap::new(), - }; - - assert!(extract_dist_info(&pkg).is_none()); - } - - #[test] - fn test_resolve_install_path_default() { - use std::collections::BTreeMap; - - let pkg = mozart_registry::installed::InstalledPackageEntry { - name: "monolog/monolog".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: BTreeMap::new(), - }; - - let vendor = PathBuf::from("/project/vendor"); - let path = resolve_install_path(&pkg, &vendor); - assert_eq!(path, PathBuf::from("/project/vendor/monolog/monolog")); - } - - #[test] - fn test_resolve_install_path_with_install_path() { - use std::collections::BTreeMap; - - let pkg = mozart_registry::installed::InstalledPackageEntry { - name: "monolog/monolog".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: Some("../monolog/monolog".to_string()), - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: BTreeMap::new(), - }; - - let vendor = PathBuf::from("/project/vendor"); - let path = resolve_install_path(&pkg, &vendor); - assert_eq!(path, PathBuf::from("/project/vendor/monolog/monolog")); - } +/// Build the `package_config` shape that `VersionGuesser` reads. The PHP +/// equivalent is `ArrayDumper::dump($package)`; we only need the fields +/// that `VersionGuesser` actually inspects. +fn build_package_config(package: &LocalPackage) -> serde_json::Value { + serde_json::json!({ + "extra": package.extra(), + }) } |
