aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-23 12:10:44 +0900
committernsfisis <nsfisis@gmail.com>2026-02-23 12:14:11 +0900
commitd8ecb21a7931ec6f1d7b447d0c15f53de32bfc45 (patch)
tree0da1e0c2ec988905b98939b524b72217e83019de /crates
parent0080efea9386d46f65d1862fcb90eb44999d9761 (diff)
downloadphp-mozart-d8ecb21a7931ec6f1d7b447d0c15f53de32bfc45.tar.gz
php-mozart-d8ecb21a7931ec6f1d7b447d0c15f53de32bfc45.tar.zst
php-mozart-d8ecb21a7931ec6f1d7b447d0c15f53de32bfc45.zip
refactor(vcs): convert VcsDriver trait to native async
Replace manual tokio::runtime::Handle::current().block_on() calls with native async/await throughout all VCS drivers. Introduce AnyVcsDriver enum for static dispatch to avoid dyn trait with async methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/mozart-registry/src/resolver.rs270
-rw-r--r--crates/mozart-registry/src/vcs_bridge.rs4
-rw-r--r--crates/mozart-vcs/src/driver/bitbucket.rs64
-rw-r--r--crates/mozart-vcs/src/driver/forgejo.rs58
-rw-r--r--crates/mozart-vcs/src/driver/git.rs21
-rw-r--r--crates/mozart-vcs/src/driver/github.rs61
-rw-r--r--crates/mozart-vcs/src/driver/gitlab.rs64
-rw-r--r--crates/mozart-vcs/src/driver/hg.rs21
-rw-r--r--crates/mozart-vcs/src/driver/mod.rs131
-rw-r--r--crates/mozart-vcs/src/driver/svn.rs21
-rw-r--r--crates/mozart-vcs/src/repository.rs24
-rw-r--r--crates/mozart-vcs/tests/git_driver_test.rs32
12 files changed, 438 insertions, 333 deletions
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index 4930b3a..1cd5bb6 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -407,110 +407,107 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
root_requires.insert(name.clone(), Some(constraint.clone()));
}
- // Capture data needed by spawn_blocking
- let handle = tokio::runtime::Handle::current();
- let repo_cache = request.repo_cache.clone();
- let platform_config = request.platform.to_versions();
- let minimum_stability = request.minimum_stability;
- let stability_flags = request.stability_flags.clone();
- let prefer_stable = request.prefer_stable;
- let prefer_lowest = request.prefer_lowest;
- let ignore_platform_reqs = request.ignore_platform_reqs;
- let ignore_platform_req_list = request.ignore_platform_req_list.clone();
- let vcs_repositories = request.repositories.clone();
+ // 2. Build pool, generate rules, and solve
+ let mut builder = PoolBuilder::new();
- // 2. Build pool, generate rules, and solve on a blocking thread
- tokio::task::spawn_blocking(move || -> Result<Vec<ResolvedPackage>, ResolveError> {
- let mut builder = PoolBuilder::new();
+ // Set up ignore list for platform requirements
+ let mut ignore_set: HashSet<String> = HashSet::new();
+ if request.ignore_platform_reqs {
+ // We'll skip platform deps in the loop below
+ }
+ for name in &request.ignore_platform_req_list {
+ ignore_set.insert(name.clone());
+ }
+ builder.set_ignore_platform_reqs(ignore_set.clone());
- // Set up ignore list for platform requirements
- let mut ignore_set: HashSet<String> = HashSet::new();
- if ignore_platform_reqs {
- // We'll skip platform deps in the loop below
- }
- for name in &ignore_platform_req_list {
- ignore_set.insert(name.clone());
+ // Add platform packages as fixed entries
+ let platform_config = request.platform.to_versions();
+ let mut fixed_packages_by_name: HashMap<String, u32> = HashMap::new();
+ for (name, version) in &platform_config {
+ if should_skip_platform_dep(
+ name,
+ request.ignore_platform_reqs,
+ &request.ignore_platform_req_list,
+ ) {
+ continue;
}
- builder.set_ignore_platform_reqs(ignore_set.clone());
+ let input = PoolPackageInput {
+ name: name.clone(),
+ version: version.to_string(),
+ pretty_version: version.to_string(),
+ requires: vec![],
+ replaces: vec![],
+ provides: vec![],
+ conflicts: vec![],
+ is_fixed: true,
+ };
+ builder.add_package(input);
+ }
- // Add platform packages as fixed entries
- let mut fixed_packages_by_name: HashMap<String, u32> = HashMap::new();
- for (name, version) in &platform_config {
- if should_skip_platform_dep(name, ignore_platform_reqs, &ignore_platform_req_list) {
- continue;
- }
- let input = PoolPackageInput {
- name: name.clone(),
- version: version.to_string(),
- pretty_version: version.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: true,
- };
+ // Scan VCS repositories and collect packages from them
+ let vcs_packages = vcs_bridge::scan_vcs_repositories(&request.repositories).await;
+ let mut vcs_package_names: HashSet<String> = HashSet::new();
+ for vpkg in &vcs_packages {
+ vcs_package_names.insert(vpkg.name.clone());
+ }
+
+ // Add VCS packages to the pool
+ for vpkg in &vcs_packages {
+ let inputs = vcs_bridge::vcs_to_pool_inputs(
+ vpkg,
+ request.minimum_stability,
+ &request.stability_flags,
+ );
+ for input in inputs {
builder.add_package(input);
}
+ }
- // Scan VCS repositories and collect packages from them
- let vcs_repos = &vcs_repositories;
- let vcs_packages = vcs_bridge::scan_vcs_repositories(vcs_repos);
- let mut vcs_package_names: HashSet<String> = HashSet::new();
- for vpkg in &vcs_packages {
- vcs_package_names.insert(vpkg.name.clone());
+ // Seed the builder with packages for root requirements
+ for name in root_requires.keys() {
+ if PackageName(name.clone()).is_platform() {
+ continue; // platform packages already added
}
- // Add VCS packages to the pool
- for vpkg in &vcs_packages {
- let inputs = vcs_bridge::vcs_to_pool_inputs(vpkg, minimum_stability, &stability_flags);
- for input in inputs {
- builder.add_package(input);
- }
+ // Skip packages already provided by VCS repositories
+ if vcs_package_names.contains(name) {
+ continue;
}
- // Seed the builder with packages for root requirements
- for name in root_requires.keys() {
- if PackageName(name.clone()).is_platform() {
- continue; // platform packages already added
- }
-
- // Skip packages already provided by VCS repositories
- if vcs_package_names.contains(name) {
- continue;
- }
+ // Fetch available versions from Packagist
+ let versions = packagist::fetch_package_versions(name, request.repo_cache.as_ref())
+ .await
+ .map_err(|e| {
+ ResolveError::DependencyFetchError(format!("Failed to fetch {}: {}", name, e))
+ })?;
- // Fetch available versions from Packagist
- let versions = handle
- .block_on(packagist::fetch_package_versions(name, repo_cache.as_ref()))
- .map_err(|e| {
- ResolveError::DependencyFetchError(format!("Failed to fetch {}: {}", name, e))
- })?;
-
- for pv in &versions {
- let inputs =
- packagist_to_pool_inputs(name, pv, minimum_stability, &stability_flags);
- for input in inputs {
- builder.add_package(input);
- }
+ for pv in &versions {
+ let inputs = packagist_to_pool_inputs(
+ name,
+ pv,
+ request.minimum_stability,
+ &request.stability_flags,
+ );
+ for input in inputs {
+ builder.add_package(input);
}
}
+ }
- // Explore transitive dependencies
- while let Some(name) = builder.next_pending() {
- if PackageName(name.clone()).is_platform() {
- // Platform package: already added if available, skip fetching
- continue;
- }
+ // Explore transitive dependencies
+ while let Some(name) = builder.next_pending() {
+ if PackageName(name.clone()).is_platform() {
+ // Platform package: already added if available, skip fetching
+ continue;
+ }
- // Skip packages already provided by VCS repositories
- if vcs_package_names.contains(&name) {
- continue;
- }
+ // Skip packages already provided by VCS repositories
+ if vcs_package_names.contains(&name) {
+ continue;
+ }
- let versions = match handle.block_on(packagist::fetch_package_versions(
- &name,
- repo_cache.as_ref(),
- )) {
+ let versions =
+ match packagist::fetch_package_versions(&name, request.repo_cache.as_ref()).await {
Ok(v) => v,
Err(_) => {
// Virtual/meta packages (e.g. "psr/http-client-implementation")
@@ -520,68 +517,69 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
}
};
- for pv in &versions {
- let inputs =
- packagist_to_pool_inputs(&name, pv, minimum_stability, &stability_flags);
- for input in inputs {
- builder.add_package(input);
- }
+ for pv in &versions {
+ let inputs = packagist_to_pool_inputs(
+ &name,
+ pv,
+ request.minimum_stability,
+ &request.stability_flags,
+ );
+ for input in inputs {
+ builder.add_package(input);
}
}
+ }
- // Build the pool
- let mut pool = builder.build();
+ // Build the pool
+ let mut pool = builder.build();
- // Collect fixed package IDs
- let mut fixed_ids: Vec<u32> = Vec::new();
- for pkg in pool.packages() {
- if pkg.is_fixed {
- fixed_ids.push(pkg.id);
- fixed_packages_by_name.insert(pkg.name.clone(), pkg.id);
- }
+ // Collect fixed package IDs
+ let mut fixed_ids: Vec<u32> = Vec::new();
+ for pkg in pool.packages() {
+ if pkg.is_fixed {
+ fixed_ids.push(pkg.id);
+ fixed_packages_by_name.insert(pkg.name.clone(), pkg.id);
}
+ }
- // Generate rules
- let mut generator = RuleSetGenerator::new(&mut pool);
- generator.set_ignore_platform_reqs(ignore_set);
- let rules = generator.generate(&root_requires, &fixed_ids);
+ // Generate rules
+ let mut generator = RuleSetGenerator::new(&mut pool);
+ generator.set_ignore_platform_reqs(ignore_set);
+ let rules = generator.generate(&root_requires, &fixed_ids);
- // Create policy and solve
- let policy = DefaultPolicy::new(prefer_stable, prefer_lowest);
- let fixed_set: HashSet<u32> = fixed_ids.into_iter().collect();
- let solver = Solver::new(rules, &pool, policy, fixed_set);
+ // Create policy and solve
+ let policy = DefaultPolicy::new(request.prefer_stable, request.prefer_lowest);
+ let fixed_set: HashSet<u32> = fixed_ids.into_iter().collect();
+ let solver = Solver::new(rules, &pool, policy, fixed_set);
- match solver.solve() {
- Ok(result) => {
- let mut resolved = Vec::new();
- for pkg_id in result.installed {
- let pkg = pool.package_by_id(pkg_id);
+ match solver.solve() {
+ Ok(result) => {
+ let mut resolved = Vec::new();
+ for pkg_id in result.installed {
+ let pkg = pool.package_by_id(pkg_id);
- // Skip platform packages from output
- if PackageName(pkg.name.clone()).is_platform() {
- continue;
- }
+ // Skip platform packages from output
+ if PackageName(pkg.name.clone()).is_platform() {
+ continue;
+ }
- let is_dev = if let Ok(v) = Version::parse(&pkg.version) {
- version_stability(&v) == Stability::Dev
- } else {
- false
- };
+ let is_dev = if let Ok(v) = Version::parse(&pkg.version) {
+ version_stability(&v) == Stability::Dev
+ } else {
+ false
+ };
- resolved.push(ResolvedPackage {
- name: pkg.name.clone(),
- version: pkg.pretty_version.clone(),
- version_normalized: pkg.version.clone(),
- is_dev,
- });
- }
- Ok(resolved)
+ resolved.push(ResolvedPackage {
+ name: pkg.name.clone(),
+ version: pkg.pretty_version.clone(),
+ version_normalized: pkg.version.clone(),
+ is_dev,
+ });
}
- Err(e) => Err(ResolveError::NoSolution(e.to_string())),
+ Ok(resolved)
}
- })
- .await
- .unwrap()
+ Err(e) => Err(ResolveError::NoSolution(e.to_string())),
+ }
}
// ─────────────────────────────────────────────────────────────────────────────
diff --git a/crates/mozart-registry/src/vcs_bridge.rs b/crates/mozart-registry/src/vcs_bridge.rs
index d81cae8..78aaaa5 100644
--- a/crates/mozart-registry/src/vcs_bridge.rs
+++ b/crates/mozart-registry/src/vcs_bridge.rs
@@ -16,7 +16,7 @@ use crate::resolver::{parse_normalized, version_stability};
/// Scan all VCS-type repositories and collect package versions.
///
/// Non-VCS repos (e.g. "composer", "package") are silently skipped.
-pub fn scan_vcs_repositories(repositories: &[RawRepository]) -> Vec<VcsPackageVersion> {
+pub async fn scan_vcs_repositories(repositories: &[RawRepository]) -> Vec<VcsPackageVersion> {
let config = DriverConfig::default();
let mut all_versions = Vec::new();
@@ -34,7 +34,7 @@ pub fn scan_vcs_repositories(repositories: &[RawRepository]) -> Vec<VcsPackageVe
let vcs_repo = VcsRepository::new(repo.url.clone(), forced_type, config.clone());
- match vcs_repo.scan() {
+ match vcs_repo.scan().await {
Ok(versions) => {
all_versions.extend(versions);
}
diff --git a/crates/mozart-vcs/src/driver/bitbucket.rs b/crates/mozart-vcs/src/driver/bitbucket.rs
index 9a0fc15..dc7d2cf 100644
--- a/crates/mozart-vcs/src/driver/bitbucket.rs
+++ b/crates/mozart-vcs/src/driver/bitbucket.rs
@@ -62,8 +62,7 @@ impl BitbucketDriver {
)
}
- fn api_get(&self, path: &str) -> Result<serde_json::Value> {
- let handle = tokio::runtime::Handle::current();
+ async fn api_get(&self, path: &str) -> Result<serde_json::Value> {
let url = self.api_url(path);
let mut req = self
.http_client
@@ -76,7 +75,7 @@ impl BitbucketDriver {
req = req.header(AUTHORIZATION, format!("Basic {credentials}"));
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if !response.status().is_success() {
bail!(
"Bitbucket API request to {} failed: {}",
@@ -84,11 +83,10 @@ impl BitbucketDriver {
response.status()
);
}
- Ok(handle.block_on(response.json())?)
+ Ok(response.json().await?)
}
- fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
- let handle = tokio::runtime::Handle::current();
+ async fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
let mut items = Vec::new();
let mut next_url = Some(self.api_url(path));
let mut pages = 0;
@@ -102,11 +100,11 @@ impl BitbucketDriver {
if let Some((key, secret)) = &self.config.bitbucket_oauth {
req = req.header(AUTHORIZATION, format!("Basic {key}:{secret}"));
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if !response.status().is_success() {
break;
}
- let data: serde_json::Value = handle.block_on(response.json())?;
+ let data: serde_json::Value = response.json().await?;
if let Some(values) = data["values"].as_array() {
items.extend(values.iter().cloned());
}
@@ -119,11 +117,11 @@ impl BitbucketDriver {
Ok(items)
}
- fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
+ async fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
if self.git_driver.is_none() {
let git_url = format!("https://bitbucket.org/{}/{}.git", self.owner, self.repo);
let mut driver = GitDriver::new(&git_url, self.config.clone());
- driver.initialize()?;
+ driver.initialize().await?;
self.git_driver = Some(Box::new(driver));
}
Ok(self.git_driver.as_mut().unwrap())
@@ -131,8 +129,8 @@ impl BitbucketDriver {
}
impl VcsDriver for BitbucketDriver {
- fn initialize(&mut self) -> Result<()> {
- match self.api_get("") {
+ async fn initialize(&mut self) -> Result<()> {
+ match self.api_get("").await {
Ok(data) => {
if let Some(scm) = data["scm"].as_str() {
self.vcs_type = scm.to_string();
@@ -145,7 +143,7 @@ impl VcsDriver for BitbucketDriver {
}
Err(_) => {
self.api_failed = true;
- let driver = self.use_git_fallback()?;
+ let driver = self.use_git_fallback().await?;
self.root_identifier = Some(driver.root_identifier().to_string());
}
}
@@ -156,14 +154,14 @@ impl VcsDriver for BitbucketDriver {
self.root_identifier.as_deref().unwrap_or("main")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let branches = driver.branches()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let branches = driver.branches().await?.clone();
self.branches = Some(branches);
} else {
- let items = self.api_get_paginated("/refs/branches?pagelen=100")?;
+ let items = self.api_get_paginated("/refs/branches?pagelen=100").await?;
let mut branches = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -178,14 +176,14 @@ impl VcsDriver for BitbucketDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let tags = driver.tags()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let tags = driver.tags().await?.clone();
self.tags = Some(tags);
} else {
- let items = self.api_get_paginated("/refs/tags?pagelen=100")?;
+ let items = self.api_get_paginated("/refs/tags?pagelen=100").await?;
let mut tags = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -200,46 +198,48 @@ impl VcsDriver for BitbucketDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = content.and_then(|c| serde_json::from_str(&c).ok());
self.info_cache
.insert(identifier.to_string(), value.clone());
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
- let handle = tokio::runtime::Handle::current();
let url = self.api_url(&format!("/src/{identifier}/{file}"));
let mut req = self.http_client.get(&url).header(USER_AGENT, "mozart/0.1");
if let Some((key, secret)) = &self.config.bitbucket_oauth {
req = req.header(AUTHORIZATION, format!("Basic {key}:{secret}"));
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if response.status().is_success() {
- Ok(Some(handle.block_on(response.text())?))
+ Ok(Some(response.text().await?))
} else {
Ok(None)
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
- match self.api_get(&format!("/commit/{identifier}")) {
+ match self.api_get(&format!("/commit/{identifier}")).await {
Ok(data) => Ok(data["date"].as_str().map(|s| s.to_string())),
Err(_) => Ok(None),
}
}
- fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
Ok(Some(DistReference {
dist_type: "zip".to_string(),
url: format!(
@@ -263,9 +263,9 @@ impl VcsDriver for BitbucketDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
if let Some(driver) = &mut self.git_driver {
- driver.cleanup()?;
+ driver.cleanup().await?;
}
Ok(())
}
diff --git a/crates/mozart-vcs/src/driver/forgejo.rs b/crates/mozart-vcs/src/driver/forgejo.rs
index f35f9df..0447422 100644
--- a/crates/mozart-vcs/src/driver/forgejo.rs
+++ b/crates/mozart-vcs/src/driver/forgejo.rs
@@ -76,8 +76,7 @@ impl ForgejoDriver {
)
}
- fn api_get(&self, path: &str) -> Result<serde_json::Value> {
- let handle = tokio::runtime::Handle::current();
+ async fn api_get(&self, path: &str) -> Result<serde_json::Value> {
let url = self.api_url(path);
let mut req = self
.http_client
@@ -87,7 +86,7 @@ impl ForgejoDriver {
if let Some(token) = &self.config.forgejo_token {
req = req.header(AUTHORIZATION, format!("token {token}"));
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if !response.status().is_success() {
bail!(
"Forgejo API request to {} failed: {}",
@@ -95,16 +94,16 @@ impl ForgejoDriver {
response.status()
);
}
- Ok(handle.block_on(response.json())?)
+ Ok(response.json().await?)
}
- fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
+ async fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
let mut items = Vec::new();
let mut page = 1;
loop {
let sep = if path.contains('?') { "&" } else { "?" };
let paged_path = format!("{path}{sep}limit=50&page={page}");
- let data = self.api_get(&paged_path)?;
+ let data = self.api_get(&paged_path).await?;
let batch: Vec<serde_json::Value> = match data {
serde_json::Value::Array(arr) => arr,
_ => break,
@@ -121,14 +120,14 @@ impl ForgejoDriver {
Ok(items)
}
- fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
+ async fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
if self.git_driver.is_none() {
let git_url = format!(
"{}://{}/{}/{}.git",
self.scheme, self.host, self.owner, self.repo
);
let mut driver = GitDriver::new(&git_url, self.config.clone());
- driver.initialize()?;
+ driver.initialize().await?;
self.git_driver = Some(Box::new(driver));
}
Ok(self.git_driver.as_mut().unwrap())
@@ -136,8 +135,8 @@ impl ForgejoDriver {
}
impl VcsDriver for ForgejoDriver {
- fn initialize(&mut self) -> Result<()> {
- match self.api_get("") {
+ async fn initialize(&mut self) -> Result<()> {
+ match self.api_get("").await {
Ok(data) => {
let default_branch = data["default_branch"]
.as_str()
@@ -147,7 +146,7 @@ impl VcsDriver for ForgejoDriver {
}
Err(_) => {
self.api_failed = true;
- let driver = self.use_git_fallback()?;
+ let driver = self.use_git_fallback().await?;
self.root_identifier = Some(driver.root_identifier().to_string());
}
}
@@ -158,14 +157,14 @@ impl VcsDriver for ForgejoDriver {
self.root_identifier.as_deref().unwrap_or("main")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let branches = driver.branches()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let branches = driver.branches().await?.clone();
self.branches = Some(branches);
} else {
- let items = self.api_get_paginated("/branches")?;
+ let items = self.api_get_paginated("/branches").await?;
let mut branches = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -180,14 +179,14 @@ impl VcsDriver for ForgejoDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let tags = driver.tags()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let tags = driver.tags().await?.clone();
self.tags = Some(tags);
} else {
- let items = self.api_get_paginated("/tags")?;
+ let items = self.api_get_paginated("/tags").await?;
let mut tags = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) = (
@@ -203,23 +202,26 @@ impl VcsDriver for ForgejoDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = content.and_then(|c| serde_json::from_str(&c).ok());
self.info_cache
.insert(identifier.to_string(), value.clone());
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
let path = format!("/contents/{}?ref={}", file, identifier);
- match self.api_get(&path) {
+ match self.api_get(&path).await {
Ok(data) => {
if let Some(content) = data["content"].as_str() {
// Forgejo returns base64-encoded content
@@ -233,17 +235,17 @@ impl VcsDriver for ForgejoDriver {
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
- match self.api_get(&format!("/git/commits/{identifier}")) {
+ match self.api_get(&format!("/git/commits/{identifier}")).await {
Ok(data) => Ok(data["created"].as_str().map(|s| s.to_string())),
Err(_) => Ok(None),
}
}
- fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
Ok(Some(DistReference {
dist_type: "zip".to_string(),
url: format!(
@@ -270,9 +272,9 @@ impl VcsDriver for ForgejoDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
if let Some(driver) = &mut self.git_driver {
- driver.cleanup()?;
+ driver.cleanup().await?;
}
Ok(())
}
diff --git a/crates/mozart-vcs/src/driver/git.rs b/crates/mozart-vcs/src/driver/git.rs
index 13fc243..cc9a210 100644
--- a/crates/mozart-vcs/src/driver/git.rs
+++ b/crates/mozart-vcs/src/driver/git.rs
@@ -131,7 +131,7 @@ impl GitDriver {
}
impl VcsDriver for GitDriver {
- fn initialize(&mut self) -> Result<()> {
+ async fn initialize(&mut self) -> Result<()> {
if self.is_local {
// Local repo: use directly (or its .git subdir)
let path = Path::new(&self.url);
@@ -174,7 +174,7 @@ impl VcsDriver for GitDriver {
self.root_identifier.as_deref().unwrap_or("master")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
let repo_dir = self.get_repo_dir()?.to_path_buf();
let process = ProcessExecutor::new();
@@ -187,7 +187,7 @@ impl VcsDriver for GitDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
let repo_dir = self.get_repo_dir()?.to_path_buf();
let process = ProcessExecutor::new();
@@ -204,12 +204,15 @@ impl VcsDriver for GitDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = match content {
Some(c) => serde_json::from_str(&c).ok(),
None => None,
@@ -220,7 +223,7 @@ impl VcsDriver for GitDriver {
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
let repo_dir = self.get_repo_dir()?;
let process = ProcessExecutor::new();
let resource = format!("{identifier}:{file}");
@@ -232,7 +235,7 @@ impl VcsDriver for GitDriver {
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
let repo_dir = self.get_repo_dir()?;
let process = ProcessExecutor::new();
let output = process.execute(
@@ -251,7 +254,7 @@ impl VcsDriver for GitDriver {
}
}
- fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> {
// Plain git repos don't provide dist archives
Ok(None)
}
@@ -268,7 +271,7 @@ impl VcsDriver for GitDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
Ok(())
}
}
diff --git a/crates/mozart-vcs/src/driver/github.rs b/crates/mozart-vcs/src/driver/github.rs
index 23eaa8a..724cb35 100644
--- a/crates/mozart-vcs/src/driver/github.rs
+++ b/crates/mozart-vcs/src/driver/github.rs
@@ -65,8 +65,7 @@ impl GitHubDriver {
)
}
- fn api_get(&self, path: &str) -> Result<serde_json::Value> {
- let handle = tokio::runtime::Handle::current();
+ async fn api_get(&self, path: &str) -> Result<serde_json::Value> {
let url = self.api_url(path);
let mut req = self
.http_client
@@ -78,7 +77,7 @@ impl GitHubDriver {
req = req.header(AUTHORIZATION, format!("token {token}"));
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if !response.status().is_success() {
bail!(
"GitHub API request to {} failed with status {}",
@@ -86,11 +85,10 @@ impl GitHubDriver {
response.status()
);
}
- Ok(handle.block_on(response.json())?)
+ Ok(response.json().await?)
}
- fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
- let handle = tokio::runtime::Handle::current();
+ async fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
let mut items = Vec::new();
let mut page = 1;
loop {
@@ -108,12 +106,12 @@ impl GitHubDriver {
req = req.header(AUTHORIZATION, format!("token {token}"));
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if !response.status().is_success() {
bail!("GitHub API paginated request failed: {}", response.status());
}
- let batch: Vec<serde_json::Value> = handle.block_on(response.json())?;
+ let batch: Vec<serde_json::Value> = response.json().await?;
if batch.is_empty() {
break;
}
@@ -127,11 +125,11 @@ impl GitHubDriver {
Ok(items)
}
- fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
+ async fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
if self.git_driver.is_none() {
let git_url = format!("https://github.com/{}/{}.git", self.owner, self.repo);
let mut driver = GitDriver::new(&git_url, self.config.clone());
- driver.initialize()?;
+ driver.initialize().await?;
self.git_driver = Some(Box::new(driver));
}
Ok(self.git_driver.as_mut().unwrap())
@@ -139,9 +137,9 @@ impl GitHubDriver {
}
impl VcsDriver for GitHubDriver {
- fn initialize(&mut self) -> Result<()> {
+ async fn initialize(&mut self) -> Result<()> {
// Try to fetch repo data from API
- match self.api_get("") {
+ match self.api_get("").await {
Ok(data) => {
let default_branch = data["default_branch"]
.as_str()
@@ -152,7 +150,7 @@ impl VcsDriver for GitHubDriver {
}
Err(_) => {
self.api_failed = true;
- let driver = self.use_git_fallback()?;
+ let driver = self.use_git_fallback().await?;
self.root_identifier = Some(driver.root_identifier().to_string());
}
}
@@ -163,14 +161,14 @@ impl VcsDriver for GitHubDriver {
self.root_identifier.as_deref().unwrap_or("main")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let branches = driver.branches()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let branches = driver.branches().await?.clone();
self.branches = Some(branches);
} else {
- let items = self.api_get_paginated("/branches")?;
+ let items = self.api_get_paginated("/branches").await?;
let mut branches = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -185,14 +183,14 @@ impl VcsDriver for GitHubDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let tags = driver.tags()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let tags = driver.tags().await?.clone();
self.tags = Some(tags);
} else {
- let items = self.api_get_paginated("/tags")?;
+ let items = self.api_get_paginated("/tags").await?;
let mut tags = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -207,12 +205,15 @@ impl VcsDriver for GitHubDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = match content {
Some(c) => serde_json::from_str(&c).ok(),
None => None,
@@ -223,7 +224,7 @@ impl VcsDriver for GitHubDriver {
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
// Can't use API, would need git fallback
// For simplicity, return None (git_driver is mutable)
@@ -231,7 +232,7 @@ impl VcsDriver for GitHubDriver {
}
let path = format!("/contents/{}?ref={}", file, identifier);
- match self.api_get(&path) {
+ match self.api_get(&path).await {
Ok(data) => {
if let Some(content) = data["content"].as_str() {
// GitHub returns base64-encoded content
@@ -245,13 +246,13 @@ impl VcsDriver for GitHubDriver {
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
let path = format!("/commits/{}", identifier);
- match self.api_get(&path) {
+ match self.api_get(&path).await {
Ok(data) => {
let date = data["commit"]["committer"]["date"]
.as_str()
@@ -262,7 +263,7 @@ impl VcsDriver for GitHubDriver {
}
}
- fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
Ok(Some(DistReference {
dist_type: "zip".to_string(),
url: format!(
@@ -286,9 +287,9 @@ impl VcsDriver for GitHubDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
if let Some(driver) = &mut self.git_driver {
- driver.cleanup()?;
+ driver.cleanup().await?;
}
Ok(())
}
diff --git a/crates/mozart-vcs/src/driver/gitlab.rs b/crates/mozart-vcs/src/driver/gitlab.rs
index ed88f27..7b1a93b 100644
--- a/crates/mozart-vcs/src/driver/gitlab.rs
+++ b/crates/mozart-vcs/src/driver/gitlab.rs
@@ -80,8 +80,7 @@ impl GitLabDriver {
)
}
- fn api_get(&self, path: &str) -> Result<serde_json::Value> {
- let handle = tokio::runtime::Handle::current();
+ async fn api_get(&self, path: &str) -> Result<serde_json::Value> {
let url = self.api_url(path);
let mut req = self
.http_client
@@ -93,7 +92,7 @@ impl GitLabDriver {
req = req.header("PRIVATE-TOKEN", token.as_str());
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if !response.status().is_success() {
bail!(
"GitLab API request to {} failed with status {}",
@@ -101,16 +100,16 @@ impl GitLabDriver {
response.status()
);
}
- Ok(handle.block_on(response.json())?)
+ Ok(response.json().await?)
}
- fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
+ async fn api_get_paginated(&self, path: &str) -> Result<Vec<serde_json::Value>> {
let mut items = Vec::new();
let mut page = 1;
loop {
let sep = if path.contains('?') { "&" } else { "?" };
let paged_path = format!("{path}{sep}per_page=100&page={page}");
- let data = self.api_get(&paged_path)?;
+ let data = self.api_get(&paged_path).await?;
let batch: Vec<serde_json::Value> = match data {
serde_json::Value::Array(arr) => arr,
_ => break,
@@ -127,14 +126,14 @@ impl GitLabDriver {
Ok(items)
}
- fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
+ async fn use_git_fallback(&mut self) -> Result<&mut GitDriver> {
if self.git_driver.is_none() {
let git_url = format!(
"{}://{}/{}/{}.git",
self.scheme, self.host, self.owner, self.repo
);
let mut driver = GitDriver::new(&git_url, self.config.clone());
- driver.initialize()?;
+ driver.initialize().await?;
self.git_driver = Some(Box::new(driver));
}
Ok(self.git_driver.as_mut().unwrap())
@@ -142,8 +141,8 @@ impl GitLabDriver {
}
impl VcsDriver for GitLabDriver {
- fn initialize(&mut self) -> Result<()> {
- match self.api_get("") {
+ async fn initialize(&mut self) -> Result<()> {
+ match self.api_get("").await {
Ok(data) => {
if let Some(id) = data["id"].as_u64() {
self.project_id = Some(id.to_string());
@@ -156,7 +155,7 @@ impl VcsDriver for GitLabDriver {
}
Err(_) => {
self.api_failed = true;
- let driver = self.use_git_fallback()?;
+ let driver = self.use_git_fallback().await?;
self.root_identifier = Some(driver.root_identifier().to_string());
}
}
@@ -167,14 +166,14 @@ impl VcsDriver for GitLabDriver {
self.root_identifier.as_deref().unwrap_or("main")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let branches = driver.branches()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let branches = driver.branches().await?.clone();
self.branches = Some(branches);
} else {
- let items = self.api_get_paginated("/repository/branches")?;
+ let items = self.api_get_paginated("/repository/branches").await?;
let mut branches = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -189,14 +188,14 @@ impl VcsDriver for GitLabDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
if self.api_failed {
- let driver = self.use_git_fallback()?;
- let tags = driver.tags()?.clone();
+ let driver = self.use_git_fallback().await?;
+ let tags = driver.tags().await?.clone();
self.tags = Some(tags);
} else {
- let items = self.api_get_paginated("/repository/tags")?;
+ let items = self.api_get_paginated("/repository/tags").await?;
let mut tags = BTreeMap::new();
for item in items {
if let (Some(name), Some(sha)) =
@@ -211,22 +210,24 @@ impl VcsDriver for GitLabDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = content.and_then(|c| serde_json::from_str(&c).ok());
self.info_cache
.insert(identifier.to_string(), value.clone());
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
- let handle = tokio::runtime::Handle::current();
let encoded_file = file.replace('/', "%2F");
let path = format!("/repository/files/{}/raw?ref={}", encoded_file, identifier);
let url = self.api_url(&path);
@@ -234,25 +235,28 @@ impl VcsDriver for GitLabDriver {
if let Some(token) = &self.config.gitlab_token {
req = req.header("PRIVATE-TOKEN", token.as_str());
}
- let response = handle.block_on(req.send())?;
+ let response = req.send().await?;
if response.status().is_success() {
- Ok(Some(handle.block_on(response.text())?))
+ Ok(Some(response.text().await?))
} else {
Ok(None)
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
if self.api_failed {
return Ok(None);
}
- match self.api_get(&format!("/repository/commits/{identifier}")) {
+ match self
+ .api_get(&format!("/repository/commits/{identifier}"))
+ .await
+ {
Ok(data) => Ok(data["committed_date"].as_str().map(|s| s.to_string())),
Err(_) => Ok(None),
}
}
- fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
Ok(Some(DistReference {
dist_type: "zip".to_string(),
url: format!(
@@ -284,9 +288,9 @@ impl VcsDriver for GitLabDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
if let Some(driver) = &mut self.git_driver {
- driver.cleanup()?;
+ driver.cleanup().await?;
}
Ok(())
}
diff --git a/crates/mozart-vcs/src/driver/hg.rs b/crates/mozart-vcs/src/driver/hg.rs
index 7bfb07e..f884c50 100644
--- a/crates/mozart-vcs/src/driver/hg.rs
+++ b/crates/mozart-vcs/src/driver/hg.rs
@@ -49,7 +49,7 @@ impl HgDriver {
}
impl VcsDriver for HgDriver {
- fn initialize(&mut self) -> Result<()> {
+ async fn initialize(&mut self) -> Result<()> {
let cache_dir = self.config.cache_dir.join("hg");
std::fs::create_dir_all(&cache_dir)?;
let repo_dir = cache_dir.join(crate::util::git::GitUtil::sanitize_url(&self.url));
@@ -83,7 +83,7 @@ impl VcsDriver for HgDriver {
self.root_identifier.as_deref().unwrap_or("default")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
let repo_dir = self.get_repo_dir()?.clone();
let mut branches = BTreeMap::new();
@@ -121,7 +121,7 @@ impl VcsDriver for HgDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
let repo_dir = self.get_repo_dir()?.clone();
let output = self.hg_util.execute(&["tags", "-q"], Some(&repo_dir))?;
@@ -142,18 +142,21 @@ impl VcsDriver for HgDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = content.and_then(|c| serde_json::from_str(&c).ok());
self.info_cache
.insert(identifier.to_string(), value.clone());
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
let repo_dir = self.get_repo_dir()?;
let output = self
.hg_util
@@ -165,7 +168,7 @@ impl VcsDriver for HgDriver {
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
let repo_dir = self.get_repo_dir()?;
let output = self.hg_util.execute(
&["log", "-r", identifier, "--template", "{date|isodatesec}"],
@@ -179,7 +182,7 @@ impl VcsDriver for HgDriver {
}
}
- fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> {
Ok(None)
}
@@ -195,7 +198,7 @@ impl VcsDriver for HgDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
Ok(())
}
}
diff --git a/crates/mozart-vcs/src/driver/mod.rs b/crates/mozart-vcs/src/driver/mod.rs
index f8e26ef..7a132ed 100644
--- a/crates/mozart-vcs/src/driver/mod.rs
+++ b/crates/mozart-vcs/src/driver/mod.rs
@@ -79,30 +79,31 @@ pub enum DriverType {
/// The VCS driver interface.
///
/// Corresponds to Composer's `VcsDriverInterface`.
-pub trait VcsDriver {
+trait VcsDriver {
/// Initialize the driver (e.g., clone mirror, fetch API metadata).
- fn initialize(&mut self) -> Result<()>;
+ async fn initialize(&mut self) -> Result<()>;
/// The root identifier (default branch/trunk).
fn root_identifier(&self) -> &str;
/// All branches as `name -> commit_hash`.
- fn branches(&mut self) -> Result<&BTreeMap<String, String>>;
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>>;
/// All tags as `name -> commit_hash`.
- fn tags(&mut self) -> Result<&BTreeMap<String, String>>;
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>>;
/// Get composer.json content parsed as JSON for a given identifier.
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>>;
+ async fn composer_information(&mut self, identifier: &str)
+ -> Result<Option<serde_json::Value>>;
/// Get raw file content at a given path and identifier.
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>>;
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>>;
/// Get the change date for a given identifier (ISO 8601).
- fn change_date(&self, identifier: &str) -> Result<Option<String>>;
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>>;
/// Get the dist reference for a given identifier.
- fn dist(&self, identifier: &str) -> Result<Option<DistReference>>;
+ async fn dist(&self, identifier: &str) -> Result<Option<DistReference>>;
/// Get the source reference for a given identifier.
fn source(&self, identifier: &str) -> SourceReference;
@@ -111,7 +112,97 @@ pub trait VcsDriver {
fn url(&self) -> &str;
/// Clean up resources (temp dirs, etc.).
- fn cleanup(&mut self) -> Result<()>;
+ async fn cleanup(&mut self) -> Result<()>;
+}
+
+/// Enum-dispatched VCS driver.
+///
+/// Wraps all concrete driver types to allow static dispatch with async trait methods.
+pub enum AnyVcsDriver {
+ GitHub(github::GitHubDriver),
+ GitLab(gitlab::GitLabDriver),
+ Bitbucket(bitbucket::BitbucketDriver),
+ Forgejo(forgejo::ForgejoDriver),
+ Git(git::GitDriver),
+ Svn(svn::SvnDriver),
+ Hg(hg::HgDriver),
+}
+
+macro_rules! dispatch {
+ ($self:expr, $method:ident $(, $arg:expr)*) => {
+ match $self {
+ AnyVcsDriver::GitHub(d) => d.$method($($arg),*),
+ AnyVcsDriver::GitLab(d) => d.$method($($arg),*),
+ AnyVcsDriver::Bitbucket(d) => d.$method($($arg),*),
+ AnyVcsDriver::Forgejo(d) => d.$method($($arg),*),
+ AnyVcsDriver::Git(d) => d.$method($($arg),*),
+ AnyVcsDriver::Svn(d) => d.$method($($arg),*),
+ AnyVcsDriver::Hg(d) => d.$method($($arg),*),
+ }
+ };
+}
+
+macro_rules! dispatch_async {
+ ($self:expr, $method:ident $(, $arg:expr)*) => {
+ match $self {
+ AnyVcsDriver::GitHub(d) => d.$method($($arg),*).await,
+ AnyVcsDriver::GitLab(d) => d.$method($($arg),*).await,
+ AnyVcsDriver::Bitbucket(d) => d.$method($($arg),*).await,
+ AnyVcsDriver::Forgejo(d) => d.$method($($arg),*).await,
+ AnyVcsDriver::Git(d) => d.$method($($arg),*).await,
+ AnyVcsDriver::Svn(d) => d.$method($($arg),*).await,
+ AnyVcsDriver::Hg(d) => d.$method($($arg),*).await,
+ }
+ };
+}
+
+impl AnyVcsDriver {
+ pub async fn initialize(&mut self) -> Result<()> {
+ dispatch_async!(self, initialize)
+ }
+
+ pub fn root_identifier(&self) -> &str {
+ dispatch!(self, root_identifier)
+ }
+
+ pub async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ dispatch_async!(self, branches)
+ }
+
+ pub async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ dispatch_async!(self, tags)
+ }
+
+ pub async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
+ dispatch_async!(self, composer_information, identifier)
+ }
+
+ pub async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ dispatch_async!(self, file_content, file, identifier)
+ }
+
+ pub async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ dispatch_async!(self, change_date, identifier)
+ }
+
+ pub async fn dist(&self, identifier: &str) -> Result<Option<DistReference>> {
+ dispatch_async!(self, dist, identifier)
+ }
+
+ pub fn source(&self, identifier: &str) -> SourceReference {
+ dispatch!(self, source, identifier)
+ }
+
+ pub fn url(&self) -> &str {
+ dispatch!(self, url)
+ }
+
+ pub async fn cleanup(&mut self) -> Result<()> {
+ dispatch_async!(self, cleanup)
+ }
}
/// Detect which driver type should handle a given URL.
@@ -182,18 +273,16 @@ pub fn detect_driver(
}
/// Create a driver instance for the given URL and type.
-pub fn create_driver(
- url: &str,
- driver_type: DriverType,
- config: DriverConfig,
-) -> Box<dyn VcsDriver> {
+pub fn create_driver(url: &str, driver_type: DriverType, config: DriverConfig) -> AnyVcsDriver {
match driver_type {
- DriverType::GitHub => Box::new(github::GitHubDriver::new(url, config)),
- DriverType::GitLab => Box::new(gitlab::GitLabDriver::new(url, config)),
- DriverType::Bitbucket => Box::new(bitbucket::BitbucketDriver::new(url, config)),
- DriverType::Forgejo => Box::new(forgejo::ForgejoDriver::new(url, config)),
- DriverType::Git => Box::new(git::GitDriver::new(url, config)),
- DriverType::Svn => Box::new(svn::SvnDriver::new(url, config)),
- DriverType::Hg => Box::new(hg::HgDriver::new(url, config)),
+ DriverType::GitHub => AnyVcsDriver::GitHub(github::GitHubDriver::new(url, config)),
+ DriverType::GitLab => AnyVcsDriver::GitLab(gitlab::GitLabDriver::new(url, config)),
+ DriverType::Bitbucket => {
+ AnyVcsDriver::Bitbucket(bitbucket::BitbucketDriver::new(url, config))
+ }
+ DriverType::Forgejo => AnyVcsDriver::Forgejo(forgejo::ForgejoDriver::new(url, config)),
+ DriverType::Git => AnyVcsDriver::Git(git::GitDriver::new(url, config)),
+ DriverType::Svn => AnyVcsDriver::Svn(svn::SvnDriver::new(url, config)),
+ DriverType::Hg => AnyVcsDriver::Hg(hg::HgDriver::new(url, config)),
}
}
diff --git a/crates/mozart-vcs/src/driver/svn.rs b/crates/mozart-vcs/src/driver/svn.rs
index 8b47f75..eea2d08 100644
--- a/crates/mozart-vcs/src/driver/svn.rs
+++ b/crates/mozart-vcs/src/driver/svn.rs
@@ -74,7 +74,7 @@ impl SvnDriver {
}
impl VcsDriver for SvnDriver {
- fn initialize(&mut self) -> Result<()> {
+ async fn initialize(&mut self) -> Result<()> {
let info = self.svn_info(&self.url)?;
if let Some(url) = info["url"].as_str() {
self.base_url = url.to_string();
@@ -87,7 +87,7 @@ impl VcsDriver for SvnDriver {
self.root_identifier.as_deref().unwrap_or("HEAD")
}
- fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn branches(&mut self) -> Result<&BTreeMap<String, String>> {
if self.branches.is_none() {
let mut branches = BTreeMap::new();
@@ -117,7 +117,7 @@ impl VcsDriver for SvnDriver {
Ok(self.branches.as_ref().unwrap())
}
- fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
+ async fn tags(&mut self) -> Result<&BTreeMap<String, String>> {
if self.tags.is_none() {
let mut tags = BTreeMap::new();
let tags_url = format!("{}/{}", self.base_url, self.tags_path);
@@ -136,18 +136,21 @@ impl VcsDriver for SvnDriver {
Ok(self.tags.as_ref().unwrap())
}
- fn composer_information(&mut self, identifier: &str) -> Result<Option<serde_json::Value>> {
+ async fn composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<serde_json::Value>> {
if let Some(cached) = self.info_cache.get(identifier) {
return Ok(cached.clone());
}
- let content = self.file_content("composer.json", identifier)?;
+ let content = self.file_content("composer.json", identifier).await?;
let value = content.and_then(|c| serde_json::from_str(&c).ok());
self.info_cache
.insert(identifier.to_string(), value.clone());
Ok(value)
}
- fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
+ async fn file_content(&self, file: &str, identifier: &str) -> Result<Option<String>> {
// identifier is either a path (trunk, branches/x, tags/y) or a revision number
let url = if identifier.contains('/') || identifier == "trunk" {
format!("{}/{}/{}", self.base_url, identifier, file)
@@ -164,7 +167,7 @@ impl VcsDriver for SvnDriver {
}
}
- fn change_date(&self, identifier: &str) -> Result<Option<String>> {
+ async fn change_date(&self, identifier: &str) -> Result<Option<String>> {
let url = if identifier.contains('/') || identifier == "trunk" {
format!("{}/{}", self.base_url, identifier)
} else {
@@ -176,7 +179,7 @@ impl VcsDriver for SvnDriver {
}
}
- fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> {
+ async fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> {
// SVN doesn't provide dist archives
Ok(None)
}
@@ -193,7 +196,7 @@ impl VcsDriver for SvnDriver {
&self.url
}
- fn cleanup(&mut self) -> Result<()> {
+ async fn cleanup(&mut self) -> Result<()> {
Ok(())
}
}
diff --git a/crates/mozart-vcs/src/repository.rs b/crates/mozart-vcs/src/repository.rs
index 14f2ceb..b941eec 100644
--- a/crates/mozart-vcs/src/repository.rs
+++ b/crates/mozart-vcs/src/repository.rs
@@ -50,17 +50,17 @@ impl VcsRepository {
/// 2. Reads composer.json from the root to get the package name
/// 3. Scans tags → version releases
/// 4. Scans branches → dev versions
- pub fn scan(&self) -> Result<Vec<VcsPackageVersion>> {
+ pub async fn scan(&self) -> Result<Vec<VcsPackageVersion>> {
let driver_type = self
.driver_type
.ok_or_else(|| anyhow::anyhow!("No suitable VCS driver found for URL: {}", self.url))?;
let mut driver = create_driver(&self.url, driver_type, self.config.clone());
- driver.initialize()?;
+ driver.initialize().await?;
// Get package name from root composer.json
let root_id = driver.root_identifier().to_string();
- let root_info = driver.composer_information(&root_id)?;
+ let root_info = driver.composer_information(&root_id).await?;
let package_name = match &root_info {
Some(info) => info["name"]
.as_str()
@@ -81,14 +81,14 @@ impl VcsRepository {
let mut versions = Vec::new();
// Scan tags
- let tags = driver.tags()?.clone();
+ let tags = driver.tags().await?.clone();
for (tag_name, tag_hash) in &tags {
if let Some(version) = self.tag_to_version(tag_name) {
- match driver.composer_information(tag_hash) {
+ match driver.composer_information(tag_hash).await {
Ok(Some(info)) => {
- let time = driver.change_date(tag_hash).unwrap_or(None);
+ let time = driver.change_date(tag_hash).await.unwrap_or(None);
let source = driver.source(tag_hash);
- let dist = driver.dist(tag_hash).unwrap_or(None);
+ let dist = driver.dist(tag_hash).await.unwrap_or(None);
// Ensure name matches root package
if info["name"].as_str() != Some(&package_name) {
@@ -114,18 +114,18 @@ impl VcsRepository {
}
// Scan branches
- let branches = driver.branches()?.clone();
+ let branches = driver.branches().await?.clone();
let default_branch = driver.root_identifier().to_string();
for (branch_name, branch_hash) in &branches {
- match driver.composer_information(branch_hash) {
+ match driver.composer_information(branch_hash).await {
Ok(Some(info)) => {
if info["name"].as_str() != Some(&package_name) {
continue;
}
- let time = driver.change_date(branch_hash).unwrap_or(None);
+ let time = driver.change_date(branch_hash).await.unwrap_or(None);
let source = driver.source(branch_hash);
- let dist = driver.dist(branch_hash).unwrap_or(None);
+ let dist = driver.dist(branch_hash).await.unwrap_or(None);
let is_default = branch_name == &default_branch;
let version = self.branch_to_version(branch_name);
@@ -154,7 +154,7 @@ impl VcsRepository {
}
}
- driver.cleanup()?;
+ driver.cleanup().await?;
Ok(versions)
}
diff --git a/crates/mozart-vcs/tests/git_driver_test.rs b/crates/mozart-vcs/tests/git_driver_test.rs
index 1fafc7c..a8f0ce7 100644
--- a/crates/mozart-vcs/tests/git_driver_test.rs
+++ b/crates/mozart-vcs/tests/git_driver_test.rs
@@ -5,8 +5,7 @@ use tempfile::TempDir;
use mozart_vcs::downloader::VcsDownloader;
use mozart_vcs::downloader::git::GitDownloader;
-use mozart_vcs::driver::git::GitDriver;
-use mozart_vcs::driver::{DriverConfig, VcsDriver};
+use mozart_vcs::driver::{DriverConfig, DriverType, create_driver};
use mozart_vcs::process::ProcessExecutor;
use mozart_vcs::util::git::GitUtil;
@@ -66,8 +65,8 @@ fn create_test_repo(dir: &Path) {
run(&["git", "checkout", "main"]);
}
-#[test]
-fn test_git_driver_local_repo() {
+#[tokio::test]
+async fn test_git_driver_local_repo() {
if !has_git() {
eprintln!("Skipping test: git not available");
return;
@@ -82,13 +81,13 @@ fn test_git_driver_local_repo() {
..DriverConfig::default()
};
- let mut driver = GitDriver::new(repo_dir.path().to_str().unwrap(), config);
+ let mut driver = create_driver(repo_dir.path().to_str().unwrap(), DriverType::Git, config);
- driver.initialize().unwrap();
+ driver.initialize().await.unwrap();
assert_eq!(driver.root_identifier(), "main");
// Check tags
- let tags = driver.tags().unwrap().clone();
+ let tags = driver.tags().await.unwrap().clone();
assert!(
tags.contains_key("v1.0.0"),
"Missing tag v1.0.0: {:?}",
@@ -101,7 +100,7 @@ fn test_git_driver_local_repo() {
);
// Check branches
- let branches = driver.branches().unwrap().clone();
+ let branches = driver.branches().await.unwrap().clone();
assert!(
branches.contains_key("main"),
"Missing branch main: {:?}",
@@ -115,25 +114,28 @@ fn test_git_driver_local_repo() {
// Read composer.json
let tag_hash = &tags["v1.0.0"];
- let info = driver.composer_information(tag_hash).unwrap();
+ let info = driver.composer_information(tag_hash).await.unwrap();
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info["name"].as_str(), Some("test/package"));
// Read file content
- let content = driver.file_content("composer.json", tag_hash).unwrap();
+ let content = driver
+ .file_content("composer.json", tag_hash)
+ .await
+ .unwrap();
assert!(content.is_some());
assert!(content.unwrap().contains("test/package"));
// Change date
- let date = driver.change_date(tag_hash).unwrap();
+ let date = driver.change_date(tag_hash).await.unwrap();
assert!(date.is_some());
// Source reference
let source = driver.source(tag_hash);
assert_eq!(source.source_type, "git");
- driver.cleanup().unwrap();
+ driver.cleanup().await.unwrap();
}
#[test]
@@ -223,8 +225,8 @@ fn test_detect_driver() {
);
}
-#[test]
-fn test_vcs_repository_scan() {
+#[tokio::test]
+async fn test_vcs_repository_scan() {
if !has_git() {
eprintln!("Skipping test: git not available");
return;
@@ -245,7 +247,7 @@ fn test_vcs_repository_scan() {
config,
);
- let versions = repo.scan().unwrap();
+ let versions = repo.scan().await.unwrap();
assert!(!versions.is_empty(), "No versions found");
// Should find tag versions