diff options
25 files changed, 605 insertions, 196 deletions
diff --git a/crates/shirabe/src/advisory/auditor.rs b/crates/shirabe/src/advisory/auditor.rs index 258099d..2fc93f3 100644 --- a/crates/shirabe/src/advisory/auditor.rs +++ b/crates/shirabe/src/advisory/auditor.rs @@ -586,7 +586,7 @@ impl Auditor { io.write_error(&sprintf( "%s is abandoned. %s.", &[ - PhpMixed::String(self.get_package_name_with_link_for_complete(pkg.clone())), + PhpMixed::String(self.get_package_name_with_link(pkg.clone().into())), PhpMixed::String(replacement), ], )); @@ -625,7 +625,7 @@ impl Auditor { table.add_row(ConsoleIO::sanitize( PhpMixed::List(vec![ Box::new(PhpMixed::String( - self.get_package_name_with_link_for_complete(pkg.clone()), + self.get_package_name_with_link(pkg.clone().into()), )), Box::new(PhpMixed::String(replacement)), ]), @@ -652,15 +652,6 @@ impl Auditor { } } - // TODO(phase-b): merge with get_package_name_with_link once CompletePackageInterface can be - // upcast to PackageInterface (e.g. via an as_package_interface() trait method) - fn get_package_name_with_link_for_complete( - &self, - package: CompletePackageInterfaceHandle, - ) -> String { - self.get_package_name_with_link(package.into()) - } - fn get_severity(&self, advisory: &SecurityAdvisory) -> String { if advisory.severity.is_none() { return String::new(); diff --git a/crates/shirabe/src/autoload/class_loader.rs b/crates/shirabe/src/autoload/class_loader.rs index edb1868..99ee5c2 100644 --- a/crates/shirabe/src/autoload/class_loader.rs +++ b/crates/shirabe/src/autoload/class_loader.rs @@ -530,8 +530,106 @@ impl ClassLoader { // Rust has no `include` operator; this is a no-op placeholder. } + /// PHP `(array) $loader`. Every property is private, so keys are mangled as + /// `"\0Composer\Autoload\ClassLoader\0<propertyName>"` using the original camelCase names, in + /// declaration order. pub fn as_array_iter(&self) -> Vec<(String, PhpMixed)> { - // TODO(phase-b): iterate over loader properties as PHP (array) cast would - todo!() + let key = |name: &str| format!("\0Composer\\Autoload\\ClassLoader\0{}", name); + let str_list = |v: &Vec<String>| { + PhpMixed::List( + v.iter() + .map(|s| Box::new(PhpMixed::String(s.clone()))) + .collect(), + ) + }; + + vec![ + ( + key("vendorDir"), + match &self.vendor_dir { + Some(s) => PhpMixed::String(s.clone()), + None => PhpMixed::Null, + }, + ), + ( + key("prefixLengthsPsr4"), + PhpMixed::Array( + self.prefix_lengths_psr4 + .iter() + .map(|(k, inner)| { + ( + k.clone(), + Box::new(PhpMixed::Array( + inner + .iter() + .map(|(k2, n)| (k2.clone(), Box::new(PhpMixed::Int(*n)))) + .collect(), + )), + ) + }) + .collect(), + ), + ), + ( + key("prefixDirsPsr4"), + PhpMixed::Array( + self.prefix_dirs_psr4 + .iter() + .map(|(k, v)| (k.clone(), Box::new(str_list(v)))) + .collect(), + ), + ), + (key("fallbackDirsPsr4"), str_list(&self.fallback_dirs_psr4)), + ( + key("prefixesPsr0"), + PhpMixed::Array( + self.prefixes_psr0 + .iter() + .map(|(k, inner)| { + ( + k.clone(), + Box::new(PhpMixed::Array( + inner + .iter() + .map(|(k2, v)| (k2.clone(), Box::new(str_list(v)))) + .collect(), + )), + ) + }) + .collect(), + ), + ), + (key("fallbackDirsPsr0"), str_list(&self.fallback_dirs_psr0)), + (key("useIncludePath"), PhpMixed::Bool(self.use_include_path)), + ( + key("classMap"), + PhpMixed::Array( + self.class_map + .iter() + .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone())))) + .collect(), + ), + ), + ( + key("classMapAuthoritative"), + PhpMixed::Bool(self.class_map_authoritative), + ), + ( + key("missingClasses"), + PhpMixed::Array( + self.missing_classes + .iter() + .map(|(k, b)| (k.clone(), Box::new(PhpMixed::Bool(*b)))) + .collect(), + ), + ), + ( + key("apcuPrefix"), + match &self.apcu_prefix { + Some(s) => PhpMixed::String(s.clone()), + None => PhpMixed::Null, + }, + ), + ] } } diff --git a/crates/shirabe/src/command/base_command.rs b/crates/shirabe/src/command/base_command.rs index 24b5a2b..d0818bf 100644 --- a/crates/shirabe/src/command/base_command.rs +++ b/crates/shirabe/src/command/base_command.rs @@ -279,6 +279,10 @@ pub trait HasBaseCommandData { fn io_mut(&mut self) -> &mut Option<Rc<RefCell<dyn IOInterface>>> { &mut self.base_command_data_mut().io } + + fn is_self_update_command(&self) -> bool { + false + } } impl<C: HasBaseCommandData> BaseCommand for C { @@ -352,7 +356,11 @@ impl<C: HasBaseCommandData> BaseCommand for C { .has_parameter_option(&["--no-scripts"], false); // TODO(phase-b): requires inner Symfony Application access for disable_plugins_by_default / disable_scripts_by_default - // TODO(phase-b): `$this instanceof SelfUpdateCommand` not representable + + if self.is_self_update_command() { + disable_plugins = true; + disable_scripts = true; + } let composer = self.try_composer(Some(disable_plugins), Some(disable_scripts)); let io = self.get_io(); @@ -503,7 +511,7 @@ impl<C: HasBaseCommandData> BaseCommand for C { }; // TODO(phase-b): Option<IndexMap<String, PhpMixed>> -> Option<LocalConfigInput> conversion let _ = config; - Factory::create(io, None, disable_plugins_kind, disable_scripts) + Factory::create(io, None, disable_plugins_kind, disable_scripts).map(|c| c.upcast()) } fn get_preferred_install_options( diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index f8bdad9..2a67532 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -757,17 +757,27 @@ impl InitCommand { io.write_error3("\nDefine your dependencies.\n", true, io_interface::NORMAL); // prepare to resolve dependencies - let _repos = self.get_repos(); + let repos = self.get_repos(); let preferred_stability = if let Some(s) = minimum_stability_default.clone().filter(|s| !s.is_empty()) { s } else { "stable".to_string() }; - // TODO(phase-b): repos instanceof CompositeRepository downcast - let _platform_repo: Option<&PlatformRepositoryHandle> = None; - - // (omitted: iterate repos to find PlatformRepository instance) + let platform_repo: Option<PlatformRepositoryHandle> = if repos.is::<CompositeRepository>() { + let borrowed = repos.borrow(); + let composite = borrowed + .as_any() + .downcast_ref::<CompositeRepository>() + .expect("is::<CompositeRepository>() checked above"); + composite + .get_repositories() + .iter() + .find(|candidate| candidate.is::<PlatformRepository>()) + .and_then(|candidate| candidate.as_platform_repository()) + } else { + None + }; let question = "Would you like to define your dependencies (require) interactively [<comment>yes</comment>]? ".to_string(); let require: Vec<String> = input @@ -785,7 +795,7 @@ impl InitCommand { input, _output, require, - _platform_repo, + platform_repo.as_ref(), &preferred_stability, false, false, @@ -820,7 +830,7 @@ impl InitCommand { input, _output, require_dev, - _platform_repo, + platform_repo.as_ref(), &preferred_stability, false, false, diff --git a/crates/shirabe/src/command/self_update_command.rs b/crates/shirabe/src/command/self_update_command.rs index 127661a..d0ffcde 100644 --- a/crates/shirabe/src/command/self_update_command.rs +++ b/crates/shirabe/src/command/self_update_command.rs @@ -15,8 +15,8 @@ use shirabe_php_shim::{ function_exists, hash_file, in_array, ini_get, is_array, is_file, is_numeric, is_writable, iterator_to_array, json_decode, openssl_free_key, openssl_get_md_methods, openssl_pkey_get_public, openssl_verify, posix_geteuid, posix_getpwuid, random_int, rename, - server_argv, sprintf, str_contains, str_replace, strpos, strtolower, strtr, tempnam, unlink, - usleep, version_compare, + server_argv, sprintf, str_replace, strpos, strtolower, strtr, tempnam, unlink, usleep, + version_compare, }; use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData}; @@ -78,42 +78,6 @@ impl SelfUpdateCommand { input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, ) -> Result<i64> { - // TODO(phase-b): __FILE__ / __DIR__ have no direct Rust equivalent - let file_path: &str = ""; - let dir_path: &str = ""; - - if strpos(file_path, "phar:") != Some(0) { - if str_contains(&strtr(dir_path, "\\", "/"), "vendor/composer/composer") { - let proj_dir = shirabe_php_shim::dirname_levels(dir_path, 6); - output.borrow().writeln( - "<error>This instance of Composer does not have the self-update command.</error>", - io_interface::NORMAL, - ); - output.borrow().writeln( - &format!( - "<comment>You are running Composer installed as a package in your current project (\"{}\").</comment>", - proj_dir - ), - io_interface::NORMAL, - ); - output.borrow().writeln( - "<comment>To update Composer, download a composer.phar from https://getcomposer.org and then run `composer.phar update composer/composer` in your project.</comment>", - io_interface::NORMAL, - ); - } else { - output.borrow().writeln( - "<error>This instance of Composer does not have the self-update command.</error>", - io_interface::NORMAL, - ); - output.borrow().writeln( - "<comment>This could be due to a number of reasons, such as Composer being installed as a system package on your OS, or Composer being installed as a package in the current project.</comment>", - io_interface::NORMAL, - ); - } - - return Ok(1); - } - if server_argv().get(0).map(|s| s.as_str()) == Some("Standard input code") { return Ok(1); } @@ -1207,4 +1171,8 @@ impl HasBaseCommandData for SelfUpdateCommand { fn base_command_data_mut(&mut self) -> &mut BaseCommandData { &mut self.base_command_data } + + fn is_self_update_command(&self) -> bool { + true + } } diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs index f324a5c..a316c19 100644 --- a/crates/shirabe/src/command/show_command.rs +++ b/crates/shirabe/src/command/show_command.rs @@ -33,6 +33,7 @@ use crate::package::version::VersionParser; use crate::package::version::VersionSelector; use crate::plugin::CommandEvent; use crate::plugin::PluginEvents; +use crate::repository::ComposerRepository; use crate::repository::CompositeRepository; use crate::repository::FilterRepository; use crate::repository::InstalledArrayRepository; @@ -745,23 +746,37 @@ impl ShowCommand { } for repo in RepositoryUtils::flatten_repositories(repos.clone(), false) { - // TODO(phase-b): InstalledRepository needs as_repository_interface / get_repositories - // wired through; placeholder classification until then. let r#type = if Self::same_repository(&repo, &platform_repo) { "platform" - } else if let Some(ref lr) = locked_repo { - if Self::same_repository(&repo, lr) { - "locked" - } else { - "available" - } + } else if locked_repo + .as_ref() + .map_or(false, |lr| Self::same_repository(&repo, lr)) + { + "locked" + } else if Self::same_repository(&repo, &installed_repo) + || installed_repo + .borrow() + .as_any() + .downcast_ref::<InstalledRepository>() + .map_or(false, |ir| { + ir.get_repositories().iter().any(|r| r.ptr_eq(&repo)) + }) + { + "installed" } else { "available" }; let type_owned = r#type.to_string(); - // TODO(phase-b): RepositoryInterface needs as_composer_repository_mut downcast helper - if false { - let _ = package_filter.as_deref(); + if let Some(cr_rc) = repo.downcast_rc::<ComposerRepository>() { + let names = cr_rc + .borrow_mut() + .get_package_names(package_filter.as_deref())?; + for name in names { + packages + .entry(type_owned.clone()) + .or_insert_with(IndexMap::new) + .insert(name.clone(), PackageOrName::Name(name)); + } } else { for package in repo.get_packages()? { let existing = packages diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 0ff2893..fbde9bd 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -103,10 +103,7 @@ impl Application { pub fn new(name: String, mut version: String) -> Self { let mut inner = BaseApplication::new(&name, &version); - // TODO(phase-b): method_exists check requires reflection-style API on BaseApplication - if true { - inner.set_catch_errors(true); - } + inner.set_catch_errors(true); // PHP: static $shutdownRegistered = false; — register only once globally static SHUTDOWN_REGISTERED: std::sync::OnceLock<()> = std::sync::OnceLock::new(); @@ -436,8 +433,9 @@ impl Application { for command in self.get_plugin_commands()? { let cmd_name = command.get_name().unwrap_or_default(); if self.inner.has(&cmd_name) { - // TODO(phase-b): get_class needs a Command-aware overload; default - // to a placeholder while the trait downcast story is settled. + // TODO(plugin): PHP uses get_class($command) for the skipped-command class + // name. Plugin command discovery (get_plugin_commands) is unimplemented, so + // this loop never runs; wire the concrete class name with the plugin API. let cls = String::new(); plugin_warnings.push(format!("<warning>Plugin command {} ({}) would override a Composer command and has been skipped</warning>", cmd_name, cls)); } else { @@ -1003,7 +1001,7 @@ impl Application { crate::factory::DisablePlugins::None }; match Factory::create(io_for_factory, None, disable_plugins_enum, disable_scripts) { - Ok(c) => self.composer = Some(c), + Ok(c) => self.composer = Some(c.upcast()), Err(e) => { if e.downcast_ref::<JsonValidationException>().is_some() || e.downcast_ref::<RuntimeException>().is_some() diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index ac7c055..c9de808 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -15,7 +15,7 @@ use shirabe_php_shim::{ use crate::autoload::AutoloadGenerator; use crate::cache::Cache; -use crate::composer::{ComposerWeakHandle, PartialOrFullComposer}; +use crate::composer::{ComposerHandle, ComposerWeakHandle, PartialOrFullComposer}; use crate::composer::{PartialComposerHandle, PartialComposerWeakHandle}; use crate::config::Config; use crate::config::JsonConfigSource; @@ -1262,7 +1262,7 @@ impl Factory { config: Option<LocalConfigInput>, disable_plugins: DisablePlugins, disable_scripts: bool, - ) -> anyhow::Result<PartialComposerHandle> { + ) -> anyhow::Result<ComposerHandle> { let factory = Self; // for BC reasons, if a config is passed in either as array or a path that is not the default composer.json path @@ -1285,14 +1285,13 @@ impl Factory { let composer = factory.create_composer(io, config, disable_plugins, None, true, disable_scripts)?; - if !composer.is_full() { - // TODO(phase-b): unreachable when fullLoad=true; downcasting needs design. - return Err(anyhow::anyhow!(RuntimeException { + // fullLoad=true guarantees a full Composer; narrow PartialComposer -> Composer (PHP `: Composer`). + composer.as_full().ok_or_else(|| { + anyhow::anyhow!(RuntimeException { message: "Composer expected with fullLoad=true".to_string(), code: 0, - })); - } - Ok(composer) + }) + }) } /// If you are calling this in a plugin, you probably should instead use `$composer->getLoop()->getHttpDownloader()` diff --git a/crates/shirabe/src/installed_versions.rs b/crates/shirabe/src/installed_versions.rs index 77d653f..fb6e8bf 100644 --- a/crates/shirabe/src/installed_versions.rs +++ b/crates/shirabe/src/installed_versions.rs @@ -423,6 +423,16 @@ impl InstalledVersions { *INSTALLED_IS_LOCAL_DIR.lock().unwrap() = false; } + /// PHP mutates the private static `$selfDir` via Reflection. Rust exposes a mutating function. + pub fn set_self_dir(dir: String) { + *SELF_DIR.lock().unwrap() = Some(dir); + } + + /// PHP mutates the private static `$installedIsLocalDir` via Reflection. Rust exposes a mutating function. + pub fn set_installed_is_local_dir(value: bool) { + *INSTALLED_IS_LOCAL_DIR.lock().unwrap() = value; + } + /// @return string fn get_self_dir() -> String { let mut self_dir = SELF_DIR.lock().unwrap(); diff --git a/crates/shirabe/src/io/buffer_io.rs b/crates/shirabe/src/io/buffer_io.rs index 0696c1c..7c999f8 100644 --- a/crates/shirabe/src/io/buffer_io.rs +++ b/crates/shirabe/src/io/buffer_io.rs @@ -100,14 +100,17 @@ impl BufferIO { } pub fn set_user_inputs(&mut self, inputs: Vec<String>) -> Result<()> { - // TODO(phase-b): downcast Box<dyn InputInterface> to StreamableInputInterface. - // as_any/set_stream are not yet exposed on the InputInterface trait object. + // PHP: `if (!$this->input instanceof StreamableInputInterface) { throw ... }` + // `$this->input->setStream($this->createStream($inputs)); $this->input->setInteractive(true);` + // + // TODO(phase-b): blocked on the symfony console crate-path duplication (see `new`/`get_output`): + // `StreamableInputInterface` lives under `symfony::console::input`, but ConsoleIO's input is + // a `symfony::component::console::input::InputInterface` from a separate, unrelated trait + // tree, so the `instanceof` downcast cannot be expressed until those trees are unified. let _ = inputs; - let _ = |i: &Box<dyn InputInterface>| -> bool { - let _ = i; - false - }; - todo!("port BufferIO::set_user_inputs once StreamableInputInterface downcast is available") + todo!( + "BufferIO::set_user_inputs: blocked on unifying symfony::console / symfony::component::console input trait trees" + ) } fn create_stream(&self, inputs: Vec<String>) -> Result<PhpMixed> { diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs index 965cc55..d2aeac6 100644 --- a/crates/shirabe/src/package/loader/array_loader.rs +++ b/crates/shirabe/src/package/loader/array_loader.rs @@ -148,6 +148,10 @@ fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> { } impl LoaderInterface for ArrayLoader { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn load( &self, mut config: IndexMap<String, PhpMixed>, diff --git a/crates/shirabe/src/package/loader/loader_interface.rs b/crates/shirabe/src/package/loader/loader_interface.rs index 586905f..6cf728e 100644 --- a/crates/shirabe/src/package/loader/loader_interface.rs +++ b/crates/shirabe/src/package/loader/loader_interface.rs @@ -10,4 +10,6 @@ pub trait LoaderInterface: std::fmt::Debug { config: IndexMap<String, PhpMixed>, class: Option<String>, ) -> anyhow::Result<PackageInterfaceHandle>; + + fn as_any(&self) -> &dyn std::any::Any; } diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index 4e752d9..bb78886 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -341,14 +341,8 @@ impl FilesystemRepository { // make sure the selfDir matches the expected data at runtime if the class was loaded from the vendor dir, as it may have been // loaded from the Composer sources, causing packages to appear twice in that case if the installed.php is loaded in addition to the // in memory loaded data from above - // TODO(phase-b): Reflection API on static properties — confirm porting approach with user - let _attempt: Result<()> = (|| -> Result<()> { - todo!( - "ReflectionProperty(Composer\\InstalledVersions::class, 'selfDir')->setValue(null, strtr($repoDir, '\\\\', '/'))" - ); - // (the second reflection block sets installedIsLocalDir = true) - })(); - // PHP: catches \ReflectionException and rethrows if not "Property does not exist" + InstalledVersions::set_self_dir(repo_dir.replace('\\', "/")); + InstalledVersions::set_installed_is_local_dir(true); } } diff --git a/crates/shirabe/src/repository/handle.rs b/crates/shirabe/src/repository/handle.rs index 0d02b83..aa79d5e 100644 --- a/crates/shirabe/src/repository/handle.rs +++ b/crates/shirabe/src/repository/handle.rs @@ -66,6 +66,24 @@ impl RepositoryInterfaceHandle { self.0.borrow().as_any().is::<T>() } + /// Downcasts the shared handle to a concrete repository type, preserving shared ownership. + pub fn downcast_rc<T: RepositoryInterface + 'static>(&self) -> Option<Rc<RefCell<T>>> { + if self.0.borrow().as_any().is::<T>() { + let rc = self.0.clone(); + let ptr = Rc::into_raw(rc) as *const RefCell<T>; + // SAFETY: is::<T>() proved the value is `T`, and handles are always allocated as + // `Rc::new(RefCell::new(concrete))`, so the layout matches `RcBox<RefCell<T>>`. + Some(unsafe { Rc::from_raw(ptr) }) + } else { + None + } + } + + pub fn as_platform_repository(&self) -> Option<PlatformRepositoryHandle> { + self.downcast_rc::<PlatformRepository>() + .map(PlatformRepositoryHandle::from_rc) + } + pub fn count(&self) -> i64 { self.0.borrow().count() } diff --git a/crates/shirabe/src/repository/installed_repository.rs b/crates/shirabe/src/repository/installed_repository.rs index 2bd6fc6..8b611ae 100644 --- a/crates/shirabe/src/repository/installed_repository.rs +++ b/crates/shirabe/src/repository/installed_repository.rs @@ -48,6 +48,10 @@ impl InstalledRepository { this } + pub fn get_repositories(&self) -> &Vec<RepositoryInterfaceHandle> { + self.inner.get_repositories() + } + pub fn find_packages_with_replacers_and_providers( &self, name: &str, diff --git a/crates/shirabe/src/repository/vcs/forgejo_driver.rs b/crates/shirabe/src/repository/vcs/forgejo_driver.rs index c93ee70..3c1366d 100644 --- a/crates/shirabe/src/repository/vcs/forgejo_driver.rs +++ b/crates/shirabe/src/repository/vcs/forgejo_driver.rs @@ -34,6 +34,23 @@ pub struct ForgejoDriver { } impl ForgejoDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + forgejo_url: None, + repository_data: None, + git_driver: None, + tags: None, + branches: None, + } + } + pub fn initialize(&mut self) -> Result<()> { let forgejo_url = ForgejoUrl::create(&self.inner.url)?; self.inner.origin_url = forgejo_url.origin_url.clone(); diff --git a/crates/shirabe/src/repository/vcs/fossil_driver.rs b/crates/shirabe/src/repository/vcs/fossil_driver.rs index 5b7ccc1..0ee20da 100644 --- a/crates/shirabe/src/repository/vcs/fossil_driver.rs +++ b/crates/shirabe/src/repository/vcs/fossil_driver.rs @@ -26,6 +26,23 @@ pub struct FossilDriver { } impl FossilDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + tags: None, + branches: None, + root_identifier: None, + repo_file: None, + checkout_dir: String::new(), + } + } + pub fn initialize(&mut self) -> anyhow::Result<()> { // Make sure fossil is installed and reachable. self.check_fossil()?; diff --git a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs index 2a8d374..3f0be13 100644 --- a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs +++ b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs @@ -58,6 +58,32 @@ pub struct GitBitbucketDriver { } impl GitBitbucketDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + owner: String::new(), + repository: String::new(), + has_issues: false, + root_identifier: None, + tags: None, + branches: None, + branches_url: String::new(), + tags_url: String::new(), + home_url: String::new(), + website: String::new(), + clone_https_url: String::new(), + repo_data: IndexMap::new(), + fallback_driver: None, + vcs_type: None, + } + } + /// @inheritDoc pub fn initialize(&mut self) -> Result<()> { let mut m: indexmap::IndexMap<CaptureKey, String> = indexmap::IndexMap::new(); diff --git a/crates/shirabe/src/repository/vcs/github_driver.rs b/crates/shirabe/src/repository/vcs/github_driver.rs index b3d5e05..6fc50ee 100644 --- a/crates/shirabe/src/repository/vcs/github_driver.rs +++ b/crates/shirabe/src/repository/vcs/github_driver.rs @@ -47,6 +47,30 @@ pub struct GitHubDriver { } impl GitHubDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + owner: String::new(), + repository: String::new(), + tags: None, + branches: None, + root_identifier: String::new(), + repo_data: None, + has_issues: false, + is_private: false, + is_archived: false, + funding_info: None, + allow_git_fallback: true, + git_driver: None, + } + } + pub fn initialize(&mut self) -> Result<()> { let mut match_: IndexMap<CaptureKey, String> = IndexMap::new(); if !Preg::is_match_strict_groups3( diff --git a/crates/shirabe/src/repository/vcs/gitlab_driver.rs b/crates/shirabe/src/repository/vcs/gitlab_driver.rs index 84a569f..84961f5 100644 --- a/crates/shirabe/src/repository/vcs/gitlab_driver.rs +++ b/crates/shirabe/src/repository/vcs/gitlab_driver.rs @@ -56,6 +56,29 @@ pub struct GitLabDriver { impl GitLabDriver { pub const URL_REGEX: &'static str = r##"#^(?:(?P<scheme>https?)://(?P<domain>.+?)(?::(?P<port>[0-9]+))?/|git@(?P<domain2>[^:]+):)(?P<parts>.+)/(?P<repo>[^/]+?)(?:\.git|/)?$#"##; + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + scheme: String::new(), + namespace: String::new(), + repository: String::new(), + project: None, + commits: IndexMap::new(), + tags: None, + branches: None, + git_driver: None, + protocol: String::new(), + is_private: true, + has_nonstandard_origin: false, + } + } + /// Extracts information from the repository url. /// /// SSH urls use https by default. Set "secure-http": false on the repository config to use http instead. diff --git a/crates/shirabe/src/repository/vcs/hg_driver.rs b/crates/shirabe/src/repository/vcs/hg_driver.rs index 1686f5d..c35a574 100644 --- a/crates/shirabe/src/repository/vcs/hg_driver.rs +++ b/crates/shirabe/src/repository/vcs/hg_driver.rs @@ -25,6 +25,22 @@ pub struct HgDriver { } impl HgDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + tags: None, + branches: None, + root_identifier: None, + repo_dir: String::new(), + } + } + pub fn initialize(&mut self) -> anyhow::Result<()> { if Filesystem::is_local_path(&self.inner.url) { self.repo_dir = self.inner.url.clone(); diff --git a/crates/shirabe/src/repository/vcs/mod.rs b/crates/shirabe/src/repository/vcs/mod.rs index 715d2e5..5c60e0a 100644 --- a/crates/shirabe/src/repository/vcs/mod.rs +++ b/crates/shirabe/src/repository/vcs/mod.rs @@ -21,3 +21,134 @@ pub use perforce_driver::*; pub use svn_driver::*; pub use vcs_driver::*; pub use vcs_driver_interface::*; + +use crate::config::Config; +use crate::io::IOInterface; +use crate::util::{HttpDownloader, ProcessExecutor}; +use indexmap::IndexMap; +use shirabe_php_shim::PhpMixed; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VcsDriverKind { + GitHub, + GitLab, + GitBitbucket, + Forgejo, + Git, + Hg, + Perforce, + Fossil, + Svn, +} + +impl VcsDriverKind { + pub fn instantiate( + self, + repo_config: IndexMap<String, PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + ) -> Box<dyn VcsDriverInterface> { + match self { + VcsDriverKind::GitHub => Box::new(GitHubDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::GitLab => Box::new(GitLabDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::GitBitbucket => Box::new(GitBitbucketDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::Forgejo => Box::new(ForgejoDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::Git => Box::new(GitDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::Hg => Box::new(HgDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::Perforce => Box::new(PerforceDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::Fossil => Box::new(FossilDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + VcsDriverKind::Svn => Box::new(SvnDriver::new( + repo_config, + io, + config, + http_downloader, + process, + )), + } + } + + pub fn supports( + self, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + url: &str, + deep: bool, + ) -> anyhow::Result<bool> { + match self { + VcsDriverKind::GitHub => GitHubDriver::supports(io, config, url, deep), + VcsDriverKind::GitLab => GitLabDriver::supports(io, config, url, deep), + VcsDriverKind::GitBitbucket => GitBitbucketDriver::supports(io, config, url, deep), + VcsDriverKind::Forgejo => ForgejoDriver::supports(io, config, url, deep), + VcsDriverKind::Git => GitDriver::supports(io, config, url, deep), + VcsDriverKind::Hg => HgDriver::supports(io, config, url, deep), + VcsDriverKind::Perforce => PerforceDriver::supports(io, config, url, deep), + VcsDriverKind::Fossil => FossilDriver::supports(io, config, url, deep), + VcsDriverKind::Svn => SvnDriver::supports(io, config, url, deep), + } + } + + /// PHP fully-qualified `class-string`, used as the fallback driver name in `getRepoName()`. + pub fn php_class_name(self) -> &'static str { + match self { + VcsDriverKind::GitHub => "Composer\\Repository\\Vcs\\GitHubDriver", + VcsDriverKind::GitLab => "Composer\\Repository\\Vcs\\GitLabDriver", + VcsDriverKind::GitBitbucket => "Composer\\Repository\\Vcs\\GitBitbucketDriver", + VcsDriverKind::Forgejo => "Composer\\Repository\\Vcs\\ForgejoDriver", + VcsDriverKind::Git => "Composer\\Repository\\Vcs\\GitDriver", + VcsDriverKind::Hg => "Composer\\Repository\\Vcs\\HgDriver", + VcsDriverKind::Perforce => "Composer\\Repository\\Vcs\\PerforceDriver", + VcsDriverKind::Fossil => "Composer\\Repository\\Vcs\\FossilDriver", + VcsDriverKind::Svn => "Composer\\Repository\\Vcs\\SvnDriver", + } + } +} diff --git a/crates/shirabe/src/repository/vcs/perforce_driver.rs b/crates/shirabe/src/repository/vcs/perforce_driver.rs index d653869..0fe0f6f 100644 --- a/crates/shirabe/src/repository/vcs/perforce_driver.rs +++ b/crates/shirabe/src/repository/vcs/perforce_driver.rs @@ -21,6 +21,21 @@ pub struct PerforceDriver { } impl PerforceDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + depot: String::new(), + branch: String::new(), + perforce: None, + } + } + pub fn initialize(&mut self) -> anyhow::Result<()> { self.depot = self .inner diff --git a/crates/shirabe/src/repository/vcs/svn_driver.rs b/crates/shirabe/src/repository/vcs/svn_driver.rs index 9b2ea47..377d698 100644 --- a/crates/shirabe/src/repository/vcs/svn_driver.rs +++ b/crates/shirabe/src/repository/vcs/svn_driver.rs @@ -50,6 +50,28 @@ pub struct SvnDriver { } impl SvnDriver { + pub fn new( + repo_config: IndexMap<String, shirabe_php_shim::PhpMixed>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<crate::util::HttpDownloader>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::ProcessExecutor>>, + ) -> Self { + Self { + inner: VcsDriverBase::new(repo_config, io, config, http_downloader, process), + base_url: String::new(), + tags: None, + branches: None, + root_identifier: None, + trunk_path: Some("trunk".to_string()), + branches_path: "branches".to_string(), + tags_path: "tags".to_string(), + package_path: String::new(), + cache_credentials: true, + util: None, + } + } + pub fn initialize(&mut self) -> Result<()> { let normalized = Self::normalize_url(&self.inner.url); self.inner.url = normalized.trim_end_matches('/').to_string(); diff --git a/crates/shirabe/src/repository/vcs_repository.rs b/crates/shirabe/src/repository/vcs_repository.rs index 4c2492a..7817172 100644 --- a/crates/shirabe/src/repository/vcs_repository.rs +++ b/crates/shirabe/src/repository/vcs_repository.rs @@ -4,10 +4,7 @@ use crate::io::io_interface; use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; -use shirabe_php_shim::{ - InvalidArgumentException, PhpMixed, array_search_mixed, count, get_class, in_array, - str_replace, strpos, -}; +use shirabe_php_shim::{InvalidArgumentException, PhpMixed, in_array, str_replace, strpos}; use shirabe_semver::constraint::AnyConstraint; use shirabe_semver::constraint::SimpleConstraint; @@ -26,12 +23,14 @@ use crate::repository::ConfigurableRepositoryInterface; use crate::repository::InvalidRepositoryException; use crate::repository::RepositoryInterface; use crate::repository::vcs::VcsDriverInterface; +use crate::repository::vcs::VcsDriverKind; use crate::repository::{VersionCacheInterface, VersionCacheResult}; use crate::util::HttpDownloader; use crate::util::Platform; use crate::util::ProcessExecutor; use crate::util::Url; +// TODO(phase-c): the driver registration should be refactored later. #[derive(Debug)] pub struct VcsRepository { pub(crate) inner: ArrayRepository, @@ -62,9 +61,12 @@ pub struct VcsRepository { /// @var bool pub(crate) branch_error_occurred: bool, /// @var array<string, class-string<VcsDriverInterface>> - drivers: IndexMap<String, String>, + drivers: IndexMap<String, VcsDriverKind>, /// @var ?VcsDriverInterface driver: Option<Box<dyn VcsDriverInterface>>, + /// Kind of the resolved `driver`, used by `get_repo_name` to recover the driver type + /// (PHP `array_search(get_class($driver), $this->drivers)`). + driver_kind: Option<VcsDriverKind>, /// @var ?VersionCacheInterface version_cache: Option<Box<dyn VersionCacheInterface>>, /// @var list<string> @@ -91,53 +93,23 @@ impl VcsRepository { http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, - drivers: Option<IndexMap<String, String>>, + drivers: Option<IndexMap<String, VcsDriverKind>>, version_cache: Option<Box<dyn VersionCacheInterface>>, ) -> Result<Self> { let inner = ArrayRepository::new(vec![])?; let drivers = drivers.unwrap_or_else(|| { - let mut m: IndexMap<String, String> = IndexMap::new(); - m.insert( - "github".to_string(), - "Composer\\Repository\\Vcs\\GitHubDriver".to_string(), - ); - m.insert( - "gitlab".to_string(), - "Composer\\Repository\\Vcs\\GitLabDriver".to_string(), - ); - m.insert( - "bitbucket".to_string(), - "Composer\\Repository\\Vcs\\GitBitbucketDriver".to_string(), - ); - m.insert( - "git-bitbucket".to_string(), - "Composer\\Repository\\Vcs\\GitBitbucketDriver".to_string(), - ); - m.insert( - "forgejo".to_string(), - "Composer\\Repository\\Vcs\\ForgejoDriver".to_string(), - ); - m.insert( - "git".to_string(), - "Composer\\Repository\\Vcs\\GitDriver".to_string(), - ); - m.insert( - "hg".to_string(), - "Composer\\Repository\\Vcs\\HgDriver".to_string(), - ); - m.insert( - "perforce".to_string(), - "Composer\\Repository\\Vcs\\PerforceDriver".to_string(), - ); - m.insert( - "fossil".to_string(), - "Composer\\Repository\\Vcs\\FossilDriver".to_string(), - ); + let mut m: IndexMap<String, VcsDriverKind> = IndexMap::new(); + m.insert("github".to_string(), VcsDriverKind::GitHub); + m.insert("gitlab".to_string(), VcsDriverKind::GitLab); + m.insert("bitbucket".to_string(), VcsDriverKind::GitBitbucket); + m.insert("git-bitbucket".to_string(), VcsDriverKind::GitBitbucket); + m.insert("forgejo".to_string(), VcsDriverKind::Forgejo); + m.insert("git".to_string(), VcsDriverKind::Git); + m.insert("hg".to_string(), VcsDriverKind::Hg); + m.insert("perforce".to_string(), VcsDriverKind::Perforce); + m.insert("fossil".to_string(), VcsDriverKind::Fossil); // svn must be last because identifying a subversion server for sure is practically impossible - m.insert( - "svn".to_string(), - "Composer\\Repository\\Vcs\\SvnDriver".to_string(), - ); + m.insert("svn".to_string(), VcsDriverKind::Svn); m }); @@ -178,6 +150,7 @@ impl VcsRepository { branch_error_occurred: false, drivers, driver: None, + driver_kind: None, version_cache, empty_references: vec![], version_transport_exceptions: IndexMap::new(), @@ -186,22 +159,18 @@ impl VcsRepository { } pub fn get_repo_name(&mut self) -> String { - // Ensure the driver is initialized; we do not need a handle here. + // Ensure the driver is resolved so `driver_kind` is populated. let _ = self.get_driver().expect("driver should be available"); - let driver_class = get_class(&PhpMixed::Null); // TODO(phase-b): obtain runtime class name of $driver - let drivers_snapshot: IndexMap<String, Box<PhpMixed>> = self - .drivers - .iter() - .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone())))) - .collect(); - let driver_type = array_search_mixed( - &PhpMixed::String(driver_class.clone()), - &PhpMixed::Array(drivers_snapshot), - false, - ) - .map(|v| v.as_string().unwrap_or("").to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or(driver_class); + // PHP: array_search(get_class($driver), $this->drivers), falling back to the class name. + let driver_type = match self.driver_kind { + Some(kind) => self + .drivers + .iter() + .find(|(_, v)| **v == kind) + .map(|(name, _)| name.clone()) + .unwrap_or_else(|| kind.php_class_name().to_string()), + None => String::new(), + }; format!( "vcs repo ({} {})", @@ -223,39 +192,56 @@ impl VcsRepository { return self.driver.as_mut(); } - if let Some(_class) = self.drivers.get(&self.r#type).cloned() { - // TODO(phase-b): dynamic class-string instantiation `new $class(...)` - let driver: Option<Box<dyn VcsDriverInterface>> = None; - if let Some(mut d) = driver { - let _ = d.initialize(); - self.driver = Some(d); - } + if let Some(kind) = self.drivers.get(&self.r#type).copied() { + let mut driver = kind.instantiate( + self.repo_config.clone(), + self.io.clone(), + self.config.clone(), + self.http_downloader.clone(), + self.process_executor.clone(), + ); + let _ = driver.initialize(); + self.driver = Some(driver); + self.driver_kind = Some(kind); return self.driver.as_mut(); } - for (_, _driver_class) in self.drivers.iter() { - // TODO(phase-b): static-method dispatch on class-string: `$driver::supports(...)` - let supports = false; - if supports { - // TODO(phase-b): dynamic class-string instantiation `new $driver(...)` - let d: Option<Box<dyn VcsDriverInterface>> = None; - if let Some(mut d) = d { - let _ = d.initialize(); - self.driver = Some(d); - } + let kinds: Vec<VcsDriverKind> = self.drivers.values().copied().collect(); + + for kind in &kinds { + if kind + .supports(self.io.clone(), self.config.clone(), &self.url, false) + .unwrap_or(false) + { + let mut driver = kind.instantiate( + self.repo_config.clone(), + self.io.clone(), + self.config.clone(), + self.http_downloader.clone(), + self.process_executor.clone(), + ); + let _ = driver.initialize(); + self.driver = Some(driver); + self.driver_kind = Some(*kind); return self.driver.as_mut(); } } - for (_, _driver_class) in self.drivers.iter() { - // TODO(phase-b): static-method dispatch on class-string: `$driver::supports(..., true)` - let supports = false; - if supports { - let d: Option<Box<dyn VcsDriverInterface>> = None; - if let Some(mut d) = d { - let _ = d.initialize(); - self.driver = Some(d); - } + for kind in &kinds { + if kind + .supports(self.io.clone(), self.config.clone(), &self.url, true) + .unwrap_or(false) + { + let mut driver = kind.instantiate( + self.repo_config.clone(), + self.io.clone(), + self.config.clone(), + self.http_downloader.clone(), + self.process_executor.clone(), + ); + let _ = driver.initialize(); + self.driver = Some(driver); + self.driver_kind = Some(*kind); return self.driver.as_mut(); } } @@ -706,14 +692,24 @@ impl VcsRepository { .as_ref() .unwrap() .load(package_data.clone(), None)?; - // TODO(phase-b): `$this->loader instanceof ValidatingArrayLoader` downcast - let loader_as_validating: Option<&ValidatingArrayLoader> = None; + // PHP: `$this->loader instanceof ValidatingArrayLoader`. + // TODO(phase-c): ValidatingArrayLoader does not implement LoaderInterface yet (its + // `load` needs `&mut self`, requiring a LoaderInterface redesign), so it can never be + // stored in `self.loader` and this downcast is always None. Production never calls + // setLoader so the default ArrayLoader matches upstream, but the InvalidPackageException + // path stays dead until the trait is reworked. + let loader_as_validating = self + .loader + .as_ref() + .and_then(|l| l.as_any().downcast_ref::<ValidatingArrayLoader>()); if let Some(validating) = loader_as_validating { - if count(&PhpMixed::Null) > 0 { - let _ = validating; - return Err( - InvalidPackageException::new(vec![], vec![], package_data).into() - ); + if !validating.get_warnings().is_empty() { + return Err(InvalidPackageException::new( + validating.get_errors().to_vec(), + validating.get_warnings().to_vec(), + package_data, + ) + .into()); } } self.inner.add_package(package)?; |
