aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-08 01:54:56 +0900
committernsfisis <nsfisis@gmail.com>2026-06-08 01:54:56 +0900
commit318ea948f5932dfa7942081a269d62fd7161a9bf (patch)
tree0fff2fe818f87a20dea89a51901f9e16071f2f53 /crates/shirabe/src
parentf232d7f9d2936ef84bd904cacd21c12cb7012b34 (diff)
downloadphp-shirabe-318ea948f5932dfa7942081a269d62fd7161a9bf.tar.gz
php-shirabe-318ea948f5932dfa7942081a269d62fd7161a9bf.tar.zst
php-shirabe-318ea948f5932dfa7942081a269d62fd7161a9bf.zip
feat(phase-c): resolve reflection/downcast phase-b TODOs
Resolve category F phase-b TODOs (class-string, instanceof, get_class, method_exists, __FILE__, Reflection API, downcast). - VcsRepository: dispatch drivers through a VcsDriverKind enum (instantiate/supports/php_class_name) and add constructors to the concrete VCS drivers - repository downcasts via RepositoryInterfaceHandle::downcast_rc and as_any (init/show commands, vcs ValidatingArrayLoader) - BaseCommand::is_self_update_command override replaces an instanceof - Factory::create narrows PartialComposer to ComposerHandle via as_full - InstalledVersions gains set_self_dir/set_installed_is_local_dir, replacing Reflection-based static property mutation - ClassLoader::as_array_iter ports the PHP (array) cast - drop the unnecessary __FILE__ phar branch in self-update application get_class(command) reclassified TODO(plugin); buffer_io StreamableInputInterface downcast and the ValidatingArrayLoader trait redesign left as tracked TODOs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/advisory/auditor.rs13
-rw-r--r--crates/shirabe/src/autoload/class_loader.rs102
-rw-r--r--crates/shirabe/src/command/base_command.rs12
-rw-r--r--crates/shirabe/src/command/init_command.rs24
-rw-r--r--crates/shirabe/src/command/self_update_command.rs44
-rw-r--r--crates/shirabe/src/command/show_command.rs37
-rw-r--r--crates/shirabe/src/console/application.rs12
-rw-r--r--crates/shirabe/src/factory.rs15
-rw-r--r--crates/shirabe/src/installed_versions.rs10
-rw-r--r--crates/shirabe/src/io/buffer_io.rs17
-rw-r--r--crates/shirabe/src/package/loader/array_loader.rs4
-rw-r--r--crates/shirabe/src/package/loader/loader_interface.rs2
-rw-r--r--crates/shirabe/src/repository/filesystem_repository.rs10
-rw-r--r--crates/shirabe/src/repository/handle.rs18
-rw-r--r--crates/shirabe/src/repository/installed_repository.rs4
-rw-r--r--crates/shirabe/src/repository/vcs/forgejo_driver.rs17
-rw-r--r--crates/shirabe/src/repository/vcs/fossil_driver.rs17
-rw-r--r--crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs26
-rw-r--r--crates/shirabe/src/repository/vcs/github_driver.rs24
-rw-r--r--crates/shirabe/src/repository/vcs/gitlab_driver.rs23
-rw-r--r--crates/shirabe/src/repository/vcs/hg_driver.rs16
-rw-r--r--crates/shirabe/src/repository/vcs/mod.rs131
-rw-r--r--crates/shirabe/src/repository/vcs/perforce_driver.rs15
-rw-r--r--crates/shirabe/src/repository/vcs/svn_driver.rs22
-rw-r--r--crates/shirabe/src/repository/vcs_repository.rs186
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)?;