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/svn.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/svn.rs')
| -rw-r--r-- | crates/mozart-core/src/vcs/driver/svn.rs | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/crates/mozart-core/src/vcs/driver/svn.rs b/crates/mozart-core/src/vcs/driver/svn.rs new file mode 100644 index 0000000..7ba9e86 --- /dev/null +++ b/crates/mozart-core/src/vcs/driver/svn.rs @@ -0,0 +1,214 @@ +use super::super::process::ProcessExecutor; +use super::super::util::svn::SvnUtil; +use super::{DistReference, DriverConfig, SourceReference, VcsDriver}; +use anyhow::Result; +use indexmap::IndexMap; +use regex::Regex; +use std::collections::BTreeMap; + +/// SVN VCS driver. +/// +/// Corresponds to Composer's `Repository\Vcs\SvnDriver`. +pub struct SvnDriver { + url: String, + base_url: String, + trunk_path: String, + branches_path: String, + tags_path: String, + root_identifier: Option<String>, + tags: Option<BTreeMap<String, String>>, + branches: Option<BTreeMap<String, String>>, + info_cache: IndexMap<String, Option<serde_json::Value>>, + svn_util: SvnUtil, +} + +impl SvnDriver { + pub fn new(url: &str, _config: DriverConfig) -> Self { + let process = ProcessExecutor::new(); + Self { + url: url.to_string(), + base_url: url.to_string(), + trunk_path: "trunk".to_string(), + branches_path: "branches".to_string(), + tags_path: "tags".to_string(), + root_identifier: None, + tags: None, + branches: None, + info_cache: IndexMap::new(), + svn_util: SvnUtil::new(process), + } + } + + pub fn supports(url: &str) -> bool { + url.starts_with("svn://") || url.starts_with("svn+ssh://") + } + + fn svn_info(&self, url: &str) -> Result<serde_json::Value> { + let output = self.svn_util.execute(&["info", "--xml", url], None)?; + // Parse minimal info from XML output + let stdout = &output.stdout; + let mut info = serde_json::Map::new(); + + if let Some(rev) = extract_xml_attr(stdout, "entry", "revision") { + info.insert("revision".to_string(), serde_json::Value::String(rev)); + } + if let Some(url_val) = extract_xml_content(stdout, "url") { + info.insert("url".to_string(), serde_json::Value::String(url_val)); + } + if let Some(date) = extract_xml_content(stdout, "date") { + info.insert("date".to_string(), serde_json::Value::String(date)); + } + + Ok(serde_json::Value::Object(info)) + } + + fn svn_ls(&self, url: &str) -> Result<Vec<String>> { + let output = self.svn_util.execute(&["ls", url], None)?; + Ok(ProcessExecutor::split_lines(&output.stdout) + .into_iter() + .map(|s| s.trim_end_matches('/').to_string()) + .collect()) + } +} + +impl VcsDriver for SvnDriver { + 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(); + } + self.root_identifier = info["revision"].as_str().map(|s| s.to_string()); + Ok(()) + } + + fn root_identifier(&self) -> &str { + self.root_identifier.as_deref().unwrap_or("HEAD") + } + + async fn branches(&mut self) -> Result<&BTreeMap<String, String>> { + if self.branches.is_none() { + let mut branches = BTreeMap::new(); + + // Add trunk + let trunk_url = format!("{}/{}", self.base_url, self.trunk_path); + if let Ok(info) = self.svn_info(&trunk_url) + && let Some(rev) = info["revision"].as_str() + { + branches.insert("trunk".to_string(), rev.to_string()); + } + + // List branches directory + let branches_url = format!("{}/{}", self.base_url, self.branches_path); + if let Ok(items) = self.svn_ls(&branches_url) { + for name in items { + let branch_url = format!("{}/{}", branches_url, name); + if let Ok(info) = self.svn_info(&branch_url) + && let Some(rev) = info["revision"].as_str() + { + branches.insert(name, rev.to_string()); + } + } + } + + self.branches = Some(branches); + } + Ok(self.branches.as_ref().unwrap()) + } + + 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); + if let Ok(items) = self.svn_ls(&tags_url) { + for name in items { + let tag_url = format!("{}/{}", tags_url, name); + if let Ok(info) = self.svn_info(&tag_url) + && let Some(rev) = info["revision"].as_str() + { + tags.insert(name, rev.to_string()); + } + } + } + self.tags = Some(tags); + } + 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 = content.and_then(|c| serde_json::from_str(&c).ok()); + self.info_cache + .insert(identifier.to_string(), value.clone()); + Ok(value) + } + + 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) + } else { + format!( + "{}/{}/{}@{}", + self.base_url, self.trunk_path, file, identifier + ) + }; + let output = self.svn_util.execute(&["cat", &url], None); + match output { + Ok(o) if !o.stdout.is_empty() => Ok(Some(o.stdout)), + _ => Ok(None), + } + } + + async fn change_date(&self, identifier: &str) -> Result<Option<String>> { + let url = if identifier.contains('/') || identifier == "trunk" { + format!("{}/{}", self.base_url, identifier) + } else { + format!("{}@{}", self.base_url, identifier) + }; + match self.svn_info(&url) { + Ok(info) => Ok(info["date"].as_str().map(|s| s.to_string())), + Err(_) => Ok(None), + } + } + + async fn dist(&self, _identifier: &str) -> Result<Option<DistReference>> { + // SVN doesn't provide dist archives + Ok(None) + } + + fn source(&self, identifier: &str) -> SourceReference { + SourceReference { + source_type: "svn".to_string(), + url: self.base_url.clone(), + reference: identifier.to_string(), + } + } + + fn url(&self) -> &str { + &self.url + } + + async fn cleanup(&mut self) -> Result<()> { + Ok(()) + } +} + +/// Extract an XML attribute value from a simple XML string. +fn extract_xml_attr(xml: &str, tag: &str, attr: &str) -> Option<String> { + let pattern = format!(r#"<{tag}\s[^>]*{attr}="([^"]*)"#); + let re = Regex::new(&pattern).ok()?; + re.captures(xml).map(|c| c[1].to_string()) +} + +/// Extract text content between XML tags. +fn extract_xml_content(xml: &str, tag: &str) -> Option<String> { + let pattern = format!(r"<{tag}>([^<]*)</{tag}>"); + let re = Regex::new(&pattern).ok()?; + re.captures(xml).map(|c| c[1].to_string()) +} |
