aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-23 11:38:42 +0900
committernsfisis <nsfisis@gmail.com>2026-02-23 11:38:42 +0900
commit0080efea9386d46f65d1862fcb90eb44999d9761 (patch)
treee9f7e17b3f12ff9b09b3df0848fd55e91003cd23 /crates/mozart/src
parenteb1e21c059d83f0af9786e4d3cace80afe8456a2 (diff)
downloadphp-mozart-0080efea9386d46f65d1862fcb90eb44999d9761.tar.gz
php-mozart-0080efea9386d46f65d1862fcb90eb44999d9761.tar.zst
php-mozart-0080efea9386d46f65d1862fcb90eb44999d9761.zip
feat(vcs): add mozart-vcs crate for VCS repository support
Implement VCS driver/downloader infrastructure mirroring Composer's VCS subsystem. Includes drivers for GitHub, GitLab, Bitbucket, Forgejo, Git, Hg, and SVN with API-based metadata resolution, plus source downloaders for Git/Hg/SVN. Integrates into mozart-registry via vcs_bridge module to scan VCS repositories and feed discovered packages into the SAT resolver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart/src')
-rw-r--r--crates/mozart/src/commands/create_project.rs2
-rw-r--r--crates/mozart/src/commands/install.rs99
-rw-r--r--crates/mozart/src/commands/remove.rs6
-rw-r--r--crates/mozart/src/commands/require.rs4
-rw-r--r--crates/mozart/src/commands/update.rs10
5 files changed, 91 insertions, 30 deletions
diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs
index 6a5b815..2a8ce4f 100644
--- a/crates/mozart/src/commands/create_project.rs
+++ b/crates/mozart/src/commands/create_project.rs
@@ -411,6 +411,7 @@ pub async fn execute(
ignore_platform_req_list: args.ignore_platform_req.clone(),
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: raw.repositories.clone(),
};
console.info("Resolving dependencies...");
@@ -502,6 +503,7 @@ pub async fn execute(
apcu_autoloader,
apcu_autoloader_prefix: None,
download_only: false,
+ prefer_source: args.prefer_source,
},
)
.await?;
diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs
index 24c79b3..bf35788 100644
--- a/crates/mozart/src/commands/install.rs
+++ b/crates/mozart/src/commands/install.rs
@@ -117,6 +117,8 @@ pub struct InstallConfig {
pub apcu_autoloader_prefix: Option<String>,
/// Only download packages, skip autoloader generation and installed.json write.
pub download_only: bool,
+ /// Prefer installing from VCS source rather than dist archives.
+ pub prefer_source: bool,
}
impl Default for InstallConfig {
@@ -133,6 +135,7 @@ impl Default for InstallConfig {
apcu_autoloader: false,
apcu_autoloader_prefix: None,
download_only: false,
+ prefer_source: false,
}
}
}
@@ -300,20 +303,51 @@ fn make_progress(show: bool, pkg_name: &str, version: &str) -> downloader::Downl
downloader::DownloadProgress::new(show, format!("{pkg_name} ({version})"))
}
-/// Install packages from a lock file into vendor/.
-///
-/// Used by both the `install` and `update` commands.
-///
-/// This function:
-/// 1. Determines which packages to install (prod + optionally dev)
-/// 2. Warns about platform requirements (unless ignored)
-/// 3. Reads currently installed packages
-/// 4. Computes install/update/skip/removal operations
-/// 5. Prints a summary
-/// 6. Executes downloads with optional progress bars (unless dry_run)
-/// 7. Writes vendor/composer/installed.json
-/// 8. Cleans up empty vendor directories
-/// 9. Generates the autoloader (unless no_autoloader)
+/// Install a package from VCS source (git/svn/hg).
+fn install_from_source(
+ source_type: &str,
+ url: &str,
+ reference: &str,
+ vendor_dir: &Path,
+ package_name: &str,
+) -> anyhow::Result<()> {
+ let target = vendor_dir.join(package_name);
+ if target.exists() {
+ std::fs::remove_dir_all(&target)?;
+ }
+
+ match source_type {
+ "git" => {
+ let process = mozart_vcs::process::ProcessExecutor::new();
+ let git_util =
+ mozart_vcs::util::git::GitUtil::new(process, vendor_dir.join(".cache").join("git"));
+ let downloader = mozart_vcs::downloader::git::GitDownloader::new(git_util);
+ use mozart_vcs::downloader::VcsDownloader;
+ downloader.download(url, reference, &target)?;
+ downloader.install(url, reference, &target)?;
+ }
+ "svn" => {
+ let process = mozart_vcs::process::ProcessExecutor::new();
+ let svn_util = mozart_vcs::util::svn::SvnUtil::new(process);
+ let downloader = mozart_vcs::downloader::svn::SvnDownloader::new(svn_util);
+ use mozart_vcs::downloader::VcsDownloader;
+ downloader.install(url, reference, &target)?;
+ }
+ "hg" => {
+ let process = mozart_vcs::process::ProcessExecutor::new();
+ let hg_util = mozart_vcs::util::hg::HgUtil::new(process);
+ let downloader = mozart_vcs::downloader::hg::HgDownloader::new(hg_util);
+ use mozart_vcs::downloader::VcsDownloader;
+ downloader.install(url, reference, &target)?;
+ }
+ _ => {
+ anyhow::bail!("Unsupported source type for VCS install: {}", source_type);
+ }
+ }
+
+ Ok(())
+}
+
pub async fn install_from_lock(
lock: &lockfile::LockFile,
working_dir: &Path,
@@ -405,11 +439,32 @@ pub async fn install_from_lock(
}
}
+ // Try source install if --prefer-source and source info is available
+ if config.prefer_source
+ && let Some(source) = &pkg.source
+ {
+ install_from_source(
+ &source.source_type,
+ &source.url,
+ source.reference.as_deref().unwrap_or("HEAD"),
+ vendor_dir,
+ &pkg.name,
+ )?;
+ continue;
+ }
+
let dist = pkg.dist.as_ref().ok_or_else(|| {
- anyhow::anyhow!(
- "Package {} has no dist information — source installs are not yet supported",
- pkg.name
- )
+ if pkg.source.is_some() {
+ anyhow::anyhow!(
+ "Package {} has no dist information. Use --prefer-source to install from VCS.",
+ pkg.name,
+ )
+ } else {
+ anyhow::anyhow!(
+ "Package {} has no dist or source information",
+ pkg.name,
+ )
+ }
})?;
let mut progress = make_progress(!config.no_progress, &pkg.name, &pkg.version);
@@ -604,18 +659,13 @@ pub async fn execute(
}
}
- // Step 5: Warn about prefer-source (not yet supported)
+ // Step 5: Determine if prefer-source is enabled
let prefer_source = args.prefer_source
|| args
.prefer_install
.as_deref()
.map(|s| s.eq_ignore_ascii_case("source"))
.unwrap_or(false);
- if prefer_source {
- console.info(&console_format!(
- "<warning>Warning: Source installs are not yet supported. Falling back to dist.</warning>"
- ));
- }
// Step 6: Determine dev mode and vendor directory
let dev_mode = !args.no_dev;
@@ -638,6 +688,7 @@ pub async fn execute(
apcu_autoloader: args.apcu_autoloader || args.apcu_autoloader_prefix.is_some(),
apcu_autoloader_prefix: args.apcu_autoloader_prefix.clone(),
download_only: args.download_only,
+ prefer_source,
},
)
.await
diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs
index 88fa4da..6d3b5e2 100644
--- a/crates/mozart/src/commands/remove.rs
+++ b/crates/mozart/src/commands/remove.rs
@@ -251,6 +251,7 @@ pub async fn execute(
ignore_platform_req_list: args.ignore_platform_req.clone(),
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: raw.repositories.clone(),
};
// Print header messages
@@ -438,6 +439,7 @@ pub async fn execute(
apcu_autoloader: args.apcu_autoloader || args.apcu_autoloader_prefix.is_some(),
apcu_autoloader_prefix: args.apcu_autoloader_prefix.clone(),
download_only: false,
+ prefer_source: false,
},
)
.await?;
@@ -497,6 +499,7 @@ async fn remove_unused(
ignore_platform_req_list: args.ignore_platform_req.clone(),
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: raw.repositories.clone(),
};
console.info("Resolving dependencies to detect unused packages...");
@@ -577,6 +580,7 @@ async fn remove_unused(
apcu_autoloader: args.apcu_autoloader || args.apcu_autoloader_prefix.is_some(),
apcu_autoloader_prefix: args.apcu_autoloader_prefix.clone(),
download_only: false,
+ prefer_source: false,
},
)
.await?;
@@ -826,6 +830,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let resolved = resolve(&request)
.await
@@ -862,6 +867,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let resolved2 = resolve(&request2)
.await
diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs
index 4ea739d..15b5f1c 100644
--- a/crates/mozart/src/commands/require.rs
+++ b/crates/mozart/src/commands/require.rs
@@ -656,6 +656,7 @@ pub async fn execute(
ignore_platform_req_list: args.ignore_platform_req.clone(),
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: raw.repositories.clone(),
};
// Print header messages
@@ -870,6 +871,7 @@ pub async fn execute(
|| config_apcu,
apcu_autoloader_prefix: args.apcu_autoloader_prefix.clone(),
download_only: false,
+ prefer_source: args.prefer_source,
},
)
.await?;
@@ -1028,6 +1030,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let resolved = resolver::resolve(&request)
@@ -1081,6 +1084,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let resolved = resolver::resolve(&request)
diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs
index 06e6b22..c1901cd 100644
--- a/crates/mozart/src/commands/update.rs
+++ b/crates/mozart/src/commands/update.rs
@@ -835,6 +835,7 @@ pub async fn execute(
ignore_platform_req_list: args.ignore_platform_req.clone(),
repo_cache: None,
temporary_constraints,
+ repositories: composer_json.repositories.clone(),
};
// Step 6: Print header and run resolver
@@ -1164,18 +1165,13 @@ pub async fn execute(
// Step 12: Install packages (unless --no-install or --dry-run)
if !args.no_install && !args.dry_run {
- // Warn about prefer-source (not yet supported)
+ // Determine if prefer-source is enabled
let prefer_source = args.prefer_source
|| args
.prefer_install
.as_deref()
.map(|s| s.eq_ignore_ascii_case("source"))
.unwrap_or(false);
- if prefer_source {
- console.info(&console_format!(
- "<warning>Warning: Source installs are not yet supported. Falling back to dist.</warning>"
- ));
- }
super::install::install_from_lock(
&new_lock,
@@ -1193,6 +1189,7 @@ pub async fn execute(
apcu_autoloader: args.apcu_autoloader || args.apcu_autoloader_prefix.is_some(),
apcu_autoloader_prefix: args.apcu_autoloader_prefix.clone(),
download_only: false,
+ prefer_source,
},
)
.await?;
@@ -1909,6 +1906,7 @@ mod tests {
ignore_platform_req_list: vec![],
repo_cache: None,
temporary_constraints: HashMap::new(),
+ repositories: vec![],
};
let resolved = resolve(&request).await.expect("Resolution should succeed");