diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-core/src/vcs/driver/git.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src/vcs/driver/git.rs')
| -rw-r--r-- | crates/mozart-core/src/vcs/driver/git.rs | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/crates/mozart-core/src/vcs/driver/git.rs b/crates/mozart-core/src/vcs/driver/git.rs new file mode 100644 index 0000000..7d6643f --- /dev/null +++ b/crates/mozart-core/src/vcs/driver/git.rs @@ -0,0 +1,275 @@ +use super::super::process::ProcessExecutor; +use super::super::util::git::GitUtil; +use super::{DistReference, DriverConfig, SourceReference, VcsDriver}; +use anyhow::Result; +use indexmap::IndexMap; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +/// Git VCS driver. +/// +/// Corresponds to Composer's `Repository\Vcs\GitDriver`. +pub struct GitDriver { + url: String, + repo_dir: Option<PathBuf>, + root_identifier: Option<String>, + tags: Option<BTreeMap<String, String>>, + branches: Option<BTreeMap<String, String>>, + info_cache: IndexMap<String, Option<serde_json::Value>>, + git_util: GitUtil, + is_local: bool, +} + +impl GitDriver { + pub fn new(url: &str, config: DriverConfig) -> Self { + let is_local = Self::is_local_path(url); + let process = ProcessExecutor::new(); + let git_util = GitUtil::new(process, config.cache_vcs_dir.clone()); + Self { + url: url.to_string(), + repo_dir: if is_local { + Some(PathBuf::from(url)) + } else { + None + }, + root_identifier: None, + tags: None, + branches: None, + info_cache: IndexMap::new(), + git_util, + is_local, + } + } + + /// Check if a URL is supported by the Git driver. + pub fn supports(url: &str) -> bool { + if Self::is_local_path(url) { + return Path::new(url).join(".git").is_dir() || url.ends_with(".git"); + } + url.starts_with("git://") + || url.starts_with("git@") + || url.ends_with(".git") + || url.contains("git.") + } + + fn is_local_path(url: &str) -> bool { + !url.contains("://") && !url.starts_with("git@") && Path::new(url).exists() + } + + fn get_repo_dir(&self) -> Result<&Path> { + self.repo_dir + .as_deref() + .ok_or_else(|| anyhow::anyhow!("GitDriver not initialized")) + } + + fn parse_branches(output: &str) -> BTreeMap<String, String> { + let mut branches = BTreeMap::new(); + for line in output.lines() { + let line = line.trim(); + if line.is_empty() || line.contains("HEAD detached") || line.contains("->") { + continue; + } + // Remove leading "* " for current branch + let line = line.strip_prefix("* ").unwrap_or(line); + // Format: "branch_name commit_hash ..." + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + branches.insert(parts[0].to_string(), parts[1].to_string()); + } + } + branches + } + + fn parse_tags(output: &str) -> BTreeMap<String, String> { + let mut tags = BTreeMap::new(); + // First pass: collect dereferenced tags (^{}) + let mut dereferenced = IndexMap::new(); + for line in output.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + // Format: "commit_hash refs/tags/tag_name" or "commit_hash refs/tags/tag_name^{}" + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let hash = parts[0]; + let refname = parts[1]; + if let Some(tag_name) = refname.strip_prefix("refs/tags/") + && let Some(tag_name) = tag_name.strip_suffix("^{}") + { + // Dereferenced tag - this is the actual commit + dereferenced.insert(tag_name.to_string(), hash.to_string()); + } + } + } + // Second pass: collect all tags, preferring dereferenced values + for line in output.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let hash = parts[0]; + let refname = parts[1]; + if let Some(tag_name) = refname.strip_prefix("refs/tags/") { + if tag_name.ends_with("^{}") { + continue; // Skip dereferenced entries themselves + } + let resolved = dereferenced + .get(tag_name) + .cloned() + .unwrap_or_else(|| hash.to_string()); + tags.insert(tag_name.to_string(), resolved); + } + } + } + tags + } +} + +impl VcsDriver for GitDriver { + async fn initialize(&mut self) -> Result<()> { + if self.is_local { + // Local repo: use directly (or its .git subdir) + let path = Path::new(&self.url); + if path.join(".git").is_dir() { + self.repo_dir = Some(path.join(".git")); + } else { + self.repo_dir = Some(path.to_path_buf()); + } + } else { + // Remote repo: sync mirror + let mirror_dir = self.git_util.sync_mirror(&self.url)?; + self.repo_dir = Some(mirror_dir); + } + + // Determine root identifier (default branch) + let repo_dir = self.repo_dir.clone().unwrap(); + if let Ok(Some(branch)) = self.git_util.get_default_branch(&repo_dir) { + self.root_identifier = Some(branch); + } else { + // Fallback: try common branch names + let process = ProcessExecutor::new(); + for name in &["main", "master"] { + let output = + process.execute(&["git", "rev-parse", "--verify", name], Some(&repo_dir))?; + if output.status == 0 { + self.root_identifier = Some(name.to_string()); + break; + } + } + } + + if self.root_identifier.is_none() { + self.root_identifier = Some("master".to_string()); + } + + Ok(()) + } + + fn root_identifier(&self) -> &str { + self.root_identifier.as_deref().unwrap_or("master") + } + + 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(); + let output = process.execute_checked( + &["git", "branch", "--no-color", "--no-abbrev", "-v"], + Some(&repo_dir), + )?; + self.branches = Some(Self::parse_branches(&output.stdout)); + } + Ok(self.branches.as_ref().unwrap()) + } + + 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(); + let output = process.execute( + &["git", "show-ref", "--tags", "--dereference"], + Some(&repo_dir), + )?; + self.tags = Some(if output.status == 0 { + Self::parse_tags(&output.stdout) + } else { + BTreeMap::new() + }); + } + Ok(self.tags.as_ref().unwrap()) + } + + 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).await?; + let value = match content { + Some(c) => serde_json::from_str(&c).ok(), + None => None, + }; + + self.info_cache + .insert(identifier.to_string(), value.clone()); + Ok(value) + } + + 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}"); + let output = process.execute(&["git", "show", &resource], Some(repo_dir))?; + if output.status == 0 { + Ok(Some(output.stdout)) + } else { + Ok(None) + } + } + + 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( + &["git", "log", "-1", "--format=%aI", identifier], + Some(repo_dir), + )?; + if output.status == 0 { + let date = output.stdout.trim().to_string(); + if date.is_empty() { + Ok(None) + } else { + Ok(Some(date)) + } + } else { + Ok(None) + } + } + + async fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> { + // Plain git repos don't provide dist archives + Ok(None) + } + + fn source(&self, identifier: &str) -> SourceReference { + SourceReference { + source_type: "git".to_string(), + url: self.url.clone(), + reference: identifier.to_string(), + } + } + + fn url(&self) -> &str { + &self.url + } + + async fn cleanup(&mut self) -> Result<()> { + Ok(()) + } +} |
