aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/repository
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 20:25:39 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 20:25:39 +0900
commit7f12c5caa3761240a611bbd1fa825e319e908f8f (patch)
tree8e38c0ceb44d7337dee55c3c8962d786bb827fb4 /crates/shirabe/src/repository
parente813e25cf6e41d89375e842f8abc7d6021d1cb21 (diff)
downloadphp-shirabe-7f12c5caa3761240a611bbd1fa825e319e908f8f.tar.gz
php-shirabe-7f12c5caa3761240a611bbd1fa825e319e908f8f.tar.zst
php-shirabe-7f12c5caa3761240a611bbd1fa825e319e908f8f.zip
feat(port): port VcsRepository.php
Diffstat (limited to 'crates/shirabe/src/repository')
-rw-r--r--crates/shirabe/src/repository/vcs_repository.rs963
1 files changed, 963 insertions, 0 deletions
diff --git a/crates/shirabe/src/repository/vcs_repository.rs b/crates/shirabe/src/repository/vcs_repository.rs
index b8d29f1..fcfd101 100644
--- a/crates/shirabe/src/repository/vcs_repository.rs
+++ b/crates/shirabe/src/repository/vcs_repository.rs
@@ -1 +1,964 @@
//! ref: composer/src/Composer/Repository/VcsRepository.php
+
+use anyhow::Result;
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_php_shim::{
+ array_search_mixed, count, get_class, in_array, str_replace, strpos, InvalidArgumentException,
+ PhpMixed,
+};
+use shirabe_semver::constraint::constraint::Constraint;
+
+use crate::config::Config;
+use crate::downloader::transport_exception::TransportException;
+use crate::event_dispatcher::event_dispatcher::EventDispatcher;
+use crate::io::io_interface::IOInterface;
+use crate::package::base_package::BasePackage;
+use crate::package::loader::array_loader::ArrayLoader;
+use crate::package::loader::invalid_package_exception::InvalidPackageException;
+use crate::package::loader::loader_interface::LoaderInterface;
+use crate::package::loader::validating_array_loader::ValidatingArrayLoader;
+use crate::package::version::version_parser::VersionParser;
+use crate::repository::array_repository::ArrayRepository;
+use crate::repository::configurable_repository_interface::ConfigurableRepositoryInterface;
+use crate::repository::invalid_repository_exception::InvalidRepositoryException;
+use crate::repository::vcs::vcs_driver_interface::VcsDriverInterface;
+use crate::repository::version_cache_interface::VersionCacheInterface;
+use crate::util::http_downloader::HttpDownloader;
+use crate::util::platform::Platform;
+use crate::util::process_executor::ProcessExecutor;
+use crate::util::url::Url;
+
+#[derive(Debug)]
+pub struct VcsRepository {
+ pub(crate) inner: ArrayRepository,
+ /// @var string
+ pub(crate) url: String,
+ /// @var ?string
+ pub(crate) package_name: Option<String>,
+ /// @var bool
+ pub(crate) is_verbose: bool,
+ /// @var bool
+ pub(crate) is_very_verbose: bool,
+ /// @var IOInterface
+ pub(crate) io: Box<dyn IOInterface>,
+ /// @var Config
+ pub(crate) config: Config,
+ /// @var VersionParser
+ pub(crate) version_parser: Option<VersionParser>,
+ /// @var string
+ pub(crate) r#type: String,
+ /// @var ?LoaderInterface
+ pub(crate) loader: Option<Box<dyn LoaderInterface>>,
+ /// @var array<string, mixed>
+ pub(crate) repo_config: IndexMap<String, PhpMixed>,
+ /// @var HttpDownloader
+ pub(crate) http_downloader: HttpDownloader,
+ /// @var ProcessExecutor
+ pub(crate) process_executor: ProcessExecutor,
+ /// @var bool
+ pub(crate) branch_error_occurred: bool,
+ /// @var array<string, class-string<VcsDriverInterface>>
+ drivers: IndexMap<String, String>,
+ /// @var ?VcsDriverInterface
+ driver: Option<Box<dyn VcsDriverInterface>>,
+ /// @var ?VersionCacheInterface
+ version_cache: Option<Box<dyn VersionCacheInterface>>,
+ /// @var list<string>
+ empty_references: Vec<String>,
+ /// @var array<'tags'|'branches', array<string, TransportException>>
+ version_transport_exceptions: IndexMap<String, IndexMap<String, TransportException>>,
+ /// @var ?EventDispatcher (preserved for plugin events)
+ _dispatcher: Option<EventDispatcher>,
+}
+
+impl ConfigurableRepositoryInterface for VcsRepository {}
+
+impl VcsRepository {
+ /// @param array{url: string, type?: string}&array<string, mixed> $repoConfig
+ /// @param array<string, class-string<VcsDriverInterface>>|null $drivers
+ pub fn new(
+ mut repo_config: IndexMap<String, PhpMixed>,
+ io: Box<dyn IOInterface>,
+ config: Config,
+ http_downloader: HttpDownloader,
+ dispatcher: Option<EventDispatcher>,
+ process: Option<ProcessExecutor>,
+ drivers: Option<IndexMap<String, String>>,
+ 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(),
+ );
+ // 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
+ });
+
+ let url = Platform::expand_path(
+ repo_config.get("url").and_then(|v| v.as_string()).unwrap_or(""),
+ );
+ repo_config.insert("url".to_string(), PhpMixed::String(url.clone()));
+ let r#type = repo_config
+ .get("type")
+ .and_then(|v| v.as_string())
+ .unwrap_or("vcs")
+ .to_string();
+ let is_verbose = io.is_verbose();
+ let is_very_verbose = io.is_very_verbose();
+ let process_executor = process.unwrap_or_else(|| ProcessExecutor::new(Some(Box::new(&*io)), None));
+
+ Ok(Self {
+ inner,
+ url,
+ package_name: None,
+ is_verbose,
+ is_very_verbose,
+ io,
+ config,
+ version_parser: None,
+ r#type,
+ loader: None,
+ repo_config,
+ http_downloader,
+ process_executor,
+ branch_error_occurred: false,
+ drivers,
+ driver: None,
+ version_cache,
+ empty_references: vec![],
+ version_transport_exceptions: IndexMap::new(),
+ _dispatcher: dispatcher,
+ })
+ }
+
+ pub fn get_repo_name(&mut self) -> String {
+ let driver = 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 driver_type = array_search_mixed(
+ &PhpMixed::String(driver_class.clone()),
+ &PhpMixed::Array(
+ self.drivers
+ .iter()
+ .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone()))))
+ .collect(),
+ ),
+ false,
+ )
+ .map(|v| v.as_string().unwrap_or("").to_string())
+ .filter(|s| !s.is_empty())
+ .unwrap_or(driver_class);
+ let _ = driver;
+
+ format!(
+ "vcs repo ({} {})",
+ driver_type,
+ Url::sanitize(&self.url)
+ )
+ }
+
+ pub fn get_repo_config(&self) -> &IndexMap<String, PhpMixed> {
+ &self.repo_config
+ }
+
+ pub fn set_loader(&mut self, loader: Box<dyn LoaderInterface>) {
+ self.loader = Some(loader);
+ }
+
+ pub fn get_driver(&mut self) -> Option<&mut Box<dyn VcsDriverInterface>> {
+ if self.driver.is_some() {
+ 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);
+ }
+ 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);
+ }
+ 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);
+ }
+ return self.driver.as_mut();
+ }
+ }
+
+ None
+ }
+
+ pub fn had_invalid_branches(&self) -> bool {
+ self.branch_error_occurred
+ }
+
+ /// @return list<string>
+ pub fn get_empty_references(&self) -> &Vec<String> {
+ &self.empty_references
+ }
+
+ /// @return array<'tags'|'branches', array<string, TransportException>>
+ pub fn get_version_transport_exceptions(
+ &self,
+ ) -> &IndexMap<String, IndexMap<String, TransportException>> {
+ &self.version_transport_exceptions
+ }
+
+ pub fn initialize(&mut self) -> Result<()> {
+ self.inner.initialize();
+
+ let is_verbose = self.is_verbose;
+ let is_very_verbose = self.is_very_verbose;
+
+ let driver_url = self.url.clone();
+ let driver = self.get_driver();
+ if driver.is_none() {
+ return Err(InvalidArgumentException {
+ message: format!("No driver found to handle VCS repository {}", driver_url),
+ code: 0,
+ }
+ .into());
+ }
+ // TODO(phase-b): VersionParser has no public `new`
+ self.version_parser = Some(todo!("VersionParser::new()"));
+ if self.loader.is_none() {
+ self.loader = Some(Box::new(ArrayLoader::new(
+ Some(todo!("phase-b: clone VersionParser")),
+ false,
+ )));
+ }
+
+ let mut has_root_identifier_composer_json = false;
+ let root_identifier_result = self.driver.as_mut().unwrap().get_root_identifier();
+ if let Ok(root_identifier) = root_identifier_result {
+ match self.driver.as_mut().unwrap().has_composer_file(&root_identifier) {
+ Ok(b) => {
+ has_root_identifier_composer_json = b;
+ if has_root_identifier_composer_json {
+ match self
+ .driver
+ .as_mut()
+ .unwrap()
+ .get_composer_information(&root_identifier)
+ {
+ Ok(Some(data)) => {
+ self.package_name = data
+ .get("name")
+ .and_then(|v| v.as_string())
+ .filter(|s| !s.is_empty())
+ .map(String::from);
+ }
+ Ok(None) => {}
+ Err(e) => {
+ // TODO(phase-b): unify exception handling below
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ if self.should_rethrow_transport_exception(te) {
+ return Err(e);
+ }
+ }
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<error>Skipped parsing {}, {}</error>",
+ root_identifier, e
+ ));
+ }
+ }
+ }
+ }
+ }
+ Err(e) => {
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ if self.should_rethrow_transport_exception(te) {
+ return Err(e);
+ }
+ }
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<error>Skipped parsing {}, {}</error>",
+ root_identifier, e
+ ));
+ }
+ }
+ }
+ }
+
+ let driver = self.driver.as_mut().unwrap();
+ for (tag, identifier) in driver.get_tags()? {
+ let mut tag = tag;
+ let msg = format!(
+ "Reading composer.json of <info>{}</info> (<comment>{}</comment>)",
+ self.package_name.clone().unwrap_or_else(|| self.url.clone()),
+ tag
+ );
+
+ // strip the release- prefix from tags if present
+ tag = str_replace("release-", "", &tag);
+
+ let cached_package = self.get_cached_package_version(
+ &tag,
+ &identifier,
+ is_verbose,
+ is_very_verbose,
+ false,
+ )?;
+ if let CachedPackageResult::Package(pkg) = cached_package {
+ self.inner.add_package(pkg)?;
+ continue;
+ }
+ if matches!(cached_package, CachedPackageResult::Missing) {
+ self.empty_references.push(identifier.clone());
+ continue;
+ }
+
+ let parsed_tag = self.validate_tag(&tag);
+ if parsed_tag.is_none() {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped tag {}, invalid tag name</warning>",
+ tag
+ ));
+ }
+ continue;
+ }
+ let parsed_tag = parsed_tag.unwrap();
+
+ if is_very_verbose {
+ self.io.write_error(&msg);
+ } else if is_verbose {
+ self.io.overwrite_error(
+ PhpMixed::String(msg.clone()),
+ false,
+ None,
+ IOInterface::NORMAL,
+ );
+ }
+
+ let result: Result<()> = (|| -> Result<()> {
+ let driver = self.driver.as_mut().unwrap();
+ let data_opt = driver.get_composer_information(&identifier)?;
+ if data_opt.is_none() {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped tag {}, no composer file</warning>",
+ tag
+ ));
+ }
+ self.empty_references.push(identifier.clone());
+ return Ok(());
+ }
+ let mut data = data_opt.unwrap();
+
+ // manually versioned package
+ if data.contains_key("version") {
+ let normalized = self
+ .version_parser
+ .as_ref()
+ .unwrap()
+ .normalize(
+ data.get("version").and_then(|v| v.as_string()).unwrap_or(""),
+ None,
+ )?;
+ data.insert(
+ "version_normalized".to_string(),
+ PhpMixed::String(normalized),
+ );
+ } else {
+ // auto-versioned package, read value from tag
+ data.insert("version".to_string(), PhpMixed::String(tag.clone()));
+ data.insert(
+ "version_normalized".to_string(),
+ PhpMixed::String(parsed_tag.clone()),
+ );
+ }
+
+ // make sure tag packages have no -dev flag
+ data.insert(
+ "version".to_string(),
+ PhpMixed::String(Preg::replace(
+ r"{[.-]?dev$}i",
+ "",
+ data.get("version").and_then(|v| v.as_string()).unwrap_or(""),
+ )),
+ );
+ data.insert(
+ "version_normalized".to_string(),
+ PhpMixed::String(Preg::replace(
+ r"{(^dev-|[.-]?dev$)}i",
+ "",
+ data.get("version_normalized")
+ .and_then(|v| v.as_string())
+ .unwrap_or(""),
+ )),
+ );
+
+ // make sure tag do not contain the default-branch marker
+ data.shift_remove("default-branch");
+
+ let version_normalized = data
+ .get("version_normalized")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+
+ // broken package, version doesn't match tag
+ if version_normalized != parsed_tag {
+ if is_very_verbose {
+ if Preg::is_match(r"{(^dev-|[.-]?dev$)}i", &parsed_tag) {
+ self.io.write_error(&format!(
+ "<warning>Skipped tag {}, invalid tag name, tags can not use dev prefixes or suffixes</warning>",
+ tag
+ ));
+ } else {
+ self.io.write_error(&format!(
+ "<warning>Skipped tag {}, tag ({}) does not match version ({}) in composer.json</warning>",
+ tag, parsed_tag, version_normalized
+ ));
+ }
+ }
+ return Ok(());
+ }
+
+ let tag_package_name = self
+ .package_name
+ .clone()
+ .filter(|s| !s.is_empty())
+ .unwrap_or_else(|| {
+ data.get("name")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string()
+ });
+ if let Some(existing_package) = self.inner.find_package(
+ &tag_package_name,
+ Box::new(Constraint::new("=", &version_normalized)),
+ ) {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped tag {}, it conflicts with an another tag ({}) as both resolve to {} internally</warning>",
+ tag, existing_package.get_pretty_version(), version_normalized
+ ));
+ }
+ return Ok(());
+ }
+
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "Importing tag {} ({})",
+ tag, version_normalized
+ ));
+ }
+
+ let driver = self.driver.as_mut().unwrap();
+ let processed = self.pre_process(&**driver, data, &identifier)?;
+ let loaded = self.loader.as_ref().unwrap().load(processed, None)?;
+ self.inner.add_package(Box::new(loaded as Box<dyn _>))?;
+ Ok(())
+ })();
+ if let Err(e) = result {
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ self.version_transport_exceptions
+ .entry("tags".to_string())
+ .or_insert_with(IndexMap::new)
+ .insert(tag.clone(), te.clone());
+ if te.get_code() == 404 {
+ self.empty_references.push(identifier.clone());
+ }
+ if self.should_rethrow_transport_exception(te) {
+ return Err(e);
+ }
+ }
+ if is_very_verbose {
+ let detail = if let Some(te) = e.downcast_ref::<TransportException>() {
+ format!(
+ "no composer file was found ({} HTTP status code)",
+ te.get_code()
+ )
+ } else {
+ format!("{}", e)
+ };
+ self.io.write_error(&format!(
+ "<warning>Skipped tag {}, {}</warning>",
+ tag, detail
+ ));
+ }
+ continue;
+ }
+ }
+
+ if !is_very_verbose {
+ self.io.overwrite_error(
+ PhpMixed::String(String::new()),
+ false,
+ None,
+ IOInterface::NORMAL,
+ );
+ }
+
+ let mut branches = self.driver.as_mut().unwrap().get_branches()?;
+ // make sure the root identifier branch gets loaded first
+ let root_identifier = self.driver.as_mut().unwrap().get_root_identifier()?;
+ if has_root_identifier_composer_json && branches.contains_key(&root_identifier) {
+ let mut new_branches: IndexMap<String, String> = IndexMap::new();
+ new_branches.insert(
+ root_identifier.clone(),
+ branches.get(&root_identifier).cloned().unwrap_or_default(),
+ );
+ for (k, v) in branches {
+ if !new_branches.contains_key(&k) {
+ new_branches.insert(k, v);
+ }
+ }
+ branches = new_branches;
+ }
+
+ for (branch, identifier) in branches {
+ let msg = format!(
+ "Reading composer.json of <info>{}</info> (<comment>{}</comment>)",
+ self.package_name.clone().unwrap_or_else(|| self.url.clone()),
+ branch
+ );
+ if is_very_verbose {
+ self.io.write_error(&msg);
+ } else if is_verbose {
+ self.io.overwrite_error(
+ PhpMixed::String(msg.clone()),
+ false,
+ None,
+ IOInterface::NORMAL,
+ );
+ }
+
+ let parsed_branch_opt = self.validate_branch(&branch);
+ if parsed_branch_opt.is_none() {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped branch {}, invalid name</warning>",
+ branch
+ ));
+ }
+ continue;
+ }
+ let mut parsed_branch = parsed_branch_opt.unwrap();
+
+ // make sure branch packages have a dev flag
+ let version: String;
+ if strpos(&parsed_branch, "dev-") == Some(0)
+ || VersionParser::DEFAULT_BRANCH_ALIAS == parsed_branch
+ {
+ version = format!("dev-{}", str_replace("#", "+", &branch));
+ parsed_branch = str_replace("#", "+", &parsed_branch);
+ } else {
+ let prefix = if strpos(&branch, "v") == Some(0) { "v" } else { "" };
+ version = format!(
+ "{}{}",
+ prefix,
+ Preg::replace(r"{(\.9{7})+}", ".x", &parsed_branch)
+ );
+ }
+
+ let is_default_branch =
+ self.driver.as_mut().unwrap().get_root_identifier()? == branch;
+ let cached_package = self.get_cached_package_version(
+ &version,
+ &identifier,
+ is_verbose,
+ is_very_verbose,
+ is_default_branch,
+ )?;
+ if let CachedPackageResult::Package(pkg) = cached_package {
+ self.inner.add_package(pkg)?;
+ continue;
+ }
+ if matches!(cached_package, CachedPackageResult::Missing) {
+ self.empty_references.push(identifier.clone());
+ continue;
+ }
+
+ let result: Result<()> = (|| -> Result<()> {
+ let driver = self.driver.as_mut().unwrap();
+ let data_opt = driver.get_composer_information(&identifier)?;
+ if data_opt.is_none() {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped branch {}, no composer file</warning>",
+ branch
+ ));
+ }
+ self.empty_references.push(identifier.clone());
+ return Ok(());
+ }
+ let mut data = data_opt.unwrap();
+
+ // branches are always auto-versioned, read value from branch name
+ data.insert("version".to_string(), PhpMixed::String(version.clone()));
+ data.insert(
+ "version_normalized".to_string(),
+ PhpMixed::String(parsed_branch.clone()),
+ );
+
+ data.shift_remove("default-branch");
+ if driver.get_root_identifier()? == branch {
+ data.insert("default-branch".to_string(), PhpMixed::Bool(true));
+ }
+
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "Importing branch {} ({})",
+ branch,
+ data.get("version").and_then(|v| v.as_string()).unwrap_or("")
+ ));
+ }
+
+ let package_data = self.pre_process(&**driver, data, &identifier)?;
+ let package = self.loader.as_ref().unwrap().load(package_data.clone(), None)?;
+ // TODO(phase-b): `$this->loader instanceof ValidatingArrayLoader` downcast
+ let loader_as_validating: Option<&ValidatingArrayLoader> = None;
+ if let Some(validating) = loader_as_validating {
+ if count(&PhpMixed::Null) > 0 {
+ let _ = validating;
+ return Err(InvalidPackageException::new(
+ vec![],
+ vec![],
+ package_data,
+ )
+ .into());
+ }
+ }
+ self.inner.add_package(Box::new(package as Box<dyn _>))?;
+ Ok(())
+ })();
+ if let Err(e) = result {
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ self.version_transport_exceptions
+ .entry("branches".to_string())
+ .or_insert_with(IndexMap::new)
+ .insert(branch.clone(), te.clone());
+ if te.get_code() == 404 {
+ self.empty_references.push(identifier.clone());
+ }
+ if self.should_rethrow_transport_exception(te) {
+ return Err(e);
+ }
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped branch {}, no composer file was found ({} HTTP status code)</warning>",
+ branch, te.get_code()
+ ));
+ }
+ continue;
+ }
+ if !is_very_verbose {
+ self.io.write_error("");
+ }
+ self.branch_error_occurred = true;
+ self.io.write_error(&format!(
+ "<error>Skipped branch {}, {}</error>",
+ branch, e
+ ));
+ self.io.write_error("");
+ continue;
+ }
+ }
+ self.driver.as_mut().unwrap().cleanup()?;
+
+ if !is_very_verbose {
+ self.io.overwrite_error(
+ PhpMixed::String(String::new()),
+ false,
+ None,
+ IOInterface::NORMAL,
+ );
+ }
+
+ if self.inner.get_packages().is_empty() {
+ return Err(InvalidRepositoryException {
+ message: format!(
+ "No valid composer.json was found in any branch or tag of {}, could not load a package from it.",
+ self.url
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ Ok(())
+ }
+
+ /// @param array{name?: string, dist?: array{type: string, url: string, reference: string, shasum: string}, source?: array{type: string, url: string, reference: string}} $data
+ ///
+ /// @return array{name: string|null, dist: array{type: string, url: string, reference: string, shasum: string}|null, source: array{type: string, url: string, reference: string}}
+ fn pre_process(
+ &self,
+ driver: &dyn VcsDriverInterface,
+ mut data: IndexMap<String, PhpMixed>,
+ identifier: &str,
+ ) -> Result<IndexMap<String, PhpMixed>> {
+ // keep the name of the main identifier for all packages
+ // this ensures that a package can be renamed in one place and that all old tags
+ // will still be installable using that new name without requiring re-tagging
+ let data_package_name = data
+ .get("name")
+ .and_then(|v| v.as_string())
+ .map(String::from);
+ let name_value = self
+ .package_name
+ .clone()
+ .filter(|s| !s.is_empty())
+ .or(data_package_name);
+ data.insert(
+ "name".to_string(),
+ match name_value {
+ Some(n) => PhpMixed::String(n),
+ None => PhpMixed::Null,
+ },
+ );
+
+ if !data.contains_key("dist") {
+ let dist = driver.get_dist(identifier);
+ data.insert(
+ "dist".to_string(),
+ match dist {
+ Some(m) => PhpMixed::Array(
+ m.into_iter()
+ .map(|(k, v)| (k, Box::new(PhpMixed::String(v))))
+ .collect(),
+ ),
+ None => PhpMixed::Null,
+ },
+ );
+ }
+ if !data.contains_key("source") {
+ let source = driver.get_source(identifier);
+ data.insert(
+ "source".to_string(),
+ PhpMixed::Array(
+ source
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(PhpMixed::String(v))))
+ .collect(),
+ ),
+ );
+ }
+
+ // if custom dist info is provided but does not provide a reference, copy the source reference to it
+ let dist_is_array = matches!(data.get("dist"), Some(PhpMixed::Array(_)));
+ let dist_lacks_reference = data
+ .get("dist")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => Some(!m.contains_key("reference")),
+ _ => None,
+ })
+ .unwrap_or(false);
+ let source_reference = data
+ .get("source")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => m.get("reference").cloned(),
+ _ => None,
+ });
+ if dist_is_array && dist_lacks_reference && source_reference.is_some() {
+ if let Some(PhpMixed::Array(dist_map)) = data.get_mut("dist") {
+ dist_map.insert("reference".to_string(), source_reference.unwrap());
+ }
+ }
+
+ Ok(data)
+ }
+
+ /// @return string|false
+ fn validate_branch(&self, branch: &str) -> Option<String> {
+ let result = self
+ .version_parser
+ .as_ref()
+ .unwrap()
+ .normalize_branch(branch);
+ if let Ok(normalized_branch) = result {
+ // validate that the branch name has no weird characters conflicting with constraints
+ if self
+ .version_parser
+ .as_ref()
+ .unwrap()
+ .parse_constraints(&normalized_branch)
+ .is_ok()
+ {
+ return Some(normalized_branch);
+ }
+ }
+
+ None
+ }
+
+ /// @return string|false
+ fn validate_tag(&self, version: &str) -> Option<String> {
+ self.version_parser
+ .as_ref()
+ .unwrap()
+ .normalize(version, None)
+ .ok()
+ }
+
+ /// @return \Composer\Package\CompletePackage|\Composer\Package\CompleteAliasPackage|null|false null if no cache present, false if the absence of a version was cached
+ fn get_cached_package_version(
+ &mut self,
+ version: &str,
+ identifier: &str,
+ is_verbose: bool,
+ is_very_verbose: bool,
+ is_default_branch: bool,
+ ) -> Result<CachedPackageResult> {
+ if self.version_cache.is_none() {
+ return Ok(CachedPackageResult::None);
+ }
+
+ let mut cached_package = self
+ .version_cache
+ .as_ref()
+ .unwrap()
+ .get_version_package(version, identifier);
+ if matches!(cached_package, VersionCacheResult::Missing) {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped {}, no composer file (cached from ref {})</warning>",
+ version, identifier
+ ));
+ }
+
+ return Ok(CachedPackageResult::Missing);
+ }
+
+ if let VersionCacheResult::Package(ref mut data) = cached_package {
+ let msg = format!(
+ "Found cached composer.json of <info>{}</info> (<comment>{}</comment>)",
+ self.package_name.clone().unwrap_or_else(|| self.url.clone()),
+ version
+ );
+ if is_very_verbose {
+ self.io.write_error(&msg);
+ } else if is_verbose {
+ self.io.overwrite_error(
+ PhpMixed::String(msg.clone()),
+ false,
+ None,
+ IOInterface::NORMAL,
+ );
+ }
+
+ data.shift_remove("default-branch");
+ if is_default_branch {
+ data.insert("default-branch".to_string(), PhpMixed::Bool(true));
+ }
+
+ let name = data.get("name").and_then(|v| v.as_string()).unwrap_or("").to_string();
+ let version_normalized = data
+ .get("version_normalized")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ if let Some(existing_package) = self
+ .inner
+ .find_package(&name, Box::new(Constraint::new("=", &version_normalized)))
+ {
+ if is_very_verbose {
+ self.io.write_error(&format!(
+ "<warning>Skipped cached version {}, it conflicts with an another tag ({}) as both resolve to {} internally</warning>",
+ version, existing_package.get_pretty_version(), version_normalized
+ ));
+ }
+ cached_package = VersionCacheResult::None;
+ }
+ }
+
+ if let VersionCacheResult::Package(data) = cached_package {
+ let loaded = self.loader.as_ref().unwrap().load(data, None)?;
+ return Ok(CachedPackageResult::Package(Box::new(loaded as Box<dyn _>)));
+ }
+
+ Ok(CachedPackageResult::None)
+ }
+
+ fn should_rethrow_transport_exception(&self, e: &TransportException) -> bool {
+ in_array(
+ PhpMixed::Int(e.get_code()),
+ &PhpMixed::List(vec![
+ Box::new(PhpMixed::Int(401)),
+ Box::new(PhpMixed::Int(403)),
+ Box::new(PhpMixed::Int(429)),
+ ]),
+ true,
+ ) || e.get_code() >= 500
+ }
+}
+
+#[derive(Debug)]
+enum CachedPackageResult {
+ None,
+ Missing,
+ Package(Box<BasePackage>),
+}
+
+#[derive(Debug)]
+enum VersionCacheResult {
+ None,
+ Missing,
+ Package(IndexMap<String, PhpMixed>),
+}