//! ref: composer/src/Composer/Downloader/SvnDownloader.php use crate::io::io_interface; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::react::promise; use shirabe_php_shim::{PhpMixed, RuntimeException, is_dir, version_compare}; use crate::config::Config; use crate::downloader::DownloaderInterface; use crate::downloader::VcsDownloaderBase; use crate::io::IOInterface; use crate::package::PackageInterface; use crate::repository::VcsRepository; use crate::util::Filesystem; use crate::util::ProcessExecutor; use crate::util::Svn as SvnUtil; #[derive(Debug)] pub struct SvnDownloader { inner: VcsDownloaderBase, pub(crate) cache_credentials: bool, } impl SvnDownloader { pub fn new( io: Box, config: std::rc::Rc>, process: std::rc::Rc>, fs: std::rc::Rc>, ) -> Self { Self { inner: VcsDownloaderBase::new(io, config, Some(process), Some(fs)), cache_credentials: true, } } pub(crate) async fn do_download( &mut self, package: &dyn PackageInterface, path: &str, url: &str, prev_package: Option<&dyn PackageInterface>, ) -> anyhow::Result> { SvnUtil::clean_env(); let mut util = SvnUtil::new( url.to_string(), self.inner.io.clone_box(), self.inner.config.clone(), Some(self.inner.process.clone()), ); if util.binary_version().is_none() { return Err(RuntimeException { message: "svn was not found in your PATH, skipping source download".to_string(), code: 0, } .into()); } Ok(promise::resolve(None)) } pub(crate) async fn do_install( &mut self, package: &dyn PackageInterface, path: &str, url: &str, ) -> anyhow::Result> { SvnUtil::clean_env(); let r#ref = package.get_source_reference(); let repo = package.get_repository(); if let Some(repo) = repo { if let Some(vcs_repo) = repo.as_any().downcast_ref::() { let repo_config = vcs_repo.get_repo_config(); if repo_config.contains_key("svn-cache-credentials") { if let Some(val) = repo_config .get("svn-cache-credentials") .and_then(|v| v.as_bool()) { self.cache_credentials = val; } } } } self.inner.io.write_error3( &format!( " Checking out {}", package.get_source_reference().unwrap_or_default() ), true, io_interface::NORMAL, ); self.execute( package, url, vec!["svn".to_string(), "co".to_string()], &format!("{}/{}", url, r#ref.unwrap_or_default()), None, Some(path), )?; Ok(promise::resolve(None)) } pub(crate) async fn do_update( &mut self, initial: &dyn PackageInterface, target: &dyn PackageInterface, path: &str, url: &str, ) -> anyhow::Result> { SvnUtil::clean_env(); let r#ref = target.get_source_reference(); if !self.has_metadata_repository(path) { return Err(RuntimeException { message: format!( "The .svn directory is missing from {}, see https://getcomposer.org/commit-deps for more information", path ), code: 0, } .into()); } let mut util = SvnUtil::new( url.to_string(), self.inner.io.clone_box(), self.inner.config.clone(), Some(self.inner.process.clone()), ); let mut flags: Vec = vec![]; if version_compare(&util.binary_version().unwrap_or_default(), "1.7.0", ">=") { flags.push("--ignore-ancestry".to_string()); } self.inner.io.write_error3( &format!(" Checking out {}", r#ref.unwrap_or_default()), true, io_interface::NORMAL, ); let mut command = vec!["svn".to_string(), "switch".to_string()]; command.extend(flags); self.execute( target, url, command, &format!("{}/{}", url, r#ref.unwrap_or_default()), Some(path), None, )?; Ok(promise::resolve(None)) } pub fn get_local_changes(&self, package: &dyn PackageInterface, path: &str) -> Option { if !self.has_metadata_repository(path) { return None; } let mut output = String::new(); self.inner.process.borrow_mut().execute_args( &["svn", "status", "--ignore-externals"] .map(|s| s.to_string()) .to_vec(), &mut output, Some(path.to_string()), ); if Preg::is_match("{^ *[^X ] +}m", &output).unwrap_or(false) { Some(output) } else { None } } pub(crate) fn execute( &self, package: &dyn PackageInterface, base_url: &str, command: Vec, url: &str, cwd: Option<&str>, path: Option<&str>, ) -> anyhow::Result { let mut util = SvnUtil::new( base_url.to_string(), self.inner.io.clone_box(), self.inner.config.clone(), Some(self.inner.process.clone()), ); util.set_cache_credentials(self.cache_credentials); util.execute(command, url, cwd, path, self.inner.io.is_verbose()) .map_err(|e| { anyhow::anyhow!( "{} could not be downloaded, {}", package.get_pretty_name(), e ) }) } pub(crate) async fn clean_changes( &mut self, package: &dyn PackageInterface, path: &str, update: bool, ) -> anyhow::Result> { let changes = self.get_local_changes(package, path); if changes.is_none() { return Ok(promise::resolve(None)); } if !self.inner.io.is_interactive() { if self .inner .config .borrow_mut() .get("discard-changes") .as_bool() == Some(true) { return self.discard_changes(path); } return self.inner.clean_changes(package, path, update); } let changes_str = changes.unwrap(); let changes: Vec = Preg::split(r"{\s*\r?\n\s*}", &changes_str) .unwrap_or_default() .into_iter() .map(|elem| format!(" {}", elem)) .collect(); let count_changes = changes.len() as i64; self.inner.io.write_error3( &format!( " {} has modified file{}:", package.get_pretty_name(), if count_changes == 1 { "" } else { "s" } ), true, io_interface::NORMAL, ); let slice_end = 10_usize.min(changes.len()); // TODO(phase-b): PHP writeError accepts array; iterate per-line for now. for line in &changes[..slice_end] { self.inner.io.write_error3(line, true, io_interface::NORMAL); } if count_changes > 10 { let remaining_changes = count_changes - 10; self.inner.io.write_error3( &format!( " {} more file{} modified, choose \"v\" to view the full list", remaining_changes, if remaining_changes == 1 { "" } else { "s" } ), true, io_interface::NORMAL, ); } loop { match self .inner .io .ask( " Discard changes [y,n,v,?]? ".to_string(), PhpMixed::String("?".to_string()), ) .as_string() { Some("y") => { self.discard_changes(path)?; break; } Some("n") => { return Err(RuntimeException { message: "Update aborted".to_string(), code: 0, } .into()); } Some("v") => { // TODO(phase-b): PHP writeError accepts array; iterate per-line. for line in &changes { self.inner.io.write_error3(line, true, io_interface::NORMAL); } } _ => { // TODO(phase-b): PHP writeError accepts array; iterate per-line. let help_lines = vec![ format!( " y - discard changes and apply the {}", if update { "update" } else { "uninstall" } ), format!( " n - abort the {} and let you manually clean things up", if update { "update" } else { "uninstall" } ), " v - view modified files".to_string(), " ? - print help".to_string(), ]; for line in &help_lines { self.inner.io.write_error3(line, true, io_interface::NORMAL); } } } } Ok(promise::resolve(None)) } pub(crate) fn get_commit_logs( &self, from_reference: &str, to_reference: &str, path: &str, ) -> anyhow::Result { if Preg::is_match(r"{@(\d+)$}", from_reference).unwrap_or(false) && Preg::is_match(r"{@(\d+)$}", to_reference).unwrap_or(false) { // retrieve the svn base url from the checkout folder let command = vec![ "svn".to_string(), "info".to_string(), "--non-interactive".to_string(), "--xml".to_string(), "--".to_string(), path.to_string(), ]; let mut output = String::new(); if self.inner.process.borrow_mut().execute_args( &command, &mut output, Some(path.to_string()), ) != 0 { return Err(RuntimeException { message: format!( "Failed to execute {}\n\n{}", command.join(" "), self.inner.process.borrow().get_error_output() ), code: 0, } .into()); } let url_pattern = "#(.*)#"; let mut matches: IndexMap = IndexMap::new(); let base_url = if Preg::match_strict_groups3(url_pattern, &output, Some(&mut matches)) .unwrap_or(false) { matches .get(&CaptureKey::ByIndex(1)) .cloned() .unwrap_or_default() } else { return Err(RuntimeException { message: format!("Unable to determine svn url for path {}", path), code: 0, } .into()); }; // strip paths from references and only keep the actual revision let from_revision = Preg::replace(r"{.*@(\d+)$}", "$1", &from_reference).unwrap_or_default(); let to_revision = Preg::replace(r"{.*@(\d+)$}", "$1", &to_reference).unwrap_or_default(); let command = vec![ "svn".to_string(), "log".to_string(), "-r".to_string(), format!("{}:{}", from_revision, to_revision), "--incremental".to_string(), ]; let mut util = SvnUtil::new( base_url, self.inner.io.clone_box(), self.inner.config.clone(), Some(self.inner.process.clone()), ); util.set_cache_credentials(self.cache_credentials); util.execute_local(command.clone(), path, None, self.inner.io.is_verbose()) .map_err(|e| { RuntimeException { message: format!("Failed to execute {}\n\n{}", command.join(" "), e), code: 0, } .into() }) } else { Ok(format!( "Could not retrieve changes between {} and {} due to missing revision information", from_reference, to_reference )) } } pub(crate) async fn discard_changes(&self, path: &str) -> anyhow::Result> { let mut output = String::new(); if self.inner.process.borrow_mut().execute_args( &["svn", "revert", "-R", "."].map(|s| s.to_string()).to_vec(), &mut output, Some(path.to_string()), ) != 0 { return Err(RuntimeException { message: format!( "Could not reset changes\n\n:{}", self.inner.process.borrow().get_error_output() ), code: 0, } .into()); } Ok(promise::resolve(None)) } pub(crate) fn has_metadata_repository(&self, path: &str) -> bool { is_dir(&format!("{}/.svn", path)) } } // TODO(phase-b): wire up VcsDownloader trait properly. SvnDownloader extends VcsDownloader which // implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the inner // VcsDownloaderBase exposes the matching impl surface. impl DownloaderInterface for SvnDownloader { fn get_installation_source(&self) -> String { todo!() } async fn download( &self, _package: &dyn PackageInterface, _path: &str, _prev_package: Option<&dyn PackageInterface>, _output: bool, ) -> anyhow::Result> { todo!() } async fn prepare( &self, _type: &str, _package: &dyn PackageInterface, _path: &str, _prev_package: Option<&dyn PackageInterface>, ) -> anyhow::Result> { todo!() } async fn install( &self, _package: &dyn PackageInterface, _path: &str, _output: bool, ) -> anyhow::Result> { todo!() } async fn update( &self, _initial: &dyn PackageInterface, _target: &dyn PackageInterface, _path: &str, ) -> anyhow::Result> { todo!() } async fn remove( &self, _package: &dyn PackageInterface, _path: &str, _output: bool, ) -> anyhow::Result> { todo!() } async fn cleanup( &self, _type: &str, _package: &dyn PackageInterface, _path: &str, _prev_package: Option<&dyn PackageInterface>, ) -> anyhow::Result> { todo!() } }