From c5bcf222f98d13b104231713bf4a0aa0833c420a Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 5 Jun 2026 00:50:47 +0900 Subject: feat(downloader): wire VcsDownloader subclasses via shared downloaders Share downloaders as Rc> in DownloadManager and make DownloaderInterface/VcsDownloader take &mut self, matching PHP's mutable-by-reference downloader objects. This lets Git/Svn/Hg/Fossil/Perforce implement VcsDownloader and route download/install/update/prepare/cleanup through the trait instead of todo!(), and wires the as_* downcast hooks end-to-end. ChangeReportInterface::get_local_changes becomes &mut self since FileDownloader downloads to a compare dir; get_commit_logs/reapply_changes gain &mut self / Result to match the concrete implementations. Co-Authored-By: Claude Opus 4.8 --- crates/shirabe/src/command/status_command.rs | 12 +- .../src/downloader/change_report_interface.rs | 2 +- crates/shirabe/src/downloader/download_manager.rs | 47 +- .../shirabe/src/downloader/downloader_interface.rs | 22 +- crates/shirabe/src/downloader/file_downloader.rs | 18 +- crates/shirabe/src/downloader/fossil_downloader.rs | 201 +- crates/shirabe/src/downloader/git_downloader.rs | 2360 ++++++++++---------- crates/shirabe/src/downloader/gzip_downloader.rs | 18 +- crates/shirabe/src/downloader/hg_downloader.rs | 155 +- crates/shirabe/src/downloader/path_downloader.rs | 18 +- .../shirabe/src/downloader/perforce_downloader.rs | 204 +- crates/shirabe/src/downloader/phar_downloader.rs | 18 +- crates/shirabe/src/downloader/rar_downloader.rs | 18 +- crates/shirabe/src/downloader/svn_downloader.rs | 214 +- crates/shirabe/src/downloader/tar_downloader.rs | 18 +- crates/shirabe/src/downloader/vcs_downloader.rs | 52 +- crates/shirabe/src/downloader/xz_downloader.rs | 18 +- crates/shirabe/src/downloader/zip_downloader.rs | 18 +- crates/shirabe/src/factory.rs | 52 +- crates/shirabe/src/util/sync_helper.rs | 20 +- 20 files changed, 1827 insertions(+), 1658 deletions(-) (limited to 'crates/shirabe/src') diff --git a/crates/shirabe/src/command/status_command.rs b/crates/shirabe/src/command/status_command.rs index 01c1b5e..457c546 100644 --- a/crates/shirabe/src/command/status_command.rs +++ b/crates/shirabe/src/command/status_command.rs @@ -128,13 +128,11 @@ impl StatusCommand { Some(d) => d, None => continue, }; - // TODO(phase-b): downloader borrow lifetime tied to dm.borrow() temporary; restructure later. - let dm_borrow = dm.borrow(); - let downloader: &dyn crate::downloader::DownloaderInterface = - match dm_borrow.get_downloader_for_package(package.clone())? { - Some(d) => d, - None => continue, - }; + let downloader_handle = match dm.borrow().get_downloader_for_package(package.clone())? { + Some(d) => d, + None => continue, + }; + let mut downloader = downloader_handle.borrow_mut(); // TODO(phase-b): isinstance checks using ChangeReportInterface/VcsCapableDownloaderInterface/DvcsDownloaderInterface if let Some(change_reporter) = downloader.as_change_report_interface() { diff --git a/crates/shirabe/src/downloader/change_report_interface.rs b/crates/shirabe/src/downloader/change_report_interface.rs index 5e770f7..718b2c1 100644 --- a/crates/shirabe/src/downloader/change_report_interface.rs +++ b/crates/shirabe/src/downloader/change_report_interface.rs @@ -6,7 +6,7 @@ use crate::package::PackageInterfaceHandle; pub trait ChangeReportInterface { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result>; diff --git a/crates/shirabe/src/downloader/download_manager.rs b/crates/shirabe/src/downloader/download_manager.rs index 7d5d76d..e0998e9 100644 --- a/crates/shirabe/src/downloader/download_manager.rs +++ b/crates/shirabe/src/downloader/download_manager.rs @@ -31,7 +31,7 @@ pub struct DownloadManager { /// @var Filesystem filesystem: std::rc::Rc>, /// @var array - downloaders: IndexMap>, + downloaders: IndexMap>>, } impl DownloadManager { @@ -91,7 +91,7 @@ impl DownloadManager { pub fn set_downloader( &mut self, r#type: &str, - downloader: Box, + downloader: std::rc::Rc>, ) -> &mut Self { let r#type = strtolower(r#type); self.downloaders.insert(r#type, downloader); @@ -103,7 +103,10 @@ impl DownloadManager { /// /// @param string $type installation type /// @throws \InvalidArgumentException if downloader for provided type is not registered - pub fn get_downloader(&self, r#type: &str) -> Result<&dyn DownloaderInterface> { + pub fn get_downloader( + &self, + r#type: &str, + ) -> Result>> { let r#type = strtolower(r#type); if !self.downloaders.contains_key(&r#type) { return Err(InvalidArgumentException { @@ -119,7 +122,7 @@ impl DownloadManager { .into()); } - Ok(self.downloaders.get(&r#type).unwrap().as_ref()) + Ok(self.downloaders.get(&r#type).unwrap().clone()) } /// Returns downloader for already installed package. @@ -131,7 +134,7 @@ impl DownloadManager { pub fn get_downloader_for_package( &self, package: PackageInterfaceHandle, - ) -> Result> { + ) -> Result>>> { let installation_source = package.get_installation_source(); if "metapackage" == package.get_type() { @@ -153,13 +156,14 @@ impl DownloadManager { .into()); }; - if installation_source.as_deref() != Some(&downloader.get_installation_source()) { + let downloader_installation_source = downloader.borrow().get_installation_source(); + if installation_source.as_deref() != Some(&downloader_installation_source) { return Err(LogicException { message: sprintf( "Downloader \"%s\" is a %s type downloader and can not be used to download %s for package %s", &[ - PhpMixed::String(shirabe_php_shim::get_class_obj(downloader)), - PhpMixed::String(downloader.get_installation_source()), + PhpMixed::String(shirabe_php_shim::get_class_obj(&*downloader.borrow())), + PhpMixed::String(downloader_installation_source), PhpMixed::String(installation_source.clone().unwrap_or_default()), PhpMixed::String(package.to_string()), ], @@ -172,13 +176,13 @@ impl DownloadManager { Ok(Some(downloader)) } - pub fn get_downloader_type(&self, downloader: &dyn DownloaderInterface) -> String { + pub fn get_downloader_type( + &self, + downloader: &std::rc::Rc>, + ) -> String { // PHP: array_search($downloader, $this->downloaders) for (r#type, candidate) in &self.downloaders { - if std::ptr::eq( - candidate.as_ref() as *const dyn DownloaderInterface as *const (), - downloader as *const dyn DownloaderInterface as *const (), - ) { + if std::rc::Rc::ptr_eq(candidate, downloader) { return r#type.clone(); } } @@ -238,6 +242,7 @@ impl DownloadManager { }; let result = match downloader + .borrow_mut() .download3(package.clone(), &target_dir, prev_package.clone()) .await { @@ -299,6 +304,7 @@ impl DownloadManager { let target_dir = self.normalize_target_dir(target_dir); if let Some(downloader) = self.get_downloader_for_package(package.clone())? { return downloader + .borrow_mut() .prepare(r#type, package, &target_dir, prev_package) .await; } @@ -321,7 +327,7 @@ impl DownloadManager { ) -> Result> { let target_dir = self.normalize_target_dir(target_dir); if let Some(downloader) = self.get_downloader_for_package(package.clone())? { - return downloader.install2(package, &target_dir).await; + return downloader.borrow_mut().install2(package, &target_dir).await; } Ok(None) @@ -353,16 +359,20 @@ impl DownloadManager { // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed if downloader.is_none() { return initial_downloader + .as_ref() .unwrap() + .borrow_mut() .remove2(initial, &target_dir) .await; } - let initial_type = self.get_downloader_type(initial_downloader.unwrap()); - let target_type = self.get_downloader_type(downloader.unwrap()); + let initial_type = self.get_downloader_type(initial_downloader.as_ref().unwrap()); + let target_type = self.get_downloader_type(downloader.as_ref().unwrap()); if initial_type == target_type { match downloader + .as_ref() .unwrap() + .borrow_mut() .update(initial.clone(), target.clone(), &target_dir) .await { @@ -399,7 +409,9 @@ impl DownloadManager { // we wipe the dir and do a new install instead of updating it // PHP: return $promise->then(fn () => $this->install($target, $targetDir)); let _ = initial_downloader + .as_ref() .unwrap() + .borrow_mut() .remove2(initial, &target_dir) .await?; self.install(target, &target_dir).await @@ -417,7 +429,7 @@ impl DownloadManager { ) -> Result> { let target_dir = self.normalize_target_dir(target_dir); if let Some(downloader) = self.get_downloader_for_package(package.clone())? { - return downloader.remove2(package, &target_dir).await; + return downloader.borrow_mut().remove2(package, &target_dir).await; } Ok(None) @@ -440,6 +452,7 @@ impl DownloadManager { let target_dir = self.normalize_target_dir(target_dir); if let Some(downloader) = self.get_downloader_for_package(package.clone())? { return downloader + .borrow_mut() .cleanup(r#type, package, &target_dir, prev_package) .await; } diff --git a/crates/shirabe/src/downloader/downloader_interface.rs b/crates/shirabe/src/downloader/downloader_interface.rs index 873438e..f63ac4e 100644 --- a/crates/shirabe/src/downloader/downloader_interface.rs +++ b/crates/shirabe/src/downloader/downloader_interface.rs @@ -8,7 +8,7 @@ pub trait DownloaderInterface: std::fmt::Debug { fn get_installation_source(&self) -> String; async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -17,7 +17,7 @@ pub trait DownloaderInterface: std::fmt::Debug { /// Convenience for the PHP default `$output = true` overload. async fn download3( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -26,7 +26,7 @@ pub trait DownloaderInterface: std::fmt::Debug { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -34,7 +34,7 @@ pub trait DownloaderInterface: std::fmt::Debug { ) -> anyhow::Result>; async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -42,7 +42,7 @@ pub trait DownloaderInterface: std::fmt::Debug { /// Convenience for the PHP default `$output = true` overload. async fn install2( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> anyhow::Result> { @@ -50,14 +50,14 @@ pub trait DownloaderInterface: std::fmt::Debug { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, ) -> anyhow::Result>; async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -65,7 +65,7 @@ pub trait DownloaderInterface: std::fmt::Debug { /// Convenience for the PHP default `$output = true` overload. async fn remove2( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> anyhow::Result> { @@ -73,14 +73,16 @@ pub trait DownloaderInterface: std::fmt::Debug { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, prev_package: Option, ) -> anyhow::Result>; - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { None } diff --git a/crates/shirabe/src/downloader/file_downloader.rs b/crates/shirabe/src/downloader/file_downloader.rs index 55882f7..e0fc0c9 100644 --- a/crates/shirabe/src/downloader/file_downloader.rs +++ b/crates/shirabe/src/downloader/file_downloader.rs @@ -153,13 +153,15 @@ impl DownloaderInterface for FileDownloader { "dist".to_owned() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } /// @inheritDoc async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, _prev_package: Option, @@ -437,7 +439,7 @@ impl DownloaderInterface for FileDownloader { /// @inheritDoc async fn prepare( - &self, + &mut self, _type: &str, _package: PackageInterfaceHandle, _path: &str, @@ -448,7 +450,7 @@ impl DownloaderInterface for FileDownloader { /// @inheritDoc async fn cleanup( - &self, + &mut self, _type: &str, package: PackageInterfaceHandle, path: &str, @@ -503,7 +505,7 @@ impl DownloaderInterface for FileDownloader { /// @inheritDoc async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -565,7 +567,7 @@ impl DownloaderInterface for FileDownloader { /// @inheritDoc async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -583,7 +585,7 @@ impl DownloaderInterface for FileDownloader { /// @inheritDoc async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -615,7 +617,7 @@ impl ChangeReportInterface for FileDownloader { /// @inheritDoc /// @throws \RuntimeException fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { diff --git a/crates/shirabe/src/downloader/fossil_downloader.rs b/crates/shirabe/src/downloader/fossil_downloader.rs index 9a38578..d6b8095 100644 --- a/crates/shirabe/src/downloader/fossil_downloader.rs +++ b/crates/shirabe/src/downloader/fossil_downloader.rs @@ -4,6 +4,7 @@ use crate::config::Config; use crate::downloader::ChangeReportInterface; use crate::downloader::DownloaderInterface; use crate::downloader::VcsCapableDownloaderInterface; +use crate::downloader::VcsDownloader; use crate::downloader::VcsDownloaderBase; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -11,6 +12,7 @@ use crate::package::PackageInterfaceHandle; use crate::util::Filesystem; use crate::util::ProcessExecutor; use anyhow::Result; +use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{PhpMixed, RuntimeException}; @@ -31,30 +33,82 @@ impl FossilDownloader { } } - pub(crate) async fn do_download( + fn execute( &self, + command: Vec, + cwd: Option, + output: &mut String, + ) -> Result<()> { + if self + .inner + .process + .borrow_mut() + .execute(&command, output, cwd)? + != 0 + { + return Err(RuntimeException { + message: format!( + "Failed to execute {}\n\n{}", + command.join(" "), + self.inner.process.borrow().get_error_output() + ), + code: 0, + } + .into()); + } + Ok(()) + } +} + +impl VcsDownloader for FossilDownloader { + fn io(&self) -> std::rc::Rc> { + self.inner.io.clone() + } + + fn config(&self) -> &std::rc::Rc> { + &self.inner.config + } + + fn process(&self) -> &std::rc::Rc> { + &self.inner.process + } + + fn filesystem(&self) -> &std::rc::Rc> { + &self.inner.filesystem + } + + fn has_cleaned_changes(&self) -> &IndexMap { + &self.inner.has_cleaned_changes + } + + fn has_cleaned_changes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.has_cleaned_changes + } + + async fn do_download( + &mut self, _package: PackageInterfaceHandle, - _path: String, - _url: String, + _path: &str, + _url: &str, _prev_package: Option, ) -> Result> { Ok(None) } - pub(crate) async fn do_install( - &self, + async fn do_install( + &mut self, package: PackageInterfaceHandle, - path: String, - url: String, + path: &str, + url: &str, ) -> Result> { self.inner.config.borrow_mut().prohibit_url_by_config( - &url, + url, Some(self.inner.io.clone()), &indexmap::IndexMap::new(), )?; let repo_file = format!("{}.fossil", path); - let real_path = shirabe_php_shim::realpath(&path); + let real_path = shirabe_php_shim::realpath(path); self.inner.io.write_error(&format!( "Cloning {}", @@ -67,7 +121,7 @@ impl FossilDownloader { "fossil".to_string(), "clone".to_string(), "--".to_string(), - url, + url.to_string(), repo_file.clone(), ], None, @@ -101,15 +155,15 @@ impl FossilDownloader { Ok(None) } - pub(crate) async fn do_update( - &self, + async fn do_update( + &mut self, _initial: PackageInterfaceHandle, target: PackageInterfaceHandle, - path: String, - url: String, + path: &str, + url: &str, ) -> Result> { self.inner.config.borrow_mut().prohibit_url_by_config( - &url, + url, Some(self.inner.io.clone()), &indexmap::IndexMap::new(), )?; @@ -119,7 +173,7 @@ impl FossilDownloader { target.get_source_reference().unwrap_or_default() )); - if !self.has_metadata_repository(&path) { + if !self.has_metadata_repository(path) { return Err(RuntimeException { message: format!( "The .fslckout file is missing from {}, see https://getcomposer.org/commit-deps for more information", @@ -129,7 +183,7 @@ impl FossilDownloader { }.into()); } - let real_path = shirabe_php_shim::realpath(&path); + let real_path = shirabe_php_shim::realpath(path); let mut output = String::new(); self.execute( vec!["fossil".to_string(), "pull".to_string()], @@ -153,11 +207,11 @@ impl FossilDownloader { Ok(None) } - pub(crate) fn get_commit_logs( - &self, - _from_reference: String, - to_reference: String, - path: String, + fn get_commit_logs( + &mut self, + _from_reference: &str, + to_reference: &str, + path: &str, ) -> Result { let mut output = String::new(); self.execute( @@ -171,9 +225,9 @@ impl FossilDownloader { "-n".to_string(), "0".to_string(), "before".to_string(), - to_reference.clone(), + to_reference.to_string(), ], - shirabe_php_shim::realpath(&path), + shirabe_php_shim::realpath(path), &mut output, )?; @@ -197,33 +251,7 @@ impl FossilDownloader { Ok(log) } - fn execute( - &self, - command: Vec, - cwd: Option, - output: &mut String, - ) -> Result<()> { - if self - .inner - .process - .borrow_mut() - .execute(&command, output, cwd)? - != 0 - { - return Err(RuntimeException { - message: format!( - "Failed to execute {}\n\n{}", - command.join(" "), - self.inner.process.borrow().get_error_output() - ), - code: 0, - } - .into()); - } - Ok(()) - } - - pub(crate) fn has_metadata_repository(&self, path: &str) -> bool { + fn has_metadata_repository(&self, path: &str) -> bool { std::path::Path::new(&format!("{}/.fslckout", path)).is_file() || std::path::Path::new(&format!("{}/_FOSSIL_", path)).is_file() } @@ -231,7 +259,7 @@ impl FossilDownloader { impl ChangeReportInterface for FossilDownloader { fn get_local_changes( - &self, + &mut self, _package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -258,12 +286,11 @@ impl VcsCapableDownloaderInterface for FossilDownloader { } } -// TODO(phase-b): wire up VcsDownloader trait properly. FossilDownloader extends VcsDownloader -// which implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the -// inner VcsDownloaderBase exposes the matching impl surface. #[async_trait::async_trait(?Send)] impl DownloaderInterface for FossilDownloader { - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } @@ -274,63 +301,63 @@ impl DownloaderInterface for FossilDownloader { } fn get_installation_source(&self) -> String { - todo!() + ::get_installation_source(self) } async fn download( - &self, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, _output: bool, ) -> Result> { - todo!() + ::download(self, package, path, prev_package).await } async fn prepare( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> Result> { - todo!() + ::prepare(self, r#type, package, path, prev_package).await } async fn install( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> Result> { - todo!() + ::install(self, package, path).await } async fn update( - &self, - _initial: PackageInterfaceHandle, - _target: PackageInterfaceHandle, - _path: &str, + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, ) -> Result> { - todo!() + ::update(self, initial, target, path).await } async fn remove( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> Result> { - todo!() + ::remove(self, package, path).await } async fn cleanup( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> Result> { - todo!() + ::cleanup(self, r#type, package, path, prev_package).await } } diff --git a/crates/shirabe/src/downloader/git_downloader.rs b/crates/shirabe/src/downloader/git_downloader.rs index 34f89a4..26ff4aa 100644 --- a/crates/shirabe/src/downloader/git_downloader.rs +++ b/crates/shirabe/src/downloader/git_downloader.rs @@ -14,6 +14,7 @@ use crate::config::Config; use crate::downloader::ChangeReportInterface; use crate::downloader::DvcsDownloaderInterface; use crate::downloader::VcsCapableDownloaderInterface; +use crate::downloader::VcsDownloader; use crate::downloader::VcsDownloaderBase; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -60,619 +61,902 @@ impl GitDownloader { } } - pub(crate) async fn do_download( - &mut self, - package: PackageInterfaceHandle, - _path: &str, - url: &str, - _prev_package: Option, - ) -> Result> { - // Do not create an extra local cache when repository is already local - if Filesystem::is_local_path(url) { + pub fn get_unpushed_changes( + &self, + _package: PackageInterfaceHandle, + path: &str, + ) -> Result> { + GitUtil::clean_env(&self.inner.process); + let path = self.normalize_path(path); + if !self.has_metadata_repository(&path) { return Ok(None); } - GitUtil::clean_env(&self.inner.process); - - let cache_path = format!( - "{}/{}/", - self.inner - .config - .borrow_mut() - .get("cache-vcs-dir") - .as_string() - .unwrap_or(""), - Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.to_string()))?, - ); - let git_version = GitUtil::get_version(&self.inner.process); - - // --dissociate option is only available since git 2.3.0-rc0 - if git_version.is_some() - && version_compare(git_version.as_deref().unwrap_or(""), "2.3.0-rc0", ">=") - && Cache::is_usable(&cache_path) + let command = vec![ + "git".to_string(), + "show-ref".to_string(), + "--head".to_string(), + "-d".to_string(), + ]; + let mut output = String::new(); + if self + .inner + .process + .borrow_mut() + .execute_args(&command, &mut output, Some(path.clone())) + != 0 { - self.inner.io.write_error3( - &format!( - " - Syncing {} ({}) into cache", - package.get_name(), - package - .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev), - ), - true, - io_interface::NORMAL, - ); - self.inner.io.write_error3( - &sprintf( - " Cloning to cache at %s", - &[PhpMixed::String(cache_path.clone())], - ), - true, - io_interface::DEBUG, - ); - let r#ref = package.get_source_reference(); - let pretty_version = package.get_pretty_version(); - if self.git_util.fetch_ref_or_sync_mirror( - url, - &cache_path, - r#ref.as_deref().unwrap_or(""), - Some(&pretty_version), - )? && is_dir(&cache_path) - { - self.cached_packages - .entry(package.get_id()) - .or_insert_with(IndexMap::new) - .insert(r#ref.as_deref().unwrap_or("").to_string(), true); - } - } else if git_version.is_none() { return Err(RuntimeException { - message: "git was not found in your PATH, skipping source download".to_string(), + message: format!( + "Failed to execute {}\n\n{}", + implode(" ", &command), + self.inner.process.borrow().get_error_output(), + ), code: 0, } .into()); } - Ok(None) - } + let mut refs = trim(&output, None); + let mut head_match: IndexMap = IndexMap::new(); + if !Preg::is_match_strict_groups3(r"{^([a-f0-9]+) HEAD$}mi", &refs, Some(&mut head_match)) + .unwrap_or(false) + { + // could not match the HEAD for some reason + return Ok(None); + } + let head_ref = head_match + .get(&CaptureKey::ByIndex(1)) + .cloned() + .unwrap_or_default(); - pub(crate) async fn do_install( - &mut self, - package: PackageInterfaceHandle, - path: &str, - url: &str, - ) -> Result> { - GitUtil::clean_env(&self.inner.process); - let path = self.normalize_path(path); - let cache_path = format!( - "{}/{}/", - self.inner - .config - .borrow_mut() - .get("cache-vcs-dir") - .as_string() - .unwrap_or(""), - Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.to_string()))?, - ); - let r#ref = package.get_source_reference().unwrap_or_default(); + let mut branches_match: IndexMap> = IndexMap::new(); + if !Preg::is_match_all_strict_groups3( + &format!("{{^{} refs/heads/(.+)$}}mi", preg_quote(&head_ref, None)), + &refs, + Some(&mut branches_match), + ) + .unwrap_or(false) + { + // not on a branch, we are either on a not-modified tag or some sort of detached head, so skip this + return Ok(None); + } + let candidate_branches: Vec = branches_match + .get(&CaptureKey::ByIndex(1)) + .cloned() + .unwrap_or_default(); - let msg; - let commands: Vec>; - let has_cached = self - .cached_packages - .get(&package.get_id()) - .and_then(|m| m.get(&r#ref)) - .copied() - .unwrap_or(false); - if has_cached { - msg = format!("Cloning {} from cache", self.get_short_hash(&r#ref)); + // use the first match as branch name for now + let mut branch = candidate_branches[0].clone(); + let mut unpushed_changes: Option = None; + let mut branch_not_found_error = false; - let mut clone_flags: Vec = vec![ - "--dissociate".to_string(), - "--reference".to_string(), - cache_path.clone(), - ]; - let transport_options = package.get_transport_options(); - if let Some(git_opts) = transport_options.get("git").and_then(|v| v.as_array()) { - if let Some(single) = git_opts.get("single_use_clone").and_then(|v| v.as_bool()) { - if single { - clone_flags = vec![]; + // do two passes, as if we find anything we want to fetch and then re-try + for i in 0..=1 { + let mut remote_branches: Vec = vec![]; + + // try to find matching branch names in remote repos + for candidate in &candidate_branches { + let mut m: IndexMap> = IndexMap::new(); + if Preg::is_match_all_strict_groups3( + &format!( + "{{^[a-f0-9]+ refs/remotes/((?:[^/]+)/{})$}}mi", + preg_quote(candidate, None) + ), + &refs, + Some(&mut m), + ) + .unwrap_or(false) + { + let matches: Vec = + m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); + for match_ in matches { + branch = candidate.clone(); + remote_branches.push(match_); } + break; } } - commands = vec![ - { - let mut base = vec![ + // if it doesn't exist, then we assume it is an unpushed branch + // this is bad as we have no reference point to do a diff so we just bail listing + // the branch as being unpushed + if remote_branches.is_empty() { + unpushed_changes = Some(format!( + "Branch {} could not be found on any remote and appears to be unpushed", + branch + )); + branch_not_found_error = true; + } else { + // if first iteration found no remote branch but it has now found some, reset $unpushedChanges + // so we get the real diff output no matter its length + if branch_not_found_error { + unpushed_changes = None; + } + for remote_branch in &remote_branches { + let command = vec![ "git".to_string(), - "clone".to_string(), - "--no-checkout".to_string(), - cache_path.clone(), - path.clone(), + "diff".to_string(), + "--name-status".to_string(), + format!("{}...{}", remote_branch, branch), + "--".to_string(), ]; - base.extend(clone_flags); - base - }, - vec![ + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &command, + &mut output, + Some(path.clone()), + ) != 0 + { + return Err(RuntimeException { + message: format!( + "Failed to execute {}\n\n{}", + implode(" ", &command), + self.inner.process.borrow().get_error_output(), + ), + code: 0, + } + .into()); + } + + let output = trim(&output, None); + // keep the shortest diff from all remote branches we compare against + if unpushed_changes.is_none() + || strlen(&output) < strlen(unpushed_changes.as_deref().unwrap_or("")) + { + unpushed_changes = Some(output); + } + } + } + + // first pass and we found unpushed changes, fetch from all remotes to make sure we have up to date + // remotes and then try again as outdated remotes can sometimes cause false-positives + if unpushed_changes.is_some() && i == 0 { + let mut output = String::new(); + self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "fetch".to_string(), "--all".to_string()], + &mut output, + Some(path.clone()), + ); + + // update list of refs after fetching + let command = vec![ "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "origin".to_string(), - "--".to_string(), - "%sanitizedUrl%".to_string(), - ], - vec![ - "git".to_string(), - "remote".to_string(), - "add".to_string(), - "composer".to_string(), - "--".to_string(), - "%sanitizedUrl%".to_string(), - ], - ]; - } else { - msg = format!("Cloning {}", self.get_short_hash(&r#ref)); - commands = vec![ - vec![ - "git".to_string(), - "clone".to_string(), - "--no-checkout".to_string(), - "--".to_string(), - "%url%".to_string(), - path.clone(), - ], - vec![ - "git".to_string(), - "remote".to_string(), - "add".to_string(), - "composer".to_string(), - "--".to_string(), - "%url%".to_string(), - ], - vec![ - "git".to_string(), - "fetch".to_string(), - "composer".to_string(), - ], - vec![ - "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "origin".to_string(), - "--".to_string(), - "%sanitizedUrl%".to_string(), - ], - vec![ - "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "composer".to_string(), - "--".to_string(), - "%sanitizedUrl%".to_string(), - ], - ]; - if Platform::get_env("COMPOSER_DISABLE_NETWORK").is_some() { - return Err(RuntimeException { - message: format!( - "The required git reference for {} is not in cache and network is disabled, aborting", - package.get_name(), - ), - code: 0, + "show-ref".to_string(), + "--head".to_string(), + "-d".to_string(), + ]; + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &command, + &mut output, + Some(path.clone()), + ) != 0 + { + return Err(RuntimeException { + message: format!( + "Failed to execute {}\n\n{}", + implode(" ", &command), + self.inner.process.borrow().get_error_output(), + ), + code: 0, + } + .into()); } - .into()); + refs = trim(&output, None); } - } - self.inner.io.write_error3(&msg, true, io_interface::NORMAL); - - self.git_util - .run_commands(commands, url, Some(&path), true, None)?; - - let source_url = package.get_source_url(); - if Some(url) != source_url.as_deref() && source_url.is_some() { - self.update_origin_url(&path, source_url.as_deref().unwrap()); - } else { - self.set_push_url(&path, url); - } - - let pretty_version = package.get_pretty_version(); - if let Some(new_ref) = - self.update_to_commit(package.clone(), &path, &r#ref, &pretty_version)? - { - if package.get_dist_reference() == package.get_source_reference() { - // TODO(phase-b): set_dist_reference requires &mut PackageInterface - // package.set_dist_reference(Some(new_ref.clone())); + // abort after first pass if we didn't find anything + if unpushed_changes.is_none() { + break; } - // package.set_source_reference(Some(new_ref)); - let _ = new_ref; } - Ok(None) + Ok(unpushed_changes) } - pub(crate) async fn do_update( + /// Updates the given path to the given commit ref + /// + /// @throws \RuntimeException + /// @return null|string if a string is returned, it is the commit reference that was checked out if the original could not be found + pub(crate) fn update_to_commit( &mut self, - _initial: PackageInterfaceHandle, - target: PackageInterfaceHandle, + package: PackageInterfaceHandle, path: &str, - url: &str, - ) -> Result> { - GitUtil::clean_env(&self.inner.process); - let path = self.normalize_path(path); - if !self.has_metadata_repository(&path) { - return Err(RuntimeException { - message: format!( - "The .git directory is missing from {}, see https://getcomposer.org/commit-deps for more information", - path - ), - code: 0, - } - .into()); - } - - let cache_path = format!( - "{}/{}/", - self.inner - .config - .borrow_mut() - .get("cache-vcs-dir") - .as_string() - .unwrap_or(""), - Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.to_string()))?, - ); - let r#ref = target.get_source_reference().unwrap_or_default(); - - let msg; - let remote_url; - let has_cached = self - .cached_packages - .get(&target.get_id()) - .and_then(|m| m.get(&r#ref)) + reference: &str, + pretty_version: &str, + ) -> Result> { + let force: Vec = if self + .has_discarded_changes + .get(path) .copied() - .unwrap_or(false); - if has_cached { - msg = format!("Checking out {} from cache", self.get_short_hash(&r#ref)); - remote_url = cache_path.clone(); + .unwrap_or(false) + || self.has_stashed_changes.get(path).copied().unwrap_or(false) + { + vec!["-f".to_string()] } else { - msg = format!("Checking out {}", self.get_short_hash(&r#ref)); - remote_url = "%url%".to_string(); - if Platform::get_env("COMPOSER_DISABLE_NETWORK").is_some() { - return Err(RuntimeException { - message: format!( - "The required git reference for {} is not in cache and network is disabled, aborting", - target.get_name(), - ), - code: 0, - } - .into()); - } - } - - self.inner.io.write_error3(&msg, true, io_interface::NORMAL); + vec![] + }; - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec![ - "git".to_string(), - "rev-parse".to_string(), - "--quiet".to_string(), - "--verify".to_string(), - format!("{}^{{commit}}", r#ref), - ], - &mut output, - Some(path.clone()), - ) != 0 - { - let commands = vec![ - vec![ - "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "composer".to_string(), - "--".to_string(), - remote_url.clone(), - ], - vec![ - "git".to_string(), - "fetch".to_string(), - "composer".to_string(), - ], - vec![ - "git".to_string(), - "fetch".to_string(), - "--tags".to_string(), - "composer".to_string(), - ], - ]; + // This uses the "--" sequence to separate branch from file parameters. + // + // Otherwise git tries the branch name as well as file name. + // If the non-existent branch is actually the name of a file, the file + // is checked out. - self.git_util - .run_commands(commands, url, Some(&path), false, None)?; - } + let mut branch = Preg::replace(r"{(?:^dev-|(?:\.x)?-dev$)}i", "", &pretty_version)?; - let command = vec![ - "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "composer".to_string(), - "--".to_string(), - "%sanitizedUrl%".to_string(), - ]; - self.git_util - .run_commands(vec![command], url, Some(&path), false, None)?; + // Closure equivalent: $execute = function(array $command) use (&$output, $path) { ... }; + // Inlined below at each call site. - let pretty_version = target.get_pretty_version(); - if let Some(new_ref) = - self.update_to_commit(target.clone(), &path, &r#ref, &pretty_version)? + let mut branches: Option = None; { - if target.get_dist_reference() == target.get_source_reference() { - // TODO(phase-b): set_dist_reference requires &mut PackageInterface - // target.set_dist_reference(Some(new_ref.clone())); + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "branch".to_string(), "-r".to_string()], + &mut output, + Some(path.to_string()), + ) == 0 + { + branches = Some(output); } - // target.set_source_reference(Some(new_ref)); - let _ = new_ref; } - let mut update_origin_url = false; - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "remote".to_string(), "-v".to_string()], - &mut output, - Some(path.clone()), - ) == 0 - { - let mut origin_match: IndexMap = IndexMap::new(); - let mut composer_match: IndexMap = IndexMap::new(); - if Preg::is_match3( - r"{^origin\s+(?P\S+)}m", - &output, - Some(&mut origin_match), + // check whether non-commitish are branches or tags, and fetch branches with the remote name + let git_ref = reference.to_string(); + if !Preg::is_match(r"{^[a-f0-9]{40}$}", reference).unwrap_or(false) + && branches.is_some() + && Preg::is_match( + &format!("{{^\\s+composer/{}$}}m", preg_quote(reference, None)), + branches.as_deref().unwrap_or(""), ) .unwrap_or(false) - && Preg::is_match3( - r"{^composer\s+(?P\S+)}m", - &output, - Some(&mut composer_match), + { + let mut command1: Vec = vec!["git".to_string(), "checkout".to_string()]; + command1.extend(force.clone()); + command1.extend(vec![ + "-B".to_string(), + branch.clone(), + format!("composer/{}", reference), + "--".to_string(), + ]); + let command2 = vec![ + "git".to_string(), + "reset".to_string(), + "--hard".to_string(), + format!("composer/{}", reference), + "--".to_string(), + ]; + + let mut output = String::new(); + let ok1 = self.inner.process.borrow_mut().execute_args( + &command1, + &mut output, + Some(path.to_string()), + ) == 0; + let ok2 = if ok1 { + let mut output = String::new(); + self.inner.process.borrow_mut().execute_args( + &command2, + &mut output, + Some(path.to_string()), + ) == 0 + } else { + false + }; + if ok1 && ok2 { + return Ok(None); + } + } + + // try to checkout branch by name and then reset it so it's on the proper branch name + if Preg::is_match(r"{^[a-f0-9]{40}$}", reference).unwrap_or(false) { + // add 'v' in front of the branch if it was stripped when generating the pretty name + if branches.is_some() + && !Preg::is_match( + &format!("{{^\\s+composer/{}$}}m", preg_quote(&branch, None)), + branches.as_deref().unwrap_or(""), + ) + .unwrap_or(false) + && Preg::is_match( + &format!("{{^\\s+composer/v{}$}}m", preg_quote(&branch, None)), + branches.as_deref().unwrap_or(""), ) .unwrap_or(false) { - let origin_url = origin_match - .get(&CaptureKey::ByName("url".to_string())) - .cloned() - .unwrap_or_default(); - let composer_url = composer_match - .get(&CaptureKey::ByName("url".to_string())) - .cloned() - .unwrap_or_default(); - if origin_url == composer_url - && Some(composer_url.as_str()) != target.get_source_url().as_deref() - { - update_origin_url = true; - } + branch = format!("v{}", branch); } - } - if update_origin_url && target.get_source_url().is_some() { - self.update_origin_url(&path, &target.get_source_url().unwrap()); - } - Ok(None) - } + let command = vec![ + "git".to_string(), + "checkout".to_string(), + branch.clone(), + "--".to_string(), + ]; + let mut fallback_command: Vec = vec!["git".to_string(), "checkout".to_string()]; + fallback_command.extend(force.clone()); + fallback_command.extend(vec![ + "-B".to_string(), + branch.clone(), + format!("composer/{}", branch), + "--".to_string(), + ]); + let reset_command = vec![ + "git".to_string(), + "reset".to_string(), + "--hard".to_string(), + reference.to_string(), + "--".to_string(), + ]; - pub fn get_unpushed_changes( - &self, - _package: PackageInterfaceHandle, - path: &str, - ) -> Result> { - GitUtil::clean_env(&self.inner.process); - let path = self.normalize_path(path); - if !self.has_metadata_repository(&path) { - return Ok(None); + let mut output = String::new(); + let ok_command = self.inner.process.borrow_mut().execute_args( + &command, + &mut output, + Some(path.to_string()), + ) == 0; + let ok_fallback = if !ok_command { + let mut output = String::new(); + self.inner.process.borrow_mut().execute_args( + &fallback_command, + &mut output, + Some(path.to_string()), + ) == 0 + } else { + false + }; + let ok_reset = if ok_command || ok_fallback { + let mut output = String::new(); + self.inner.process.borrow_mut().execute_args( + &reset_command, + &mut output, + Some(path.to_string()), + ) == 0 + } else { + false + }; + if (ok_command || ok_fallback) && ok_reset { + return Ok(None); + } } - let command = vec![ + let mut command1: Vec = vec!["git".to_string(), "checkout".to_string()]; + command1.extend(force.clone()); + command1.extend(vec![git_ref.clone(), "--".to_string()]); + let command2 = vec![ "git".to_string(), - "show-ref".to_string(), - "--head".to_string(), - "-d".to_string(), + "reset".to_string(), + "--hard".to_string(), + git_ref.clone(), + "--".to_string(), ]; - let mut output = String::new(); - if self - .inner - .process - .borrow_mut() - .execute_args(&command, &mut output, Some(path.clone())) - != 0 { - return Err(RuntimeException { - message: format!( - "Failed to execute {}\n\n{}", - implode(" ", &command), - self.inner.process.borrow().get_error_output(), - ), - code: 0, + let mut output = String::new(); + let ok1 = self.inner.process.borrow_mut().execute_args( + &command1, + &mut output, + Some(path.to_string()), + ) == 0; + let ok2 = if ok1 { + let mut output = String::new(); + self.inner.process.borrow_mut().execute_args( + &command2, + &mut output, + Some(path.to_string()), + ) == 0 + } else { + false + }; + if ok1 && ok2 { + return Ok(None); } - .into()); } - let mut refs = trim(&output, None); - let mut head_match: IndexMap = IndexMap::new(); - if !Preg::is_match_strict_groups3(r"{^([a-f0-9]+) HEAD$}mi", &refs, Some(&mut head_match)) - .unwrap_or(false) - { - // could not match the HEAD for some reason - return Ok(None); - } - let head_ref = head_match - .get(&CaptureKey::ByIndex(1)) - .cloned() - .unwrap_or_default(); + let mut exception_extra = String::new(); - let mut branches_match: IndexMap> = IndexMap::new(); - if !Preg::is_match_all_strict_groups3( - &format!("{{^{} refs/heads/(.+)$}}mi", preg_quote(&head_ref, None)), - &refs, - Some(&mut branches_match), - ) - .unwrap_or(false) - { - // not on a branch, we are either on a not-modified tag or some sort of detached head, so skip this - return Ok(None); + // reference was not found (prints "fatal: reference is not a tree: $ref") + if strpos(self.inner.process.borrow().get_error_output(), reference).is_some() { + self.inner.io.write_error3( + &format!( + " {} is gone (history was rewritten?)", + reference + ), + true, + io_interface::NORMAL, + ); + exception_extra = format!( + "\nIt looks like the commit hash is not available in the repository, maybe {}? Run \"composer update {}\" to resolve this.", + if package.is_dev() { + "the commit was removed from the branch" + } else { + "the tag was recreated" + }, + package.get_pretty_name(), + ); } - let candidate_branches: Vec = branches_match - .get(&CaptureKey::ByIndex(1)) - .cloned() - .unwrap_or_default(); - - // use the first match as branch name for now - let mut branch = candidate_branches[0].clone(); - let mut unpushed_changes: Option = None; - let mut branch_not_found_error = false; - // do two passes, as if we find anything we want to fetch and then re-try - for i in 0..=1 { - let mut remote_branches: Vec = vec![]; + let command = format!("{} && {}", implode(" ", &command1), implode(" ", &command2)); - // try to find matching branch names in remote repos - for candidate in &candidate_branches { - let mut m: IndexMap> = IndexMap::new(); - if Preg::is_match_all_strict_groups3( - &format!( - "{{^[a-f0-9]+ refs/remotes/((?:[^/]+)/{})$}}mi", - preg_quote(candidate, None) - ), - &refs, - Some(&mut m), - ) - .unwrap_or(false) - { - let matches: Vec = - m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default(); - for match_ in matches { - branch = candidate.clone(); - remote_branches.push(match_); - } - break; - } + Err(RuntimeException { + message: Url::sanitize(format!( + "Failed to execute {}\n\n{}{}", + command, + self.inner.process.borrow().get_error_output(), + exception_extra, + )), + code: 0, + } + .into()) + } + + pub(crate) fn update_origin_url(&mut self, path: &str, url: &str) { + let mut output = String::new(); + self.inner.process.borrow_mut().execute_args( + &vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "origin".to_string(), + "--".to_string(), + url.to_string(), + ], + &mut output, + Some(path.to_string()), + ); + self.set_push_url(path, url); + } + + pub(crate) fn set_push_url(&mut self, path: &str, url: &str) { + // set push url for github projects + let mut match_: IndexMap = IndexMap::new(); + if Preg::is_match3( + &format!( + "{{^(?:https?|git)://{}/([^/]+)/([^/]+?)(?:\\.git)?$}}", + GitUtil::get_github_domains_regex(&*self.inner.config.borrow()) + ), + url, + Some(&mut match_), + ) + .unwrap_or(false) + { + let protocols = self.inner.config.borrow_mut().get("github-protocols"); + let m1 = match_ + .get(&CaptureKey::ByIndex(1)) + .cloned() + .unwrap_or_default(); + let m2 = match_ + .get(&CaptureKey::ByIndex(2)) + .cloned() + .unwrap_or_default(); + let m3 = match_ + .get(&CaptureKey::ByIndex(3)) + .cloned() + .unwrap_or_default(); + let mut push_url = format!("git@{}:{}/{}.git", m1, m2, m3); + if !in_array(PhpMixed::String("ssh".to_string()), &protocols, true) { + push_url = format!("https://{}/{}/{}.git", m1, m2, m3); } + let cmd = vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "--push".to_string(), + "origin".to_string(), + "--".to_string(), + push_url, + ]; + let mut ignored_output = String::new(); + self.inner.process.borrow_mut().execute_args( + &cmd, + &mut ignored_output, + Some(path.to_string()), + ); + } + } - // if it doesn't exist, then we assume it is an unpushed branch - // this is bad as we have no reference point to do a diff so we just bail listing - // the branch as being unpushed - if remote_branches.is_empty() { - unpushed_changes = Some(format!( - "Branch {} could not be found on any remote and appears to be unpushed", - branch - )); - branch_not_found_error = true; - } else { - // if first iteration found no remote branch but it has now found some, reset $unpushedChanges - // so we get the real diff output no matter its length - if branch_not_found_error { - unpushed_changes = None; - } - for remote_branch in &remote_branches { - let command = vec![ - "git".to_string(), - "diff".to_string(), - "--name-status".to_string(), - format!("{}...{}", remote_branch, branch), - "--".to_string(), - ]; - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &command, - &mut output, - Some(path.clone()), - ) != 0 - { - return Err(RuntimeException { - message: format!( - "Failed to execute {}\n\n{}", - implode(" ", &command), - self.inner.process.borrow().get_error_output(), - ), - code: 0, - } - .into()); - } + /// @phpstan-return PromiseInterface + /// @throws \RuntimeException + pub(crate) async fn discard_changes(&mut self, path: &str) -> Result> { + let path = self.normalize_path(path); + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "clean".to_string(), "-df".to_string()], + &mut output, + Some(path.clone()), + ) != 0 + { + return Err(RuntimeException { + message: format!("Could not reset changes\n\n:{}", output), + code: 0, + } + .into()); + } + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "reset".to_string(), "--hard".to_string()], + &mut output, + Some(path.clone()), + ) != 0 + { + return Err(RuntimeException { + message: format!("Could not reset changes\n\n:{}", output), + code: 0, + } + .into()); + } - let output = trim(&output, None); - // keep the shortest diff from all remote branches we compare against - if unpushed_changes.is_none() - || strlen(&output) < strlen(unpushed_changes.as_deref().unwrap_or("")) - { - unpushed_changes = Some(output); - } - } + self.has_discarded_changes.insert(path, true); + + Ok(None) + } + + /// @phpstan-return PromiseInterface + /// @throws \RuntimeException + pub(crate) async fn stash_changes(&mut self, path: &str) -> Result> { + let path = self.normalize_path(path); + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec![ + "git".to_string(), + "stash".to_string(), + "--include-untracked".to_string(), + ], + &mut output, + Some(path.clone()), + ) != 0 + { + return Err(RuntimeException { + message: format!("Could not stash changes\n\n:{}", output), + code: 0, } + .into()); + } - // first pass and we found unpushed changes, fetch from all remotes to make sure we have up to date - // remotes and then try again as outdated remotes can sometimes cause false-positives - if unpushed_changes.is_some() && i == 0 { - let mut output = String::new(); - self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "fetch".to_string(), "--all".to_string()], - &mut output, - Some(path.clone()), - ); + self.has_stashed_changes.insert(path, true); - // update list of refs after fetching - let command = vec![ - "git".to_string(), - "show-ref".to_string(), - "--head".to_string(), - "-d".to_string(), - ]; - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &command, - &mut output, - Some(path.clone()), - ) != 0 - { - return Err(RuntimeException { - message: format!( - "Failed to execute {}\n\n{}", - implode(" ", &command), - self.inner.process.borrow().get_error_output(), - ), - code: 0, - } - .into()); - } - refs = trim(&output, None); + Ok(None) + } + + /// @throws \RuntimeException + pub(crate) fn view_diff(&mut self, path: &str) { + let path = self.normalize_path(path); + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "diff".to_string(), "HEAD".to_string()], + &mut output, + Some(path.clone()), + ) != 0 + { + // TODO(phase-b): cannot throw from non-Result fn; bubble error via Result later + panic!("{}", format!("Could not view diff\n\n:{}", output)); + } + + self.inner + .io + .write_error3(&output, true, io_interface::NORMAL); + } + + pub(crate) fn normalize_path(&self, path: &str) -> String { + let mut path = path.to_string(); + if Platform::is_windows() && strlen(&path) > 0 { + let mut base_path = path.clone(); + let mut removed: Vec = vec![]; + + while !is_dir(&base_path) && base_path != "\\" { + let mut new_removed = vec![basename(&base_path)]; + new_removed.extend(removed); + removed = new_removed; + base_path = dirname(&base_path); } - // abort after first pass if we didn't find anything - if unpushed_changes.is_none() { - break; + if base_path == "\\" { + return path; } + + path = rtrim( + &format!( + "{}/{}", + realpath(&base_path).unwrap_or_default(), + implode("/", &removed), + ), + Some("/"), + ); } - Ok(unpushed_changes) + path + } + + pub(crate) fn get_short_hash(&self, reference: &str) -> String { + if !self.inner.io.is_verbose() + && Preg::is_match(r"{^[0-9a-f]{40}$}", reference).unwrap_or(false) + { + return substr(reference, 0, Some(10)); + } + + reference.to_string() + } +} + +impl DvcsDownloaderInterface for GitDownloader { + fn get_unpushed_changes( + &self, + package: PackageInterfaceHandle, + path: String, + ) -> Result> { + GitDownloader::get_unpushed_changes(self, package, &path) + } +} + +impl ChangeReportInterface for GitDownloader { + fn get_local_changes( + &mut self, + _package: PackageInterfaceHandle, + path: &str, + ) -> Result> { + GitUtil::clean_env(&self.inner.process); + if !self.has_metadata_repository(path) { + return Ok(None); + } + + let command = vec![ + "git".to_string(), + "status".to_string(), + "--porcelain".to_string(), + "--untracked-files=no".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{}", + implode(" ", &command), + self.inner.process.borrow().get_error_output(), + ), + code: 0, + } + .into()); + } + + let output = trim(&output, None); + + Ok(if strlen(&output) > 0 { + Some(output) + } else { + None + }) + } +} + +impl VcsCapableDownloaderInterface for GitDownloader { + fn get_vcs_reference(&self, package: PackageInterfaceHandle, path: String) -> Option { + self.inner.get_vcs_reference(package, &path) + } +} + +impl VcsDownloader for GitDownloader { + fn io(&self) -> std::rc::Rc> { + self.inner.io.clone() + } + + fn config(&self) -> &std::rc::Rc> { + &self.inner.config + } + + fn process(&self) -> &std::rc::Rc> { + &self.inner.process + } + + fn filesystem(&self) -> &std::rc::Rc> { + &self.inner.filesystem + } + + fn has_cleaned_changes(&self) -> &IndexMap { + &self.inner.has_cleaned_changes } - pub(crate) async fn clean_changes( + fn has_cleaned_changes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.has_cleaned_changes + } + + async fn do_download( &mut self, package: PackageInterfaceHandle, path: &str, - update: bool, + url: &str, + prev_package: Option, ) -> Result> { + // Do not create an extra local cache when repository is already local + if Filesystem::is_local_path(url) { + return Ok(None); + } + GitUtil::clean_env(&self.inner.process); - let path = self.normalize_path(path); - let unpushed = self.get_unpushed_changes(package.clone(), &path)?; - if let Some(unpushed) = unpushed.as_deref() { - if self.inner.io.is_interactive() - || self - .inner - .config - .borrow_mut() - .get("discard-changes") - .as_bool() - != Some(true) + let cache_path = format!( + "{}/{}/", + self.inner + .config + .borrow_mut() + .get("cache-vcs-dir") + .as_string() + .unwrap_or(""), + Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.to_string()))?, + ); + let git_version = GitUtil::get_version(&self.inner.process); + + // --dissociate option is only available since git 2.3.0-rc0 + if git_version.is_some() + && version_compare(git_version.as_deref().unwrap_or(""), "2.3.0-rc0", ">=") + && Cache::is_usable(&cache_path) + { + self.inner.io.write_error3( + &format!( + " - Syncing {} ({}) into cache", + package.get_name(), + package + .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev), + ), + true, + io_interface::NORMAL, + ); + self.inner.io.write_error3( + &sprintf( + " Cloning to cache at %s", + &[PhpMixed::String(cache_path.clone())], + ), + true, + io_interface::DEBUG, + ); + let r#ref = package.get_source_reference(); + let pretty_version = package.get_pretty_version(); + if self.git_util.fetch_ref_or_sync_mirror( + url, + &cache_path, + r#ref.as_deref().unwrap_or(""), + Some(&pretty_version), + )? && is_dir(&cache_path) { + self.cached_packages + .entry(package.get_id()) + .or_insert_with(IndexMap::new) + .insert(r#ref.as_deref().unwrap_or("").to_string(), true); + } + } else if git_version.is_none() { + return Err(RuntimeException { + message: "git was not found in your PATH, skipping source download".to_string(), + code: 0, + } + .into()); + } + + Ok(None) + } + + async fn do_install( + &mut self, + package: PackageInterfaceHandle, + path: &str, + url: &str, + ) -> Result> { + GitUtil::clean_env(&self.inner.process); + let path = self.normalize_path(path); + let cache_path = format!( + "{}/{}/", + self.inner + .config + .borrow_mut() + .get("cache-vcs-dir") + .as_string() + .unwrap_or(""), + Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.to_string()))?, + ); + let r#ref = package.get_source_reference().unwrap_or_default(); + + let msg; + let commands: Vec>; + let has_cached = self + .cached_packages + .get(&package.get_id()) + .and_then(|m| m.get(&r#ref)) + .copied() + .unwrap_or(false); + if has_cached { + msg = format!("Cloning {} from cache", self.get_short_hash(&r#ref)); + + let mut clone_flags: Vec = vec![ + "--dissociate".to_string(), + "--reference".to_string(), + cache_path.clone(), + ]; + let transport_options = package.get_transport_options(); + if let Some(git_opts) = transport_options.get("git").and_then(|v| v.as_array()) { + if let Some(single) = git_opts.get("single_use_clone").and_then(|v| v.as_bool()) { + if single { + clone_flags = vec![]; + } + } + } + + commands = vec![ + { + let mut base = vec![ + "git".to_string(), + "clone".to_string(), + "--no-checkout".to_string(), + cache_path.clone(), + path.clone(), + ]; + base.extend(clone_flags); + base + }, + vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "origin".to_string(), + "--".to_string(), + "%sanitizedUrl%".to_string(), + ], + vec![ + "git".to_string(), + "remote".to_string(), + "add".to_string(), + "composer".to_string(), + "--".to_string(), + "%sanitizedUrl%".to_string(), + ], + ]; + } else { + msg = format!("Cloning {}", self.get_short_hash(&r#ref)); + commands = vec![ + vec![ + "git".to_string(), + "clone".to_string(), + "--no-checkout".to_string(), + "--".to_string(), + "%url%".to_string(), + path.clone(), + ], + vec![ + "git".to_string(), + "remote".to_string(), + "add".to_string(), + "composer".to_string(), + "--".to_string(), + "%url%".to_string(), + ], + vec![ + "git".to_string(), + "fetch".to_string(), + "composer".to_string(), + ], + vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "origin".to_string(), + "--".to_string(), + "%sanitizedUrl%".to_string(), + ], + vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "composer".to_string(), + "--".to_string(), + "%sanitizedUrl%".to_string(), + ], + ]; + if Platform::get_env("COMPOSER_DISABLE_NETWORK").is_some() { return Err(RuntimeException { message: format!( - "Source directory {} has unpushed changes on the current branch: \n{}", - path, unpushed + "The required git reference for {} is not in cache and network is disabled, aborting", + package.get_name(), ), code: 0, } @@ -680,169 +964,84 @@ impl GitDownloader { } } - let changes = match self.get_local_changes(package.clone(), &path)? { - Some(c) => c, - None => return Ok(None), - }; - - if !self.inner.io.is_interactive() { - let discard_changes = self.inner.config.borrow_mut().get("discard-changes"); - if discard_changes.as_bool() == Some(true) { - return self.discard_changes(&path).await; - } - if discard_changes.as_string() == Some("stash") { - if !update { - return self - .inner - .clean_changes(package.clone(), &path, update) - .await; - } - - return self.stash_changes(&path).await; - } + self.inner.io.write_error3(&msg, true, io_interface::NORMAL); - return self.inner.clean_changes(package, &path, update).await; - } + self.git_util + .run_commands(commands, url, Some(&path), true, None)?; - let changes: Vec = array_map( - |elem: &String| format!(" {}", elem), - &Preg::split(r"{\s*\r?\n\s*}", &changes)?, - ); - self.inner.io.write_error3( - &format!( - " {} has modified files:", - package.get_pretty_name() - ), - true, - io_interface::NORMAL, - ); - let slice_end = 10_usize.min(changes.len()); - // TODO(phase-b): PHP passes the list directly to writeError; joined here so write_error3 takes &str - self.inner - .io - .write_error3(&changes[..slice_end].join("\n"), true, io_interface::NORMAL); - if (changes.len() as i64) > 10 { - self.inner.io.write_error3( - &format!( - " {} more files modified, choose \"v\" to view the full list", - changes.len() as i64 - 10 - ), - true, - io_interface::NORMAL, - ); + let source_url = package.get_source_url(); + if Some(url) != source_url.as_deref() && source_url.is_some() { + self.update_origin_url(&path, source_url.as_deref().unwrap()); + } else { + self.set_push_url(&path, url); } - 'outer: loop { - let answer = self - .inner - .io - .ask( - format!( - " Discard changes [y,n,v,{}?]? ", - if update { "s," } else { "" } - ), - PhpMixed::String("?".to_string()), - ) - .as_string() - .map(|s| s.to_string()); - let mut do_help = false; - match answer.as_deref() { - Some("y") => { - self.discard_changes(&path).await?; - break 'outer; - } - Some("s") => { - if !update { - // goto help; - do_help = true; - } else { - self.stash_changes(&path).await?; - break 'outer; - } - } - Some("n") => { - return Err(RuntimeException { - message: "Update aborted".to_string(), - code: 0, - } - .into()); - } - Some("v") => { - // TODO(phase-b): PHP passes list directly; joined here for &str arg - self.inner - .io - .write_error3(&changes.join("\n"), true, io_interface::NORMAL); - } - Some("d") => { - self.view_diff(&path); - } - _ => { - // case '?': default: - do_help = true; - } - } - - if do_help { - // help: - // TODO(phase-b): PHP passes list directly; joined here for &str arg - self.inner.io.write_error3( - &[ - 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(), - " d - view local modifications (diff)".to_string(), - ] - .join("\n"), - true, - io_interface::NORMAL, - ); - if update { - self.inner.io.write_error3( - " s - stash changes and try to reapply them after the update", - true, - io_interface::NORMAL, - ); - } - self.inner - .io - .write_error3(" ? - print help", true, io_interface::NORMAL); + let pretty_version = package.get_pretty_version(); + if let Some(new_ref) = + self.update_to_commit(package.clone(), &path, &r#ref, &pretty_version)? + { + if package.get_dist_reference() == package.get_source_reference() { + // TODO(phase-b): set_dist_reference requires &mut PackageInterface + // package.set_dist_reference(Some(new_ref.clone())); } + // package.set_source_reference(Some(new_ref)); + let _ = new_ref; } Ok(None) } - pub(crate) fn reapply_changes(&mut self, path: &str) -> Result<()> { + async fn do_update( + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, + url: &str, + ) -> Result> { + GitUtil::clean_env(&self.inner.process); let path = self.normalize_path(path); - if self - .has_stashed_changes - .get(&path) + if !self.has_metadata_repository(&path) { + return Err(RuntimeException { + message: format!( + "The .git directory is missing from {}, see https://getcomposer.org/commit-deps for more information", + path + ), + code: 0, + } + .into()); + } + + let cache_path = format!( + "{}/{}/", + self.inner + .config + .borrow_mut() + .get("cache-vcs-dir") + .as_string() + .unwrap_or(""), + Preg::replace(r"{[^a-z0-9.]}i", "-", &Url::sanitize(url.to_string()))?, + ); + let r#ref = target.get_source_reference().unwrap_or_default(); + + let msg; + let remote_url; + let has_cached = self + .cached_packages + .get(&target.get_id()) + .and_then(|m| m.get(&r#ref)) .copied() - .unwrap_or(false) - { - self.has_stashed_changes.shift_remove(&path); - self.inner.io.write_error3( - " Re-applying stashed changes", - true, - io_interface::NORMAL, - ); - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "stash".to_string(), "pop".to_string()], - &mut output, - Some(path.clone()), - ) != 0 - { + .unwrap_or(false); + if has_cached { + msg = format!("Checking out {} from cache", self.get_short_hash(&r#ref)); + remote_url = cache_path.clone(); + } else { + msg = format!("Checking out {}", self.get_short_hash(&r#ref)); + remote_url = "%url%".to_string(); + if Platform::get_env("COMPOSER_DISABLE_NETWORK").is_some() { return Err(RuntimeException { message: format!( - "Failed to apply stashed changes:\n\n{}", - self.inner.process.borrow().get_error_output() + "The required git reference for {} is not in cache and network is disabled, aborting", + target.get_name(), ), code: 0, } @@ -850,310 +1049,321 @@ impl GitDownloader { } } - self.has_discarded_changes.shift_remove(&path); - Ok(()) - } + self.inner.io.write_error3(&msg, true, io_interface::NORMAL); - /// Updates the given path to the given commit ref - /// - /// @throws \RuntimeException - /// @return null|string if a string is returned, it is the commit reference that was checked out if the original could not be found - pub(crate) fn update_to_commit( - &mut self, - package: PackageInterfaceHandle, - path: &str, - reference: &str, - pretty_version: &str, - ) -> Result> { - let force: Vec = if self - .has_discarded_changes - .get(path) - .copied() - .unwrap_or(false) - || self.has_stashed_changes.get(path).copied().unwrap_or(false) + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec![ + "git".to_string(), + "rev-parse".to_string(), + "--quiet".to_string(), + "--verify".to_string(), + format!("{}^{{commit}}", r#ref), + ], + &mut output, + Some(path.clone()), + ) != 0 { - vec!["-f".to_string()] - } else { - vec![] - }; - - // This uses the "--" sequence to separate branch from file parameters. - // - // Otherwise git tries the branch name as well as file name. - // If the non-existent branch is actually the name of a file, the file - // is checked out. + let commands = vec![ + vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "composer".to_string(), + "--".to_string(), + remote_url.clone(), + ], + vec![ + "git".to_string(), + "fetch".to_string(), + "composer".to_string(), + ], + vec![ + "git".to_string(), + "fetch".to_string(), + "--tags".to_string(), + "composer".to_string(), + ], + ]; - let mut branch = Preg::replace(r"{(?:^dev-|(?:\.x)?-dev$)}i", "", &pretty_version)?; + self.git_util + .run_commands(commands, url, Some(&path), false, None)?; + } - // Closure equivalent: $execute = function(array $command) use (&$output, $path) { ... }; - // Inlined below at each call site. + let command = vec![ + "git".to_string(), + "remote".to_string(), + "set-url".to_string(), + "composer".to_string(), + "--".to_string(), + "%sanitizedUrl%".to_string(), + ]; + self.git_util + .run_commands(vec![command], url, Some(&path), false, None)?; - let mut branches: Option = None; + let pretty_version = target.get_pretty_version(); + if let Some(new_ref) = + self.update_to_commit(target.clone(), &path, &r#ref, &pretty_version)? { - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "branch".to_string(), "-r".to_string()], - &mut output, - Some(path.to_string()), - ) == 0 - { - branches = Some(output); + if target.get_dist_reference() == target.get_source_reference() { + // TODO(phase-b): set_dist_reference requires &mut PackageInterface + // target.set_dist_reference(Some(new_ref.clone())); } + // target.set_source_reference(Some(new_ref)); + let _ = new_ref; } - // check whether non-commitish are branches or tags, and fetch branches with the remote name - let git_ref = reference.to_string(); - if !Preg::is_match(r"{^[a-f0-9]{40}$}", reference).unwrap_or(false) - && branches.is_some() - && Preg::is_match( - &format!("{{^\\s+composer/{}$}}m", preg_quote(reference, None)), - branches.as_deref().unwrap_or(""), + let mut update_origin_url = false; + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "remote".to_string(), "-v".to_string()], + &mut output, + Some(path.clone()), + ) == 0 + { + let mut origin_match: IndexMap = IndexMap::new(); + let mut composer_match: IndexMap = IndexMap::new(); + if Preg::is_match3( + r"{^origin\s+(?P\S+)}m", + &output, + Some(&mut origin_match), ) .unwrap_or(false) - { - let mut command1: Vec = vec!["git".to_string(), "checkout".to_string()]; - command1.extend(force.clone()); - command1.extend(vec![ - "-B".to_string(), - branch.clone(), - format!("composer/{}", reference), - "--".to_string(), - ]); - let command2 = vec![ - "git".to_string(), - "reset".to_string(), - "--hard".to_string(), - format!("composer/{}", reference), - "--".to_string(), - ]; - - let mut output = String::new(); - let ok1 = self.inner.process.borrow_mut().execute_args( - &command1, - &mut output, - Some(path.to_string()), - ) == 0; - let ok2 = if ok1 { - let mut output = String::new(); - self.inner.process.borrow_mut().execute_args( - &command2, - &mut output, - Some(path.to_string()), - ) == 0 - } else { - false - }; - if ok1 && ok2 { - return Ok(None); - } - } - - // try to checkout branch by name and then reset it so it's on the proper branch name - if Preg::is_match(r"{^[a-f0-9]{40}$}", reference).unwrap_or(false) { - // add 'v' in front of the branch if it was stripped when generating the pretty name - if branches.is_some() - && !Preg::is_match( - &format!("{{^\\s+composer/{}$}}m", preg_quote(&branch, None)), - branches.as_deref().unwrap_or(""), - ) - .unwrap_or(false) - && Preg::is_match( - &format!("{{^\\s+composer/v{}$}}m", preg_quote(&branch, None)), - branches.as_deref().unwrap_or(""), + && Preg::is_match3( + r"{^composer\s+(?P\S+)}m", + &output, + Some(&mut composer_match), ) .unwrap_or(false) { - branch = format!("v{}", branch); + let origin_url = origin_match + .get(&CaptureKey::ByName("url".to_string())) + .cloned() + .unwrap_or_default(); + let composer_url = composer_match + .get(&CaptureKey::ByName("url".to_string())) + .cloned() + .unwrap_or_default(); + if origin_url == composer_url + && Some(composer_url.as_str()) != target.get_source_url().as_deref() + { + update_origin_url = true; + } } + } + if update_origin_url && target.get_source_url().is_some() { + self.update_origin_url(&path, &target.get_source_url().unwrap()); + } - let command = vec![ - "git".to_string(), - "checkout".to_string(), - branch.clone(), - "--".to_string(), - ]; - let mut fallback_command: Vec = vec!["git".to_string(), "checkout".to_string()]; - fallback_command.extend(force.clone()); - fallback_command.extend(vec![ - "-B".to_string(), - branch.clone(), - format!("composer/{}", branch), - "--".to_string(), - ]); - let reset_command = vec![ - "git".to_string(), - "reset".to_string(), - "--hard".to_string(), - reference.to_string(), - "--".to_string(), - ]; + Ok(None) + } - let mut output = String::new(); - let ok_command = self.inner.process.borrow_mut().execute_args( - &command, - &mut output, - Some(path.to_string()), - ) == 0; - let ok_fallback = if !ok_command { - let mut output = String::new(); - self.inner.process.borrow_mut().execute_args( - &fallback_command, - &mut output, - Some(path.to_string()), - ) == 0 - } else { - false - }; - let ok_reset = if ok_command || ok_fallback { - let mut output = String::new(); - self.inner.process.borrow_mut().execute_args( - &reset_command, - &mut output, - Some(path.to_string()), - ) == 0 - } else { - false - }; - if (ok_command || ok_fallback) && ok_reset { - return Ok(None); + async fn clean_changes( + &mut self, + package: PackageInterfaceHandle, + path: &str, + update: bool, + ) -> Result> { + GitUtil::clean_env(&self.inner.process); + let path = self.normalize_path(path); + + let unpushed = self.get_unpushed_changes(package.clone(), &path)?; + if let Some(unpushed) = unpushed.as_deref() { + if self.inner.io.is_interactive() + || self + .inner + .config + .borrow_mut() + .get("discard-changes") + .as_bool() + != Some(true) + { + return Err(RuntimeException { + message: format!( + "Source directory {} has unpushed changes on the current branch: \n{}", + path, unpushed + ), + code: 0, + } + .into()); } } - let mut command1: Vec = vec!["git".to_string(), "checkout".to_string()]; - command1.extend(force.clone()); - command1.extend(vec![git_ref.clone(), "--".to_string()]); - let command2 = vec![ - "git".to_string(), - "reset".to_string(), - "--hard".to_string(), - git_ref.clone(), - "--".to_string(), - ]; - { - let mut output = String::new(); - let ok1 = self.inner.process.borrow_mut().execute_args( - &command1, - &mut output, - Some(path.to_string()), - ) == 0; - let ok2 = if ok1 { - let mut output = String::new(); - self.inner.process.borrow_mut().execute_args( - &command2, - &mut output, - Some(path.to_string()), - ) == 0 - } else { - false - }; - if ok1 && ok2 { - return Ok(None); + let changes = match self.get_local_changes(package.clone(), &path)? { + Some(c) => c, + None => return Ok(None), + }; + + if !self.inner.io.is_interactive() { + let discard_changes = self.inner.config.borrow_mut().get("discard-changes"); + if discard_changes.as_bool() == Some(true) { + return self.discard_changes(&path).await; } - } + if discard_changes.as_string() == Some("stash") { + if !update { + return self + .inner + .clean_changes(package.clone(), &path, update) + .await; + } - let mut exception_extra = String::new(); + return self.stash_changes(&path).await; + } - // reference was not found (prints "fatal: reference is not a tree: $ref") - if strpos(self.inner.process.borrow().get_error_output(), reference).is_some() { + return self.inner.clean_changes(package, &path, update).await; + } + + let changes: Vec = array_map( + |elem: &String| format!(" {}", elem), + &Preg::split(r"{\s*\r?\n\s*}", &changes)?, + ); + self.inner.io.write_error3( + &format!( + " {} has modified files:", + package.get_pretty_name() + ), + true, + io_interface::NORMAL, + ); + let slice_end = 10_usize.min(changes.len()); + // TODO(phase-b): PHP passes the list directly to writeError; joined here so write_error3 takes &str + self.inner + .io + .write_error3(&changes[..slice_end].join("\n"), true, io_interface::NORMAL); + if (changes.len() as i64) > 10 { self.inner.io.write_error3( &format!( - " {} is gone (history was rewritten?)", - reference + " {} more files modified, choose \"v\" to view the full list", + changes.len() as i64 - 10 ), true, io_interface::NORMAL, ); - exception_extra = format!( - "\nIt looks like the commit hash is not available in the repository, maybe {}? Run \"composer update {}\" to resolve this.", - if package.is_dev() { - "the commit was removed from the branch" - } else { - "the tag was recreated" - }, - package.get_pretty_name(), - ); - } - - let command = format!("{} && {}", implode(" ", &command1), implode(" ", &command2)); - - Err(RuntimeException { - message: Url::sanitize(format!( - "Failed to execute {}\n\n{}{}", - command, - self.inner.process.borrow().get_error_output(), - exception_extra, - )), - code: 0, } - .into()) - } - - pub(crate) fn update_origin_url(&mut self, path: &str, url: &str) { - let mut output = String::new(); - self.inner.process.borrow_mut().execute_args( - &vec![ - "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "origin".to_string(), - "--".to_string(), - url.to_string(), - ], - &mut output, - Some(path.to_string()), - ); - self.set_push_url(path, url); - } - pub(crate) fn set_push_url(&mut self, path: &str, url: &str) { - // set push url for github projects - let mut match_: IndexMap = IndexMap::new(); - if Preg::is_match3( - &format!( - "{{^(?:https?|git)://{}/([^/]+)/([^/]+?)(?:\\.git)?$}}", - GitUtil::get_github_domains_regex(&*self.inner.config.borrow()) - ), - url, - Some(&mut match_), - ) - .unwrap_or(false) - { - let protocols = self.inner.config.borrow_mut().get("github-protocols"); - let m1 = match_ - .get(&CaptureKey::ByIndex(1)) - .cloned() - .unwrap_or_default(); - let m2 = match_ - .get(&CaptureKey::ByIndex(2)) - .cloned() - .unwrap_or_default(); - let m3 = match_ - .get(&CaptureKey::ByIndex(3)) - .cloned() - .unwrap_or_default(); - let mut push_url = format!("git@{}:{}/{}.git", m1, m2, m3); - if !in_array(PhpMixed::String("ssh".to_string()), &protocols, true) { - push_url = format!("https://{}/{}/{}.git", m1, m2, m3); + 'outer: loop { + let answer = self + .inner + .io + .ask( + format!( + " Discard changes [y,n,v,{}?]? ", + if update { "s," } else { "" } + ), + PhpMixed::String("?".to_string()), + ) + .as_string() + .map(|s| s.to_string()); + let mut do_help = false; + match answer.as_deref() { + Some("y") => { + self.discard_changes(&path).await?; + break 'outer; + } + Some("s") => { + if !update { + // goto help; + do_help = true; + } else { + self.stash_changes(&path).await?; + break 'outer; + } + } + Some("n") => { + return Err(RuntimeException { + message: "Update aborted".to_string(), + code: 0, + } + .into()); + } + Some("v") => { + // TODO(phase-b): PHP passes list directly; joined here for &str arg + self.inner + .io + .write_error3(&changes.join("\n"), true, io_interface::NORMAL); + } + Some("d") => { + self.view_diff(&path); + } + _ => { + // case '?': default: + do_help = true; + } } - let cmd = vec![ - "git".to_string(), - "remote".to_string(), - "set-url".to_string(), - "--push".to_string(), - "origin".to_string(), - "--".to_string(), - push_url, - ]; - let mut ignored_output = String::new(); - self.inner.process.borrow_mut().execute_args( - &cmd, - &mut ignored_output, - Some(path.to_string()), + + if do_help { + // help: + // TODO(phase-b): PHP passes list directly; joined here for &str arg + self.inner.io.write_error3( + &[ + 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(), + " d - view local modifications (diff)".to_string(), + ] + .join("\n"), + true, + io_interface::NORMAL, + ); + if update { + self.inner.io.write_error3( + " s - stash changes and try to reapply them after the update", + true, + io_interface::NORMAL, + ); + } + self.inner + .io + .write_error3(" ? - print help", true, io_interface::NORMAL); + } + } + + Ok(None) + } + + fn reapply_changes(&mut self, path: &str) -> Result<()> { + let path = self.normalize_path(path); + if self + .has_stashed_changes + .get(&path) + .copied() + .unwrap_or(false) + { + self.has_stashed_changes.shift_remove(&path); + self.inner.io.write_error3( + " Re-applying stashed changes", + true, + io_interface::NORMAL, ); + let mut output = String::new(); + if self.inner.process.borrow_mut().execute_args( + &vec!["git".to_string(), "stash".to_string(), "pop".to_string()], + &mut output, + Some(path.clone()), + ) != 0 + { + return Err(RuntimeException { + message: format!( + "Failed to apply stashed changes:\n\n{}", + self.inner.process.borrow().get_error_output() + ), + code: 0, + } + .into()); + } } + + self.has_discarded_changes.shift_remove(&path); + Ok(()) } - pub(crate) fn get_commit_logs( + fn get_commit_logs( &mut self, from_reference: &str, to_reference: &str, @@ -1189,203 +1399,17 @@ impl GitDownloader { Ok(GitUtil::parse_rev_list_output(&output, &self.inner.process)) } - /// @phpstan-return PromiseInterface - /// @throws \RuntimeException - pub(crate) async fn discard_changes(&mut self, path: &str) -> Result> { - let path = self.normalize_path(path); - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "clean".to_string(), "-df".to_string()], - &mut output, - Some(path.clone()), - ) != 0 - { - return Err(RuntimeException { - message: format!("Could not reset changes\n\n:{}", output), - code: 0, - } - .into()); - } - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "reset".to_string(), "--hard".to_string()], - &mut output, - Some(path.clone()), - ) != 0 - { - return Err(RuntimeException { - message: format!("Could not reset changes\n\n:{}", output), - code: 0, - } - .into()); - } - - self.has_discarded_changes.insert(path, true); - - Ok(None) - } - - /// @phpstan-return PromiseInterface - /// @throws \RuntimeException - pub(crate) async fn stash_changes(&mut self, path: &str) -> Result> { - let path = self.normalize_path(path); - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec![ - "git".to_string(), - "stash".to_string(), - "--include-untracked".to_string(), - ], - &mut output, - Some(path.clone()), - ) != 0 - { - return Err(RuntimeException { - message: format!("Could not stash changes\n\n:{}", output), - code: 0, - } - .into()); - } - - self.has_stashed_changes.insert(path, true); - - Ok(None) - } - - /// @throws \RuntimeException - pub(crate) fn view_diff(&mut self, path: &str) { - let path = self.normalize_path(path); - let mut output = String::new(); - if self.inner.process.borrow_mut().execute_args( - &vec!["git".to_string(), "diff".to_string(), "HEAD".to_string()], - &mut output, - Some(path.clone()), - ) != 0 - { - // TODO(phase-b): cannot throw from non-Result fn; bubble error via Result later - panic!("{}", format!("Could not view diff\n\n:{}", output)); - } - - self.inner - .io - .write_error3(&output, true, io_interface::NORMAL); - } - - pub(crate) fn normalize_path(&self, path: &str) -> String { - let mut path = path.to_string(); - if Platform::is_windows() && strlen(&path) > 0 { - let mut base_path = path.clone(); - let mut removed: Vec = vec![]; - - while !is_dir(&base_path) && base_path != "\\" { - let mut new_removed = vec![basename(&base_path)]; - new_removed.extend(removed); - removed = new_removed; - base_path = dirname(&base_path); - } - - if base_path == "\\" { - return path; - } - - path = rtrim( - &format!( - "{}/{}", - realpath(&base_path).unwrap_or_default(), - implode("/", &removed), - ), - Some("/"), - ); - } - - path - } - - pub(crate) fn has_metadata_repository(&self, path: &str) -> bool { + fn has_metadata_repository(&self, path: &str) -> bool { let path = self.normalize_path(path); is_dir(&format!("{}/.git", path)) } - - pub(crate) fn get_short_hash(&self, reference: &str) -> String { - if !self.inner.io.is_verbose() - && Preg::is_match(r"{^[0-9a-f]{40}$}", reference).unwrap_or(false) - { - return substr(reference, 0, Some(10)); - } - - reference.to_string() - } -} - -impl DvcsDownloaderInterface for GitDownloader { - fn get_unpushed_changes( - &self, - package: PackageInterfaceHandle, - path: String, - ) -> Result> { - GitDownloader::get_unpushed_changes(self, package, &path) - } -} - -impl ChangeReportInterface for GitDownloader { - fn get_local_changes( - &self, - _package: PackageInterfaceHandle, - path: &str, - ) -> Result> { - GitUtil::clean_env(&self.inner.process); - if !self.has_metadata_repository(path) { - return Ok(None); - } - - let command = vec![ - "git".to_string(), - "status".to_string(), - "--porcelain".to_string(), - "--untracked-files=no".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{}", - implode(" ", &command), - self.inner.process.borrow().get_error_output(), - ), - code: 0, - } - .into()); - } - - let output = trim(&output, None); - - Ok(if strlen(&output) > 0 { - Some(output) - } else { - None - }) - } -} - -impl VcsCapableDownloaderInterface for GitDownloader { - fn get_vcs_reference(&self, package: PackageInterfaceHandle, path: String) -> Option { - self.inner.get_vcs_reference(package, &path) - } } -// TODO(phase-b): GitDownloader extends VcsDownloader which implements DownloaderInterface. -// Delegating each trait method to todo!() until the inner VcsDownloaderBase exposes the -// matching impl surface. #[async_trait::async_trait(?Send)] impl crate::downloader::DownloaderInterface for GitDownloader { fn get_installation_source(&self) -> String { - todo!() + ::get_installation_source(self) } fn as_dvcs_downloader_interface( @@ -1394,7 +1418,9 @@ impl crate::downloader::DownloaderInterface for GitDownloader { Some(self) } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } @@ -1405,59 +1431,59 @@ impl crate::downloader::DownloaderInterface for GitDownloader { } async fn download( - &self, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, _output: bool, ) -> anyhow::Result> { - todo!() + ::download(self, package, path, prev_package).await } async fn prepare( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> anyhow::Result> { - todo!() + ::prepare(self, r#type, package, path, prev_package).await } async fn install( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> anyhow::Result> { - todo!() + ::install(self, package, path).await } async fn update( - &self, - _initial: PackageInterfaceHandle, - _target: PackageInterfaceHandle, - _path: &str, + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, ) -> anyhow::Result> { - todo!() + ::update(self, initial, target, path).await } async fn remove( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> anyhow::Result> { - todo!() + ::remove(self, package, path).await } async fn cleanup( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> anyhow::Result> { - todo!() + ::cleanup(self, r#type, package, path, prev_package).await } } diff --git a/crates/shirabe/src/downloader/gzip_downloader.rs b/crates/shirabe/src/downloader/gzip_downloader.rs index 337e298..0c47b67 100644 --- a/crates/shirabe/src/downloader/gzip_downloader.rs +++ b/crates/shirabe/src/downloader/gzip_downloader.rs @@ -132,7 +132,7 @@ impl GzipDownloader { impl ChangeReportInterface for GzipDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -146,12 +146,14 @@ impl crate::downloader::DownloaderInterface for GzipDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -163,7 +165,7 @@ impl crate::downloader::DownloaderInterface for GzipDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -175,7 +177,7 @@ impl crate::downloader::DownloaderInterface for GzipDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -184,7 +186,7 @@ impl crate::downloader::DownloaderInterface for GzipDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -193,7 +195,7 @@ impl crate::downloader::DownloaderInterface for GzipDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -202,7 +204,7 @@ impl crate::downloader::DownloaderInterface for GzipDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/downloader/hg_downloader.rs b/crates/shirabe/src/downloader/hg_downloader.rs index c5a65a0..a9a5bf8 100644 --- a/crates/shirabe/src/downloader/hg_downloader.rs +++ b/crates/shirabe/src/downloader/hg_downloader.rs @@ -4,6 +4,7 @@ use crate::config::Config; use crate::downloader::ChangeReportInterface; use crate::downloader::DownloaderInterface; use crate::downloader::VcsCapableDownloaderInterface; +use crate::downloader::VcsDownloader; use crate::downloader::VcsDownloaderBase; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -12,6 +13,7 @@ use crate::util::Filesystem; use crate::util::Hg as HgUtils; use crate::util::ProcessExecutor; use anyhow::Result; +use indexmap::IndexMap; use shirabe_php_shim::{PhpMixed, RuntimeException}; #[derive(Debug)] @@ -30,13 +32,39 @@ impl HgDownloader { inner: VcsDownloaderBase::new(io, config, Some(process), Some(fs)), } } +} - pub(crate) async fn do_download( - &self, - package: PackageInterfaceHandle, - path: String, - url: String, - prev_package: Option, +impl VcsDownloader for HgDownloader { + fn io(&self) -> std::rc::Rc> { + self.inner.io.clone() + } + + fn config(&self) -> &std::rc::Rc> { + &self.inner.config + } + + fn process(&self) -> &std::rc::Rc> { + &self.inner.process + } + + fn filesystem(&self) -> &std::rc::Rc> { + &self.inner.filesystem + } + + fn has_cleaned_changes(&self) -> &IndexMap { + &self.inner.has_cleaned_changes + } + + fn has_cleaned_changes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.has_cleaned_changes + } + + async fn do_download( + &mut self, + _package: PackageInterfaceHandle, + _path: &str, + _url: &str, + _prev_package: Option, ) -> Result> { if HgUtils::get_version(&self.inner.process).is_none() { return Err(RuntimeException { @@ -49,11 +77,11 @@ impl HgDownloader { Ok(None) } - pub(crate) async fn do_install( - &self, + async fn do_install( + &mut self, package: PackageInterfaceHandle, - path: String, - url: String, + path: &str, + url: &str, ) -> Result> { let hg_utils = HgUtils::new( self.inner.io.clone(), @@ -61,7 +89,7 @@ impl HgDownloader { &self.inner.process, ); - let path_clone = path.clone(); + let path_clone = path.to_string(); let clone_command = move |url: String| -> Vec { vec![ "hg".to_string(), @@ -71,7 +99,7 @@ impl HgDownloader { path_clone.clone(), ] }; - hg_utils.run_command(clone_command, url, Some(path.clone())); + hg_utils.run_command(clone_command, url.to_string(), Some(path.to_string())); let command = vec![ "hg".to_string(), @@ -86,7 +114,7 @@ impl HgDownloader { if self.inner.process.borrow_mut().execute_args( &command, &mut ignored_output, - shirabe_php_shim::realpath(&path), + shirabe_php_shim::realpath(path), ) != 0 { return Err(RuntimeException { @@ -103,12 +131,12 @@ impl HgDownloader { Ok(None) } - pub(crate) async fn do_update( - &self, - initial: PackageInterfaceHandle, + async fn do_update( + &mut self, + _initial: PackageInterfaceHandle, target: PackageInterfaceHandle, - path: String, - url: String, + path: &str, + url: &str, ) -> Result> { let hg_utils = HgUtils::new( self.inner.io.clone(), @@ -125,7 +153,7 @@ impl HgDownloader { target.get_source_reference().unwrap_or_default() )); - if !self.has_metadata_repository(path.clone()) { + if !self.has_metadata_repository(path) { return Err(RuntimeException { message: format!( "The .hg directory is missing from {}, see https://getcomposer.org/commit-deps for more information", @@ -138,7 +166,7 @@ impl HgDownloader { let pull_command = |url: String| -> Vec { vec!["hg".to_string(), "pull".to_string(), "--".to_string(), url] }; - hg_utils.run_command(pull_command, url.clone(), Some(path.clone())); + hg_utils.run_command(pull_command, url.to_string(), Some(path.to_string())); let ref_clone = ref_.clone(); let up_command = move |_url: String| -> Vec { @@ -149,16 +177,16 @@ impl HgDownloader { ref_clone.clone(), ] }; - hg_utils.run_command(up_command, url, Some(path)); + hg_utils.run_command(up_command, url.to_string(), Some(path.to_string())); Ok(None) } - pub(crate) fn get_commit_logs( - &self, - from_reference: String, - to_reference: String, - path: String, + fn get_commit_logs( + &mut self, + from_reference: &str, + to_reference: &str, + path: &str, ) -> Result { let command = vec![ "hg".to_string(), @@ -173,7 +201,7 @@ impl HgDownloader { if self.inner.process.borrow_mut().execute_args( &command, &mut output, - shirabe_php_shim::realpath(&path), + shirabe_php_shim::realpath(path), ) != 0 { return Err(RuntimeException { @@ -190,14 +218,14 @@ impl HgDownloader { Ok(output) } - pub(crate) fn has_metadata_repository(&self, path: String) -> bool { + fn has_metadata_repository(&self, path: &str) -> bool { std::path::Path::new(&format!("{}/.hg", path)).is_dir() } } impl ChangeReportInterface for HgDownloader { fn get_local_changes( - &self, + &mut self, _package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -228,12 +256,11 @@ impl VcsCapableDownloaderInterface for HgDownloader { } } -// TODO(phase-b): wire up VcsDownloader trait properly. HgDownloader extends VcsDownloader which -// implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the inner -// VcsDownloaderBase exposes the matching impl surface. #[async_trait::async_trait(?Send)] impl DownloaderInterface for HgDownloader { - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } @@ -244,63 +271,63 @@ impl DownloaderInterface for HgDownloader { } fn get_installation_source(&self) -> String { - todo!() + ::get_installation_source(self) } async fn download( - &self, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, _output: bool, ) -> Result> { - todo!() + ::download(self, package, path, prev_package).await } async fn prepare( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> Result> { - todo!() + ::prepare(self, r#type, package, path, prev_package).await } async fn install( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> Result> { - todo!() + ::install(self, package, path).await } async fn update( - &self, - _initial: PackageInterfaceHandle, - _target: PackageInterfaceHandle, - _path: &str, + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, ) -> Result> { - todo!() + ::update(self, initial, target, path).await } async fn remove( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> Result> { - todo!() + ::remove(self, package, path).await } async fn cleanup( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> Result> { - todo!() + ::cleanup(self, r#type, package, path, prev_package).await } } diff --git a/crates/shirabe/src/downloader/path_downloader.rs b/crates/shirabe/src/downloader/path_downloader.rs index 3eb2a48..99d72e7 100644 --- a/crates/shirabe/src/downloader/path_downloader.rs +++ b/crates/shirabe/src/downloader/path_downloader.rs @@ -537,7 +537,7 @@ impl VcsCapableDownloaderInterface for PathDownloader { impl crate::downloader::ChangeReportInterface for PathDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> anyhow::Result> { @@ -555,7 +555,9 @@ impl DownloaderInterface for PathDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } @@ -566,7 +568,7 @@ impl DownloaderInterface for PathDownloader { } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -578,7 +580,7 @@ impl DownloaderInterface for PathDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -590,7 +592,7 @@ impl DownloaderInterface for PathDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -599,7 +601,7 @@ impl DownloaderInterface for PathDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -608,7 +610,7 @@ impl DownloaderInterface for PathDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -617,7 +619,7 @@ impl DownloaderInterface for PathDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/downloader/perforce_downloader.rs b/crates/shirabe/src/downloader/perforce_downloader.rs index e33b159..2d91c60 100644 --- a/crates/shirabe/src/downloader/perforce_downloader.rs +++ b/crates/shirabe/src/downloader/perforce_downloader.rs @@ -4,6 +4,7 @@ use crate::config::Config; use crate::downloader::ChangeReportInterface; use crate::downloader::DownloaderInterface; use crate::downloader::VcsCapableDownloaderInterface; +use crate::downloader::VcsDownloader; use crate::downloader::VcsDownloaderBase; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -37,46 +38,6 @@ impl PerforceDownloader { } } - pub(crate) async fn do_download( - &self, - _package: PackageInterfaceHandle, - _path: String, - _url: String, - _prev_package: Option, - ) -> Result> { - Ok(None) - } - - pub async fn do_install( - &mut self, - package: PackageInterfaceHandle, - path: String, - url: String, - ) -> Result> { - let source_ref = package.get_source_reference().map(|s| s.to_string()); - let label = self.get_label_from_source_reference(source_ref.clone().unwrap_or_default()); - - self.inner.io.write_error(&format!( - "Cloning {}", - source_ref.clone().unwrap_or_default() - )); - self.init_perforce(package, path.clone(), url); - self.perforce - .as_mut() - .unwrap() - .set_stream(&source_ref.clone().unwrap_or_default()); - self.perforce.as_mut().unwrap().p4_login(); - self.perforce.as_mut().unwrap().write_p4_client_spec(); - self.perforce.as_mut().unwrap().connect_client(); - self.perforce - .as_mut() - .unwrap() - .sync_code_base(label.as_deref()); - self.perforce.as_mut().unwrap().cleanup_client_spec(); - - Ok(None) - } - fn get_label_from_source_reference(&self, source_ref: String) -> Option { let pos = source_ref.find('@'); if let Some(pos) = pos { @@ -116,42 +77,108 @@ impl PerforceDownloader { repository.get_repo_config().clone() } - pub(crate) async fn do_update( + pub fn set_perforce(&mut self, perforce: Perforce) { + self.perforce = Some(perforce); + } +} + +impl VcsDownloader for PerforceDownloader { + fn io(&self) -> std::rc::Rc> { + self.inner.io.clone() + } + + fn config(&self) -> &std::rc::Rc> { + &self.inner.config + } + + fn process(&self) -> &std::rc::Rc> { + &self.inner.process + } + + fn filesystem(&self) -> &std::rc::Rc> { + &self.inner.filesystem + } + + fn has_cleaned_changes(&self) -> &IndexMap { + &self.inner.has_cleaned_changes + } + + fn has_cleaned_changes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.has_cleaned_changes + } + + async fn do_download( + &mut self, + _package: PackageInterfaceHandle, + _path: &str, + _url: &str, + _prev_package: Option, + ) -> Result> { + Ok(None) + } + + async fn do_install( + &mut self, + package: PackageInterfaceHandle, + path: &str, + url: &str, + ) -> Result> { + let source_ref = package.get_source_reference().map(|s| s.to_string()); + let label = self.get_label_from_source_reference(source_ref.clone().unwrap_or_default()); + + self.inner.io.write_error(&format!( + "Cloning {}", + source_ref.clone().unwrap_or_default() + )); + self.init_perforce(package, path.to_string(), url.to_string()); + self.perforce + .as_mut() + .unwrap() + .set_stream(&source_ref.clone().unwrap_or_default()); + self.perforce.as_mut().unwrap().p4_login(); + self.perforce.as_mut().unwrap().write_p4_client_spec(); + self.perforce.as_mut().unwrap().connect_client(); + self.perforce + .as_mut() + .unwrap() + .sync_code_base(label.as_deref()); + self.perforce.as_mut().unwrap().cleanup_client_spec(); + + Ok(None) + } + + async fn do_update( &mut self, _initial: PackageInterfaceHandle, target: PackageInterfaceHandle, - path: String, - url: String, + path: &str, + url: &str, ) -> Result> { self.do_install(target, path, url).await } - pub(crate) fn get_commit_logs( + fn get_commit_logs( &mut self, - from_reference: String, - to_reference: String, - _path: String, + from_reference: &str, + to_reference: &str, + _path: &str, ) -> Result { Ok(self .perforce .as_mut() .unwrap() - .get_commit_logs(&from_reference, &to_reference) + .get_commit_logs(from_reference, to_reference) .unwrap_or_default()) } - pub fn set_perforce(&mut self, perforce: Perforce) { - self.perforce = Some(perforce); - } - - pub(crate) fn has_metadata_repository(&self, _path: &str) -> bool { + fn has_metadata_repository(&self, _path: &str) -> bool { true } } impl ChangeReportInterface for PerforceDownloader { fn get_local_changes( - &self, + &mut self, _package: PackageInterfaceHandle, _path: &str, ) -> Result> { @@ -169,12 +196,11 @@ impl VcsCapableDownloaderInterface for PerforceDownloader { } } -// TODO(phase-b): wire up VcsDownloader trait properly. PerforceDownloader extends VcsDownloader -// which implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the -// inner VcsDownloaderBase exposes the matching impl surface. #[async_trait::async_trait(?Send)] impl DownloaderInterface for PerforceDownloader { - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } @@ -185,63 +211,63 @@ impl DownloaderInterface for PerforceDownloader { } fn get_installation_source(&self) -> String { - todo!() + ::get_installation_source(self) } async fn download( - &self, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, _output: bool, ) -> Result> { - todo!() + ::download(self, package, path, prev_package).await } async fn prepare( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> Result> { - todo!() + ::prepare(self, r#type, package, path, prev_package).await } async fn install( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> Result> { - todo!() + ::install(self, package, path).await } async fn update( - &self, - _initial: PackageInterfaceHandle, - _target: PackageInterfaceHandle, - _path: &str, + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, ) -> Result> { - todo!() + ::update(self, initial, target, path).await } async fn remove( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> Result> { - todo!() + ::remove(self, package, path).await } async fn cleanup( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> Result> { - todo!() + ::cleanup(self, r#type, package, path, prev_package).await } } diff --git a/crates/shirabe/src/downloader/phar_downloader.rs b/crates/shirabe/src/downloader/phar_downloader.rs index 46fdd84..8180764 100644 --- a/crates/shirabe/src/downloader/phar_downloader.rs +++ b/crates/shirabe/src/downloader/phar_downloader.rs @@ -66,7 +66,7 @@ impl PharDownloader { impl ChangeReportInterface for PharDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -80,12 +80,14 @@ impl DownloaderInterface for PharDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -97,7 +99,7 @@ impl DownloaderInterface for PharDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -109,7 +111,7 @@ impl DownloaderInterface for PharDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -118,7 +120,7 @@ impl DownloaderInterface for PharDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -127,7 +129,7 @@ impl DownloaderInterface for PharDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -136,7 +138,7 @@ impl DownloaderInterface for PharDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/downloader/rar_downloader.rs b/crates/shirabe/src/downloader/rar_downloader.rs index 4385bc3..5676a96 100644 --- a/crates/shirabe/src/downloader/rar_downloader.rs +++ b/crates/shirabe/src/downloader/rar_downloader.rs @@ -146,7 +146,7 @@ impl RarDownloader { impl ChangeReportInterface for RarDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -160,12 +160,14 @@ impl crate::downloader::DownloaderInterface for RarDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -177,7 +179,7 @@ impl crate::downloader::DownloaderInterface for RarDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -189,7 +191,7 @@ impl crate::downloader::DownloaderInterface for RarDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -198,7 +200,7 @@ impl crate::downloader::DownloaderInterface for RarDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -207,7 +209,7 @@ impl crate::downloader::DownloaderInterface for RarDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -216,7 +218,7 @@ impl crate::downloader::DownloaderInterface for RarDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/downloader/svn_downloader.rs b/crates/shirabe/src/downloader/svn_downloader.rs index 1e00c03..ef0fd16 100644 --- a/crates/shirabe/src/downloader/svn_downloader.rs +++ b/crates/shirabe/src/downloader/svn_downloader.rs @@ -9,6 +9,7 @@ use crate::config::Config; use crate::downloader::ChangeReportInterface; use crate::downloader::DownloaderInterface; use crate::downloader::VcsCapableDownloaderInterface; +use crate::downloader::VcsDownloader; use crate::downloader::VcsDownloaderBase; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; @@ -37,12 +38,85 @@ impl SvnDownloader { } } - pub(crate) async fn do_download( - &mut self, + pub(crate) fn execute( + &self, package: PackageInterfaceHandle, - path: &str, + base_url: &str, + command: Vec, url: &str, - prev_package: Option, + cwd: Option<&str>, + path: Option<&str>, + ) -> anyhow::Result { + let mut util = SvnUtil::new( + base_url.to_string(), + self.inner.io.clone(), + 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 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(None) + } +} + +impl VcsDownloader for SvnDownloader { + fn io(&self) -> std::rc::Rc> { + self.inner.io.clone() + } + + fn config(&self) -> &std::rc::Rc> { + &self.inner.config + } + + fn process(&self) -> &std::rc::Rc> { + &self.inner.process + } + + fn filesystem(&self) -> &std::rc::Rc> { + &self.inner.filesystem + } + + fn has_cleaned_changes(&self) -> &IndexMap { + &self.inner.has_cleaned_changes + } + + fn has_cleaned_changes_mut(&mut self) -> &mut IndexMap { + &mut self.inner.has_cleaned_changes + } + + async fn do_download( + &mut self, + _package: PackageInterfaceHandle, + _path: &str, + url: &str, + _prev_package: Option, ) -> anyhow::Result> { SvnUtil::clean_env(); let mut util = SvnUtil::new( @@ -62,7 +136,7 @@ impl SvnDownloader { Ok(None) } - pub(crate) async fn do_install( + async fn do_install( &mut self, package: PackageInterfaceHandle, path: &str, @@ -109,9 +183,9 @@ impl SvnDownloader { Ok(None) } - pub(crate) async fn do_update( + async fn do_update( &mut self, - initial: PackageInterfaceHandle, + _initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, url: &str, @@ -160,33 +234,7 @@ impl SvnDownloader { Ok(None) } - pub(crate) fn execute( - &self, - package: PackageInterfaceHandle, - 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(), - 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( + async fn clean_changes( &mut self, package: PackageInterfaceHandle, path: &str, @@ -294,8 +342,8 @@ impl SvnDownloader { Ok(None) } - pub(crate) fn get_commit_logs( - &self, + fn get_commit_logs( + &mut self, from_reference: &str, to_reference: &str, path: &str, @@ -384,35 +432,14 @@ impl SvnDownloader { } } - 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(None) - } - - pub(crate) fn has_metadata_repository(&self, path: &str) -> bool { + fn has_metadata_repository(&self, path: &str) -> bool { is_dir(&format!("{}/.svn", path)) } } impl ChangeReportInterface for SvnDownloader { fn get_local_changes( - &self, + &mut self, _package: PackageInterfaceHandle, path: &str, ) -> anyhow::Result> { @@ -445,16 +472,15 @@ impl VcsCapableDownloaderInterface for SvnDownloader { } } -// 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. #[async_trait::async_trait(?Send)] impl DownloaderInterface for SvnDownloader { fn get_installation_source(&self) -> String { - todo!() + ::get_installation_source(self) } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } @@ -465,59 +491,59 @@ impl DownloaderInterface for SvnDownloader { } async fn download( - &self, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, _output: bool, ) -> anyhow::Result> { - todo!() + ::download(self, package, path, prev_package).await } async fn prepare( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> anyhow::Result> { - todo!() + ::prepare(self, r#type, package, path, prev_package).await } async fn install( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> anyhow::Result> { - todo!() + ::install(self, package, path).await } async fn update( - &self, - _initial: PackageInterfaceHandle, - _target: PackageInterfaceHandle, - _path: &str, + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, ) -> anyhow::Result> { - todo!() + ::update(self, initial, target, path).await } async fn remove( - &self, - _package: PackageInterfaceHandle, - _path: &str, + &mut self, + package: PackageInterfaceHandle, + path: &str, _output: bool, ) -> anyhow::Result> { - todo!() + ::remove(self, package, path).await } async fn cleanup( - &self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option, ) -> anyhow::Result> { - todo!() + ::cleanup(self, r#type, package, path, prev_package).await } } diff --git a/crates/shirabe/src/downloader/tar_downloader.rs b/crates/shirabe/src/downloader/tar_downloader.rs index 6cf1176..8835e7d 100644 --- a/crates/shirabe/src/downloader/tar_downloader.rs +++ b/crates/shirabe/src/downloader/tar_downloader.rs @@ -61,7 +61,7 @@ impl TarDownloader { impl ChangeReportInterface for TarDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -75,12 +75,14 @@ impl DownloaderInterface for TarDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -92,7 +94,7 @@ impl DownloaderInterface for TarDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -104,7 +106,7 @@ impl DownloaderInterface for TarDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -113,7 +115,7 @@ impl DownloaderInterface for TarDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -122,7 +124,7 @@ impl DownloaderInterface for TarDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -131,7 +133,7 @@ impl DownloaderInterface for TarDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/downloader/vcs_downloader.rs b/crates/shirabe/src/downloader/vcs_downloader.rs index 493440c..0637f08 100644 --- a/crates/shirabe/src/downloader/vcs_downloader.rs +++ b/crates/shirabe/src/downloader/vcs_downloader.rs @@ -93,13 +93,9 @@ pub trait VcsDownloader: DownloaderInterface + ChangeReportInterface + VcsCapableDownloaderInterface { fn io(&self) -> std::rc::Rc>; - fn io_mut(&mut self) -> &mut dyn IOInterface; fn config(&self) -> &std::rc::Rc>; - fn config_mut(&mut self) -> &mut std::rc::Rc>; fn process(&self) -> &std::rc::Rc>; - fn process_mut(&mut self) -> &mut std::rc::Rc>; fn filesystem(&self) -> &std::rc::Rc>; - fn filesystem_mut(&mut self) -> &mut std::rc::Rc>; fn has_cleaned_changes(&self) -> &IndexMap; fn has_cleaned_changes_mut(&mut self) -> &mut IndexMap; @@ -130,7 +126,12 @@ pub trait VcsDownloader: ) -> Result>; /// Fetches the commit logs between two commits - fn get_commit_logs(&self, from_reference: &str, to_reference: &str, path: &str) -> String; + fn get_commit_logs( + &mut self, + from_reference: &str, + to_reference: &str, + path: &str, + ) -> Result; /// Checks if VCS metadata repository has been initialized /// repository example: .git|.svn|.hg @@ -174,7 +175,7 @@ pub trait VcsDownloader: return Err(e); } if self.io().is_debug() { - self.io_mut().write_error3( + self.io().write_error3( &format!("Failed: [{}] {}", get_class_err(&e), e,), true, io_interface::NORMAL, @@ -185,7 +186,7 @@ pub trait VcsDownloader: .collect(), )) > 0 { - self.io_mut().write_error3( + self.io().write_error3( " Failed, trying the next URL", true, io_interface::NORMAL, @@ -219,9 +220,7 @@ pub trait VcsDownloader: self.has_cleaned_changes_mut() .insert(prev_package.unwrap().get_unique_name(), true); } else if r#type == "install" { - self.filesystem_mut() - .borrow_mut() - .empty_directory(path, true)?; + self.filesystem().borrow_mut().empty_directory(path, true)?; } else if r#type == "uninstall" { self.clean_changes(package, path, false).await?; } @@ -245,7 +244,7 @@ pub trait VcsDownloader: }) .unwrap_or(false) { - self.reapply_changes(path); + self.reapply_changes(path)?; self.has_cleaned_changes_mut() .shift_remove(&prev_package.unwrap().get_unique_name()); } @@ -269,7 +268,7 @@ pub trait VcsDownloader: .into()); } - self.io_mut().write_error3( + self.io().write_error3( &format!(" - {}: ", InstallOperation::format(package.clone(), false)), false, io_interface::NORMAL, @@ -290,7 +289,7 @@ pub trait VcsDownloader: return Err(e); } if self.io().is_debug() { - self.io_mut().write_error3( + self.io().write_error3( &format!("Failed: [{}] {}", get_class_err(&e), e,), true, io_interface::NORMAL, @@ -301,7 +300,7 @@ pub trait VcsDownloader: .collect(), )) > 0 { - self.io_mut().write_error3( + self.io().write_error3( " Failed, trying the next URL", true, io_interface::NORMAL, @@ -339,7 +338,7 @@ pub trait VcsDownloader: .into()); } - self.io_mut().write_error3( + self.io().write_error3( &format!( " - {}: ", UpdateOperation::format(initial.clone(), target.clone(), false), @@ -369,7 +368,7 @@ pub trait VcsDownloader: return Err(e); } if self.io().is_debug() { - self.io_mut().write_error3( + self.io().write_error3( &format!("Failed: [{}] {}", get_class_err(&e), e,), true, io_interface::NORMAL, @@ -380,7 +379,7 @@ pub trait VcsDownloader: .collect(), )) > 0 { - self.io_mut().write_error3( + self.io().write_error3( " Failed, trying the next URL", true, io_interface::NORMAL, @@ -397,11 +396,11 @@ pub trait VcsDownloader: let initial_ref = initial.get_source_reference().unwrap_or_default(); let target_ref = target.get_source_reference().unwrap_or_default(); let mut message = "Pulling in changes:"; - let mut logs = self.get_commit_logs(&initial_ref, &target_ref, path); + let mut logs = self.get_commit_logs(&initial_ref, &target_ref, path)?; if trim(&logs, None) == "" { message = "Rolling back changes:"; - logs = self.get_commit_logs(&target_ref, &initial_ref, path); + logs = self.get_commit_logs(&target_ref, &initial_ref, path)?; } if trim(&logs, None) != "" { @@ -414,10 +413,9 @@ pub trait VcsDownloader: // escape angle brackets for proper output in the console logs = str_replace("<", "\\<", &logs); - self.io_mut() + self.io() .write_error3(&format!(" {}", message), true, io_interface::NORMAL); - self.io_mut() - .write_error3(&logs, true, io_interface::NORMAL); + self.io().write_error3(&logs, true, io_interface::NORMAL); } } @@ -435,14 +433,14 @@ pub trait VcsDownloader: package: PackageInterfaceHandle, path: &str, ) -> Result> { - self.io_mut().write_error3( + self.io().write_error3( &format!(" - {}", UninstallOperation::format(package, false)), true, io_interface::NORMAL, ); let result = self - .filesystem_mut() + .filesystem() .borrow_mut() .remove_directory_async(path) .await?; @@ -481,7 +479,7 @@ pub trait VcsDownloader: /// @param bool $update if true (update) the changes can be stashed and reapplied after an update, /// if false (remove) the changes should be assumed to be lost if the operation is not aborted async fn clean_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, _update: bool, @@ -499,7 +497,9 @@ pub trait VcsDownloader: } /// Reapply previously stashed changes if applicable, only called after an update (regardless if successful or not) - fn reapply_changes(&self, _path: &str) {} + fn reapply_changes(&mut self, _path: &str) -> Result<()> { + Ok(()) + } fn prepare_urls(&self, mut urls: Vec) -> Vec { for index in 0..urls.len() { diff --git a/crates/shirabe/src/downloader/xz_downloader.rs b/crates/shirabe/src/downloader/xz_downloader.rs index 976497a..10a7edf 100644 --- a/crates/shirabe/src/downloader/xz_downloader.rs +++ b/crates/shirabe/src/downloader/xz_downloader.rs @@ -80,7 +80,7 @@ impl XzDownloader { impl ChangeReportInterface for XzDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -94,12 +94,14 @@ impl crate::downloader::DownloaderInterface for XzDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -111,7 +113,7 @@ impl crate::downloader::DownloaderInterface for XzDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -123,7 +125,7 @@ impl crate::downloader::DownloaderInterface for XzDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -132,7 +134,7 @@ impl crate::downloader::DownloaderInterface for XzDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -141,7 +143,7 @@ impl crate::downloader::DownloaderInterface for XzDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -150,7 +152,7 @@ impl crate::downloader::DownloaderInterface for XzDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/downloader/zip_downloader.rs b/crates/shirabe/src/downloader/zip_downloader.rs index 3fda248..90772ce 100644 --- a/crates/shirabe/src/downloader/zip_downloader.rs +++ b/crates/shirabe/src/downloader/zip_downloader.rs @@ -556,7 +556,7 @@ impl ZipDownloader { impl ChangeReportInterface for ZipDownloader { fn get_local_changes( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, ) -> Result> { @@ -573,12 +573,14 @@ impl crate::downloader::DownloaderInterface for ZipDownloader { self.inner.get_installation_source() } - fn as_change_report_interface(&self) -> Option<&dyn crate::downloader::ChangeReportInterface> { + fn as_change_report_interface( + &mut self, + ) -> Option<&mut dyn crate::downloader::ChangeReportInterface> { Some(self) } async fn download( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, @@ -590,7 +592,7 @@ impl crate::downloader::DownloaderInterface for ZipDownloader { } async fn prepare( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, @@ -602,7 +604,7 @@ impl crate::downloader::DownloaderInterface for ZipDownloader { } async fn install( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -611,7 +613,7 @@ impl crate::downloader::DownloaderInterface for ZipDownloader { } async fn update( - &self, + &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, @@ -620,7 +622,7 @@ impl crate::downloader::DownloaderInterface for ZipDownloader { } async fn remove( - &self, + &mut self, package: PackageInterfaceHandle, path: &str, output: bool, @@ -629,7 +631,7 @@ impl crate::downloader::DownloaderInterface for ZipDownloader { } async fn cleanup( - &self, + &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index 24ac2e0..90d8593 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -1011,52 +1011,52 @@ impl Factory { dm.set_downloader( "git", - Box::new(GitDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(GitDownloader::new( io.clone(), config.clone(), Some(process.clone()), Some(fs.clone()), - )), + ))), ); dm.set_downloader( "svn", - Box::new(SvnDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(SvnDownloader::new( io.clone(), config.clone(), process.clone(), fs.clone(), - )), + ))), ); dm.set_downloader( "fossil", - Box::new(FossilDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(FossilDownloader::new( io.clone(), config.clone(), process.clone(), fs.clone(), - )), + ))), ); dm.set_downloader( "hg", - Box::new(HgDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(HgDownloader::new( io.clone(), config.clone(), process.clone(), fs.clone(), - )), + ))), ); dm.set_downloader( "perforce", - Box::new(PerforceDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(PerforceDownloader::new( io.clone(), config.clone(), process.clone(), fs.clone(), - )), + ))), ); dm.set_downloader( "zip", - Box::new(ZipDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(ZipDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1064,11 +1064,11 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); dm.set_downloader( "rar", - Box::new(RarDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(RarDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1076,11 +1076,11 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); dm.set_downloader( "tar", - Box::new(TarDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(TarDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1088,11 +1088,11 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); dm.set_downloader( "gzip", - Box::new(GzipDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(GzipDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1100,11 +1100,11 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); dm.set_downloader( "xz", - Box::new(XzDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(XzDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1112,11 +1112,11 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); dm.set_downloader( "phar", - Box::new(PharDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(PharDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1124,11 +1124,11 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); dm.set_downloader( "file", - Box::new(FileDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(FileDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1136,11 +1136,11 @@ impl Factory { cache.clone(), Some(fs.clone()), Some(process.clone()), - )), + ))), ); dm.set_downloader( "path", - Box::new(PathDownloader::new( + std::rc::Rc::new(std::cell::RefCell::new(PathDownloader::new( io.clone(), config.clone(), http_downloader.clone(), @@ -1148,7 +1148,7 @@ impl Factory { cache.clone(), fs.clone(), process.clone(), - )), + ))), ); Ok(std::rc::Rc::new(std::cell::RefCell::new(dm))) diff --git a/crates/shirabe/src/util/sync_helper.rs b/crates/shirabe/src/util/sync_helper.rs index 4181b23..6220f5c 100644 --- a/crates/shirabe/src/util/sync_helper.rs +++ b/crates/shirabe/src/util/sync_helper.rs @@ -8,7 +8,7 @@ use anyhow::Result; use shirabe_php_shim::PhpMixed; pub enum DownloaderOrManager<'a> { - Interface(&'a dyn DownloaderInterface), + Interface(&'a std::rc::Rc>), Manager(&'a std::rc::Rc>), } @@ -20,7 +20,7 @@ impl<'a> DownloaderOrManager<'a> { prev_package: Option, ) -> Result> { match self { - Self::Interface(d) => d.download3(package, path, prev_package).await, + Self::Interface(d) => d.borrow_mut().download3(package, path, prev_package).await, Self::Manager(d) => d.borrow().download(package, path, prev_package).await, } } @@ -33,7 +33,11 @@ impl<'a> DownloaderOrManager<'a> { prev_package: Option, ) -> Result> { match self { - Self::Interface(d) => d.prepare(r#type, package, path, prev_package).await, + Self::Interface(d) => { + d.borrow_mut() + .prepare(r#type, package, path, prev_package) + .await + } Self::Manager(d) => { d.borrow() .prepare(r#type, package, path, prev_package) @@ -48,7 +52,7 @@ impl<'a> DownloaderOrManager<'a> { path: &str, ) -> Result> { match self { - Self::Interface(d) => d.install2(package, path).await, + Self::Interface(d) => d.borrow_mut().install2(package, path).await, Self::Manager(d) => d.borrow().install(package, path).await, } } @@ -60,7 +64,7 @@ impl<'a> DownloaderOrManager<'a> { path: &str, ) -> Result> { match self { - Self::Interface(d) => d.update(package, prev_package, path).await, + Self::Interface(d) => d.borrow_mut().update(package, prev_package, path).await, Self::Manager(d) => d.borrow().update(package, prev_package, path).await, } } @@ -73,7 +77,11 @@ impl<'a> DownloaderOrManager<'a> { prev_package: Option, ) -> Result> { match self { - Self::Interface(d) => d.cleanup(r#type, package, path, prev_package).await, + Self::Interface(d) => { + d.borrow_mut() + .cleanup(r#type, package, path, prev_package) + .await + } Self::Manager(d) => { d.borrow() .cleanup(r#type, package, path, prev_package) -- cgit v1.3.1