aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-07 07:26:48 +0900
committernsfisis <nsfisis@gmail.com>2026-08-07 07:26:48 +0900
commitf749a47804cd296a3059cd3f8079c62dbaa5fdc0 (patch)
treea84d5d40f6f9eea2a83355a273d0fb57214a864a /crates
parente7f83b74e8f8c12b4a1b0f9f613387b03858dbdd (diff)
downloadphp-shirabe-f749a47804cd296a3059cd3f8079c62dbaa5fdc0.tar.gz
php-shirabe-f749a47804cd296a3059cd3f8079c62dbaa5fdc0.tar.zst
php-shirabe-f749a47804cd296a3059cd3f8079c62dbaa5fdc0.zip
refactor: merge split inherent impl blocks into one per type
Enable clippy::multiple_inherent_impl and fix the 21 sites it reports. Types whose inherent methods were spread across two or three impl blocks now keep them in a single block; only the impl headers move, no method bodies change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/input/array_input.rs118
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs2
-rw-r--r--crates/shirabe-php-shim/src/zip.rs2
-rw-r--r--crates/shirabe/src/command/archive_command.rs312
-rw-r--r--crates/shirabe/src/command/audit_command.rs110
-rw-r--r--crates/shirabe/src/command/config_command.rs736
-rw-r--r--crates/shirabe/src/command/create_project_command.rs410
-rw-r--r--crates/shirabe/src/command/diagnose_command.rs776
-rw-r--r--crates/shirabe/src/command/init_command.rs730
-rw-r--r--crates/shirabe/src/command/require_command.rs1164
-rw-r--r--crates/shirabe/src/command/show_command.rs2748
-rw-r--r--crates/shirabe/src/command/update_command.rs394
-rw-r--r--crates/shirabe/src/command/validate_command.rs190
-rw-r--r--crates/shirabe/src/console/application.rs2
-rw-r--r--crates/shirabe/src/dependency_resolver/request.rs50
-rw-r--r--crates/shirabe/src/downloader/file_downloader.rs460
-rw-r--r--crates/shirabe/src/package/loader/array_loader.rs296
-rw-r--r--crates/shirabe/src/package/loader/validating_array_loader.rs554
18 files changed, 4506 insertions, 4548 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs b/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs
index 1ae7069d..9a19b839 100644
--- a/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/input/array_input.rs
@@ -130,67 +130,7 @@ impl ArrayInput {
default
}
-}
-
-/// Returns a stringified representation of the args passed to the command.
-impl std::fmt::Display for ArrayInput {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let mut params: Vec<String> = vec![];
- for (param, val) in &self.parameters {
- // $param && \is_string($param) && '-' === $param[0]
- let is_option_key =
- matches!(param, PhpMixed::String(s) if !s.is_empty() && s.as_bytes()[0] == b'-');
- if is_option_key {
- let param = param.as_string().unwrap();
- let glue = if param.as_bytes().get(1) == Some(&b'-') {
- "="
- } else {
- " "
- };
- if let PhpMixed::List(list) = val {
- for v in list {
- let v = shirabe_php_shim::php_to_string(v);
- params.push(format!(
- "{}{}",
- param,
- if !v.is_empty() {
- format!("{}{}", glue, self.inner.escape_token(&v))
- } else {
- String::new()
- }
- ));
- }
- } else {
- let val = shirabe_php_shim::php_to_string(val);
- params.push(format!(
- "{}{}",
- param,
- if !val.is_empty() {
- format!("{}{}", glue, self.inner.escape_token(&val))
- } else {
- String::new()
- }
- ));
- }
- } else if let PhpMixed::List(list) = val {
- let escaped: Vec<String> = list
- .iter()
- .map(|v| self.inner.escape_token(&shirabe_php_shim::php_to_string(v)))
- .collect();
- params.push(shirabe_php_shim::implode(" ", &escaped));
- } else {
- params.push(
- self.inner
- .escape_token(&shirabe_php_shim::php_to_string(val)),
- );
- }
- }
-
- write!(f, "{}", shirabe_php_shim::implode(" ", &params))
- }
-}
-impl ArrayInput {
fn parse(&mut self) -> anyhow::Result<()> {
// Clone to avoid borrowing self while mutating; PHP iterates over a copy semantically.
let parameters = self.parameters.clone();
@@ -296,6 +236,64 @@ impl ArrayInput {
}
}
+/// Returns a stringified representation of the args passed to the command.
+impl std::fmt::Display for ArrayInput {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let mut params: Vec<String> = vec![];
+ for (param, val) in &self.parameters {
+ // $param && \is_string($param) && '-' === $param[0]
+ let is_option_key =
+ matches!(param, PhpMixed::String(s) if !s.is_empty() && s.as_bytes()[0] == b'-');
+ if is_option_key {
+ let param = param.as_string().unwrap();
+ let glue = if param.as_bytes().get(1) == Some(&b'-') {
+ "="
+ } else {
+ " "
+ };
+ if let PhpMixed::List(list) = val {
+ for v in list {
+ let v = shirabe_php_shim::php_to_string(v);
+ params.push(format!(
+ "{}{}",
+ param,
+ if !v.is_empty() {
+ format!("{}{}", glue, self.inner.escape_token(&v))
+ } else {
+ String::new()
+ }
+ ));
+ }
+ } else {
+ let val = shirabe_php_shim::php_to_string(val);
+ params.push(format!(
+ "{}{}",
+ param,
+ if !val.is_empty() {
+ format!("{}{}", glue, self.inner.escape_token(&val))
+ } else {
+ String::new()
+ }
+ ));
+ }
+ } else if let PhpMixed::List(list) = val {
+ let escaped: Vec<String> = list
+ .iter()
+ .map(|v| self.inner.escape_token(&shirabe_php_shim::php_to_string(v)))
+ .collect();
+ params.push(shirabe_php_shim::implode(" ", &escaped));
+ } else {
+ params.push(
+ self.inner
+ .escape_token(&shirabe_php_shim::php_to_string(val)),
+ );
+ }
+ }
+
+ write!(f, "{}", shirabe_php_shim::implode(" ", &params))
+ }
+}
+
impl InputInterface for ArrayInput {
fn dup(&self) -> std::rc::Rc<std::cell::RefCell<dyn InputInterface>> {
std::rc::Rc::new(std::cell::RefCell::new(self.clone()))
diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
index 9097faff..b62c2565 100644
--- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs
@@ -479,9 +479,7 @@ impl SymfonyStyle {
as Box<dyn Fn(Option<PhpMixed>) -> Result<PhpMixed, InvalidArgumentException>>
})
}
-}
-impl SymfonyStyle {
/// {@inheritdoc}
pub fn writeln(&mut self, messages: PhpMixed, r#type: i64) {
let messages: Vec<PhpMixed> = if !shirabe_php_shim::is_iterable(&messages) {
diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs
index 56f02db7..6a088419 100644
--- a/crates/shirabe-php-shim/src/zip.rs
+++ b/crates/shirabe-php-shim/src/zip.rs
@@ -265,9 +265,7 @@ impl ZipArchive {
_ => String::new(),
}
}
-}
-impl ZipArchive {
pub const CREATE: i64 = 1;
pub const OPSYS_UNIX: i64 = 3;
pub const ER_SEEK: i64 = 4;
diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs
index 7a24dacc..e749d13f 100644
--- a/crates/shirabe/src/command/archive_command.rs
+++ b/crates/shirabe/src/command/archive_command.rs
@@ -80,155 +80,7 @@ impl ArchiveCommand {
.expect("ArchiveCommand::configure uses static, valid metadata");
command
}
-}
-
-impl Command for ArchiveCommand {
- fn configure(&self) -> anyhow::Result<()> {
- self.set_name("archive")?;
- self.set_description("Creates an archive of this composer package");
- self.set_definition(&[
- InputArgument::new5("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None, self.suggest_available_package(99)).unwrap().into(),
- InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None).unwrap().into(),
- InputOption::new6("format", Some(shirabe_php_shim::PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)", None, SuggestedValues::List(Self::FORMATS.iter().map(|s| s.to_string()).collect())).unwrap().into(),
- InputOption::new("dir", None, Some(InputOption::VALUE_REQUIRED), "Write the archive to this directory", None).unwrap().into(),
- InputOption::new("file", None, Some(InputOption::VALUE_REQUIRED), "Write the archive with the given file name. Note that the format will be appended.", None).unwrap().into(),
- InputOption::new("ignore-filters", None, Some(InputOption::VALUE_NONE), "Ignore filters when saving package", None).unwrap().into(),
- ]);
- self.set_help(
- "The <info>archive</info> command creates an archive of the specified format\n\
- containing the files and directories of the Composer project or the specified\n\
- package in the specified version and writes it to the specified directory.\n\n\
- <info>shirabe archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\
- Read more at https://getcomposer.org/doc/03-cli.md#archive"
- );
- Ok(())
- }
-
- fn execute(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- let composer = self.try_composer(None, None);
-
- let config = if let Some(ref composer) = composer {
- let config = composer.borrow_partial().get_config();
- // TODO(plugin): dispatch CommandEvent
- let command_event =
- CommandEvent::new(PluginEvents::COMMAND, "archive", input.clone(), output);
- let event_dispatcher = composer.borrow_partial().get_event_dispatcher();
- event_dispatcher
- .borrow_mut()
- .dispatch(Some(command_event.get_name()), None);
- event_dispatcher.borrow_mut().dispatch_script(
- ScriptEvents::PRE_ARCHIVE_CMD,
- true,
- vec![],
- indexmap::IndexMap::new(),
- );
- config
- } else {
- std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?))
- };
-
- let format = input
- .borrow()
- .get_option("format")?
- .as_string()
- .map(|s| s.to_string())
- .unwrap_or_else(|| {
- config
- .borrow_mut()
- .get("archive-format")
- .as_string()
- .unwrap_or("tar")
- .to_string()
- });
-
- let dir = input
- .borrow()
- .get_option("dir")?
- .as_string()
- .map(|s| s.to_string())
- .unwrap_or_else(|| {
- config
- .borrow_mut()
- .get("archive-dir")
- .as_string()
- .unwrap_or(".")
- .to_string()
- });
- let io = self.get_io().clone();
- let return_code = self.archive(
- io.clone(),
- &config,
- input
- .borrow()
- .get_argument("package")?
- .as_string()
- .map(|s| s.to_string()),
- input
- .borrow()
- .get_argument("version")?
- .as_string()
- .map(|s| s.to_string()),
- &format,
- &dir,
- input
- .borrow()
- .get_option("file")?
- .as_string()
- .map(|s| s.to_string()),
- input
- .borrow()
- .get_option("ignore-filters")?
- .as_bool()
- .unwrap_or(false),
- composer.as_ref(),
- )?;
-
- if return_code == 0
- && let Some(ref composer) = composer
- {
- composer
- .borrow_partial()
- .get_event_dispatcher()
- .borrow_mut()
- .dispatch_script(
- ScriptEvents::POST_ARCHIVE_CMD,
- true,
- vec![],
- indexmap::IndexMap::new(),
- );
- }
-
- Ok(return_code)
- }
-
- fn initialize(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<()> {
- if self.test_hooks.borrow().skip_initialize {
- return Ok(());
- }
- base_command_initialize(self, input, output)
- }
-
- fn complete(
- &self,
- input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
- suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
- ) -> anyhow::Result<()> {
- crate::command::base_command::base_command_complete(self, input, suggestions)
- }
-
- shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
-}
-
-impl ArchiveCommand {
/// For testing only: makes `initialize` a no-op (PHPUnit `onlyMethods(['initialize'])`).
pub fn __test_skip_initialize(&self) {
self.test_hooks.borrow_mut().skip_initialize = true;
@@ -244,17 +96,7 @@ impl ArchiveCommand {
pub fn __test_archive_calls(&self) -> Vec<ArchiveCallRecord> {
self.test_hooks.borrow().archive_calls.clone()
}
-}
-
-impl BaseCommand for ArchiveCommand {
- fn base_command_data(&self) -> &crate::command::BaseCommandData {
- &self.base_command_data
- }
-
- crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
-}
-impl ArchiveCommand {
#[allow(clippy::too_many_arguments, reason = "to keep PHP signature")]
pub fn archive(
&self,
@@ -474,3 +316,157 @@ impl ArchiveCommand {
Ok(Some(complete))
}
}
+
+impl Command for ArchiveCommand {
+ fn configure(&self) -> anyhow::Result<()> {
+ self.set_name("archive")?;
+ self.set_description("Creates an archive of this composer package");
+ self.set_definition(&[
+ InputArgument::new5("package", Some(InputArgument::OPTIONAL), "The package to archive instead of the current project", None, self.suggest_available_package(99)).unwrap().into(),
+ InputArgument::new("version", Some(InputArgument::OPTIONAL), "A version constraint to find the package to archive", None).unwrap().into(),
+ InputOption::new6("format", Some(shirabe_php_shim::PhpMixed::String("f".to_string())), Some(InputOption::VALUE_REQUIRED), "Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)", None, SuggestedValues::List(Self::FORMATS.iter().map(|s| s.to_string()).collect())).unwrap().into(),
+ InputOption::new("dir", None, Some(InputOption::VALUE_REQUIRED), "Write the archive to this directory", None).unwrap().into(),
+ InputOption::new("file", None, Some(InputOption::VALUE_REQUIRED), "Write the archive with the given file name. Note that the format will be appended.", None).unwrap().into(),
+ InputOption::new("ignore-filters", None, Some(InputOption::VALUE_NONE), "Ignore filters when saving package", None).unwrap().into(),
+ ]);
+ self.set_help(
+ "The <info>archive</info> command creates an archive of the specified format\n\
+ containing the files and directories of the Composer project or the specified\n\
+ package in the specified version and writes it to the specified directory.\n\n\
+ <info>shirabe archive [--format=zip] [--dir=/foo] [--file=filename] [package [version]]</info>\n\n\
+ Read more at https://getcomposer.org/doc/03-cli.md#archive"
+ );
+ Ok(())
+ }
+
+ fn execute(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ let composer = self.try_composer(None, None);
+
+ let config = if let Some(ref composer) = composer {
+ let config = composer.borrow_partial().get_config();
+ // TODO(plugin): dispatch CommandEvent
+ let command_event =
+ CommandEvent::new(PluginEvents::COMMAND, "archive", input.clone(), output);
+ let event_dispatcher = composer.borrow_partial().get_event_dispatcher();
+ event_dispatcher
+ .borrow_mut()
+ .dispatch(Some(command_event.get_name()), None);
+ event_dispatcher.borrow_mut().dispatch_script(
+ ScriptEvents::PRE_ARCHIVE_CMD,
+ true,
+ vec![],
+ indexmap::IndexMap::new(),
+ );
+ config
+ } else {
+ std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?))
+ };
+
+ let format = input
+ .borrow()
+ .get_option("format")?
+ .as_string()
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| {
+ config
+ .borrow_mut()
+ .get("archive-format")
+ .as_string()
+ .unwrap_or("tar")
+ .to_string()
+ });
+
+ let dir = input
+ .borrow()
+ .get_option("dir")?
+ .as_string()
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| {
+ config
+ .borrow_mut()
+ .get("archive-dir")
+ .as_string()
+ .unwrap_or(".")
+ .to_string()
+ });
+
+ let io = self.get_io().clone();
+ let return_code = self.archive(
+ io.clone(),
+ &config,
+ input
+ .borrow()
+ .get_argument("package")?
+ .as_string()
+ .map(|s| s.to_string()),
+ input
+ .borrow()
+ .get_argument("version")?
+ .as_string()
+ .map(|s| s.to_string()),
+ &format,
+ &dir,
+ input
+ .borrow()
+ .get_option("file")?
+ .as_string()
+ .map(|s| s.to_string()),
+ input
+ .borrow()
+ .get_option("ignore-filters")?
+ .as_bool()
+ .unwrap_or(false),
+ composer.as_ref(),
+ )?;
+
+ if return_code == 0
+ && let Some(ref composer) = composer
+ {
+ composer
+ .borrow_partial()
+ .get_event_dispatcher()
+ .borrow_mut()
+ .dispatch_script(
+ ScriptEvents::POST_ARCHIVE_CMD,
+ true,
+ vec![],
+ indexmap::IndexMap::new(),
+ );
+ }
+
+ Ok(return_code)
+ }
+
+ fn initialize(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<()> {
+ if self.test_hooks.borrow().skip_initialize {
+ return Ok(());
+ }
+ base_command_initialize(self, input, output)
+ }
+
+ fn complete(
+ &self,
+ input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
+ suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ crate::command::base_command::base_command_complete(self, input, suggestions)
+ }
+
+ shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl BaseCommand for ArchiveCommand {
+ fn base_command_data(&self) -> &crate::command::BaseCommandData {
+ &self.base_command_data
+ }
+
+ crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
+}
diff --git a/crates/shirabe/src/command/audit_command.rs b/crates/shirabe/src/command/audit_command.rs
index 082afdc2..0eee0036 100644
--- a/crates/shirabe/src/command/audit_command.rs
+++ b/crates/shirabe/src/command/audit_command.rs
@@ -44,6 +44,60 @@ impl AuditCommand {
.expect("AuditCommand::configure uses static, valid metadata");
command
}
+
+ fn get_packages(
+ &self,
+ composer: &PartialComposerHandle,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ ) -> anyhow::Result<Vec<crate::package::PackageInterfaceHandle>> {
+ let composer = crate::composer::composer_full(composer);
+ if input
+ .borrow()
+ .get_option("locked")?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ let locker = composer.get_locker().clone();
+ let mut locker = locker.borrow_mut();
+ if !locker.is_locked() {
+ return Err(UnexpectedValueException {
+ message: "Valid composer.json and composer.lock files are required to run this command with --locked".to_string(),
+ code: 0,
+ }.into());
+ }
+ let locked_repo = locker.get_locked_repository(
+ !input
+ .borrow()
+ .get_option("no-dev")?
+ .as_bool()
+ .unwrap_or(false),
+ )?;
+ return locked_repo.borrow_mut().get_packages();
+ }
+
+ let root_pkg = composer.get_package();
+ let local_repo = composer
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository();
+ let mut installed_repo = InstalledRepository::new(vec![local_repo]);
+
+ if input
+ .borrow()
+ .get_option("no-dev")?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(RepositoryUtils::filter_required_packages(
+ &installed_repo.get_packages()?,
+ root_pkg.clone().into(),
+ false,
+ vec![],
+ ));
+ }
+
+ installed_repo.get_packages()
+ }
}
impl Command for AuditCommand {
@@ -255,59 +309,3 @@ impl BaseCommand for AuditCommand {
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
-
-impl AuditCommand {
- fn get_packages(
- &self,
- composer: &PartialComposerHandle,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- ) -> anyhow::Result<Vec<crate::package::PackageInterfaceHandle>> {
- let composer = crate::composer::composer_full(composer);
- if input
- .borrow()
- .get_option("locked")?
- .as_bool()
- .unwrap_or(false)
- {
- let locker = composer.get_locker().clone();
- let mut locker = locker.borrow_mut();
- if !locker.is_locked() {
- return Err(UnexpectedValueException {
- message: "Valid composer.json and composer.lock files are required to run this command with --locked".to_string(),
- code: 0,
- }.into());
- }
- let locked_repo = locker.get_locked_repository(
- !input
- .borrow()
- .get_option("no-dev")?
- .as_bool()
- .unwrap_or(false),
- )?;
- return locked_repo.borrow_mut().get_packages();
- }
-
- let root_pkg = composer.get_package();
- let local_repo = composer
- .get_repository_manager()
- .borrow()
- .get_local_repository();
- let mut installed_repo = InstalledRepository::new(vec![local_repo]);
-
- if input
- .borrow()
- .get_option("no-dev")?
- .as_bool()
- .unwrap_or(false)
- {
- return Ok(RepositoryUtils::filter_required_packages(
- &installed_repo.get_packages()?,
- root_pkg.clone().into(),
- false,
- vec![],
- ));
- }
-
- installed_repo.get_packages()
- }
-}
diff --git a/crates/shirabe/src/command/config_command.rs b/crates/shirabe/src/command/config_command.rs
index c4409f20..2727f36b 100644
--- a/crates/shirabe/src/command/config_command.rs
+++ b/crates/shirabe/src/command/config_command.rs
@@ -61,15 +61,7 @@ impl ConfigCommand {
"suggest",
"extra",
];
-}
-
-impl Default for ConfigCommand {
- fn default() -> Self {
- Self::new()
- }
-}
-impl ConfigCommand {
pub fn new() -> Self {
let command = ConfigCommand {
base_command_data: BaseCommandData::new(None),
@@ -84,6 +76,372 @@ impl ConfigCommand {
.expect("ConfigCommand::configure uses static, valid metadata");
command
}
+
+ pub(crate) fn handle_single_value(
+ &self,
+ key: &str,
+ callbacks: &(ValidatorFn, NormalizerFn),
+ values: &[String],
+ method: &str,
+ ) -> anyhow::Result<()> {
+ let (validator, normalizer) = callbacks;
+ if 1 != values.len() {
+ return Err(RuntimeException {
+ message: "You can only pass one value. Example: shirabe config process-timeout 300"
+ .to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let validation = validator(&PhpMixed::String(values[0].clone()));
+ if validation.as_bool() != Some(true) {
+ let suffix = if !validation.is_null() && validation.as_bool() != Some(false) {
+ format!(" ({})", validation.as_string().unwrap_or(""))
+ } else {
+ String::new()
+ };
+ return Err(RuntimeException {
+ message: format!("\"{}\" is an invalid value{}", values[0].clone(), suffix),
+ code: 0,
+ }
+ .into());
+ }
+
+ let normalized_value = normalizer(&PhpMixed::String(values[0].clone()));
+
+ if key == "disable-tls" {
+ let config = self.config.borrow().as_ref().unwrap().clone();
+ if !normalized_value.as_bool().unwrap_or(false)
+ && config
+ .borrow()
+ .get("disable-tls")
+ .as_bool()
+ .unwrap_or(false)
+ {
+ self.get_io().write_error(
+ "<info>You are now running Composer with SSL/TLS protection enabled.</info>",
+ );
+ } else if normalized_value.as_bool().unwrap_or(false)
+ && !config
+ .borrow()
+ .get("disable-tls")
+ .as_bool()
+ .unwrap_or(false)
+ {
+ self.get_io().write_error("<warning>You are now running Composer with SSL/TLS protection disabled.</warning>");
+ }
+ }
+
+ let mut config_source = self.config_source.borrow_mut();
+ let config_source = config_source.as_mut().unwrap();
+ match method {
+ "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?,
+ "addProperty" => config_source.add_property(key, normalized_value)?,
+ _ => unreachable!(),
+ }
+ Ok(())
+ }
+
+ pub(crate) fn handle_multi_value(
+ &self,
+ key: &str,
+ callbacks: &(ValidatorFn, NormalizerFn),
+ values: &[String],
+ method: &str,
+ ) -> anyhow::Result<()> {
+ let (validator, normalizer) = callbacks;
+ let values_mixed =
+ PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect());
+ let validation = validator(&values_mixed);
+ if validation.as_bool() != Some(true) {
+ let suffix = if !validation.is_null() && validation.as_bool() != Some(false) {
+ format!(" ({})", validation.as_string().unwrap_or(""))
+ } else {
+ String::new()
+ };
+ return Err(RuntimeException {
+ message: format!(
+ "{} is an invalid value{}",
+ PhpMixed::from(json_encode(&values_mixed).ok()),
+ suffix
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ let mut config_source = self.config_source.borrow_mut();
+ let config_source = config_source.as_mut().unwrap();
+ match method {
+ "addConfigSetting" => {
+ config_source.add_config_setting(key, normalizer(&values_mixed))?
+ }
+ "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?,
+ _ => unreachable!(),
+ }
+ Ok(())
+ }
+
+ /// Display the contents of the file in a pretty formatted way
+ pub(crate) fn list_configuration(
+ &self,
+ contents: PhpMixed,
+ raw_contents: PhpMixed,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ k: Option<String>,
+ show_source: bool,
+ ) {
+ let orig_k = k.clone();
+ let contents_arr = contents.as_array().cloned().unwrap_or_default();
+ let raw_contents_arr = raw_contents.as_array().cloned().unwrap_or_default();
+ let mut k = k;
+ for (key, value) in &contents_arr {
+ if k.is_none() && !matches!(key.as_str(), "config" | "repositories") {
+ continue;
+ }
+
+ let raw_val = raw_contents_arr.get(key).cloned().unwrap_or(PhpMixed::Null);
+
+ let value_inner = value.clone();
+
+ if is_array(&value_inner)
+ && (!is_numeric(&key_first_key(&value_inner).unwrap_or_default().into())
+ || (key == "repositories" && k.is_none()))
+ {
+ let mut new_k = k.clone().unwrap_or_default();
+ new_k.push_str(&Preg::replace(
+ php_regex!("{^config\\.}"),
+ "",
+ &format!("{}.", key),
+ ));
+ k = Some(new_k);
+ self.list_configuration(
+ value_inner,
+ raw_val,
+ output.clone(),
+ k.clone(),
+ show_source,
+ );
+ k = orig_k.clone();
+
+ continue;
+ }
+
+ let value_display: String = if is_array(&value_inner) {
+ let arr_strs: Vec<String> = value_inner
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .map(|val| {
+ if is_array(val) {
+ json_encode(val).unwrap_or_default()
+ } else {
+ val.as_string().unwrap_or("").to_string()
+ }
+ })
+ .collect::<Vec<_>>()
+ })
+ .unwrap_or_default();
+ format!("[{}]", implode(", ", &arr_strs))
+ } else if is_bool(&value_inner) {
+ var_export(&value_inner, true)
+ } else {
+ value_inner.as_string().unwrap_or("").to_string()
+ };
+
+ let source = if show_source {
+ format!(
+ " ({})",
+ self.config
+ .borrow()
+ .as_ref()
+ .unwrap()
+ .borrow_mut()
+ .get_source_of_value(&format!("{}{}", k.clone().unwrap_or_default(), key))
+ )
+ } else {
+ String::new()
+ };
+
+ let link: String =
+ if k.is_some() && strpos(k.as_ref().unwrap(), "repositories") == Some(0) {
+ "https://getcomposer.org/doc/05-repositories.md".to_string()
+ } else {
+ let id_source = if k.as_deref() == Some("") || k.is_none() {
+ key.clone()
+ } else {
+ k.clone().unwrap()
+ };
+ let id = Preg::replace(php_regex!("{\\..*$}"), "", &id_source);
+ let id = Preg::replace(
+ php_regex!("{[^a-z0-9]}i"),
+ "-",
+ &strtolower(&shirabe_php_shim::trim(&id, Some(" \t\n\r\0\u{0B}"))),
+ );
+ let id = Preg::replace(php_regex!("{-+}"), "-", &id);
+ format!("https://getcomposer.org/doc/06-config.md#{}", id)
+ };
+ if is_string(&raw_val)
+ && raw_val
+ .as_string()
+ .map(|s| s.to_string())
+ .unwrap_or_default()
+ != value_display
+ {
+ self.get_io().write3(
+ &format!(
+ "[<fg=yellow;href={}>{}{}</>] <info>{} ({})</info>{}",
+ link,
+ k.clone().unwrap_or_default(),
+ key,
+ raw_val.as_string().unwrap_or(""),
+ value_display,
+ source
+ ),
+ true,
+ io_interface::QUIET,
+ );
+ } else {
+ self.get_io().write3(
+ &format!(
+ "[<fg=yellow;href={}>{}{}</>] <info>{}</info>{}",
+ link,
+ k.clone().unwrap_or_default(),
+ key,
+ value_display,
+ source
+ ),
+ true,
+ io_interface::QUIET,
+ );
+ }
+ }
+ }
+
+ /// Suggest setting-keys, while taking given options in account.
+ fn suggest_setting_keys(&self) -> crate::console::input::SuggestedValues {
+ crate::console::input::SuggestedValues::Closure(Box::new(|this, input, _suggestions| {
+ if input.get_option("list")?.to_bool()
+ || input.get_option("editor")?.to_bool()
+ || input.get_option("auth")?.to_bool()
+ {
+ return Ok(vec![]);
+ }
+
+ let this = this
+ .as_any()
+ .downcast_ref::<ConfigCommand>()
+ .expect("suggestSettingKeys is bound to ConfigCommand");
+ // PHP passes the CompletionInput itself; the accessors only read from it, so a
+ // clone behind a fresh handle is equivalent.
+ let input_handle: std::rc::Rc<
+ std::cell::RefCell<
+ dyn shirabe_external_packages::symfony::console::input::InputInterface,
+ >,
+ > = std::rc::Rc::new(std::cell::RefCell::new(input.clone()));
+
+ // initialize configuration
+ let mut config = Factory::create_config(None, None)?;
+
+ // load configuration
+ let config_file = JsonFile::new(
+ this.get_composer_config_file(input_handle.clone(), &config)?,
+ None,
+ None,
+ )?;
+ if config_file.exists() {
+ let path = config_file.get_path().to_string();
+ let data = config_file.read()?.as_array().cloned().unwrap_or_default();
+ config.merge(&data, &path);
+ }
+
+ // load auth-configuration
+ let auth_config_file = JsonFile::new(
+ this.get_auth_config_file(input_handle.clone(), &config)?,
+ None,
+ None,
+ )?;
+ if auth_config_file.exists() {
+ let path = auth_config_file.get_path().to_string();
+ let mut data = IndexMap::new();
+ data.insert("config".to_string(), auth_config_file.read()?);
+ config.merge(&data, &path);
+ }
+
+ // collect all configuration setting-keys
+ let raw_config = config.raw();
+ let mut keys = flatten_setting_keys(
+ raw_config.get("config").cloned().unwrap_or(PhpMixed::Null),
+ "",
+ );
+ keys.extend(flatten_setting_keys(
+ raw_config
+ .get("repositories")
+ .cloned()
+ .unwrap_or(PhpMixed::Null),
+ "repositories.",
+ ));
+
+ // if unsetting …
+ if input.get_option("unset")?.to_bool() {
+ // … keep only the currently customized setting-keys …
+ let sources = [
+ config_file.get_path().to_string(),
+ auth_config_file.get_path().to_string(),
+ ];
+ keys.retain(|key| sources.contains(&config.get_source_of_value(key)));
+
+ // … else if showing or setting a value …
+ } else {
+ // … add all configurable package-properties, no matter if it exist
+ keys.extend(
+ Self::CONFIGURABLE_PACKAGE_PROPERTIES
+ .iter()
+ .map(|property| property.to_string()),
+ );
+
+ // it would be nice to distinguish between showing and setting
+ // a value, but that makes the implementation much more complex
+ // and partially impossible because symfony's implementation
+ // does not complete arguments followed by other arguments
+ }
+
+ // add all existing configurable package-properties
+ if config_file.exists() {
+ let properties: IndexMap<String, PhpMixed> = config_file
+ .read()?
+ .as_array()
+ .cloned()
+ .unwrap_or_default()
+ .into_iter()
+ .filter(|(key, _)| {
+ Self::CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str())
+ })
+ .collect();
+
+ keys.extend(flatten_setting_keys(PhpMixed::Array(properties), ""));
+ }
+
+ // filter settings-keys by completion value
+ let completion_value = input.get_completion_value();
+
+ if !completion_value.is_empty() {
+ keys.retain(|key| key.starts_with(&completion_value));
+ }
+
+ keys.sort();
+
+ keys.dedup();
+ Ok(keys)
+ }))
+ }
+}
+
+impl Default for ConfigCommand {
+ fn default() -> Self {
+ Self::new()
+ }
}
impl Command for ConfigCommand {
@@ -1275,368 +1633,6 @@ impl BaseCommand for ConfigCommand {
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
-impl ConfigCommand {
- pub(crate) fn handle_single_value(
- &self,
- key: &str,
- callbacks: &(ValidatorFn, NormalizerFn),
- values: &[String],
- method: &str,
- ) -> anyhow::Result<()> {
- let (validator, normalizer) = callbacks;
- if 1 != values.len() {
- return Err(RuntimeException {
- message: "You can only pass one value. Example: shirabe config process-timeout 300"
- .to_string(),
- code: 0,
- }
- .into());
- }
-
- let validation = validator(&PhpMixed::String(values[0].clone()));
- if validation.as_bool() != Some(true) {
- let suffix = if !validation.is_null() && validation.as_bool() != Some(false) {
- format!(" ({})", validation.as_string().unwrap_or(""))
- } else {
- String::new()
- };
- return Err(RuntimeException {
- message: format!("\"{}\" is an invalid value{}", values[0].clone(), suffix),
- code: 0,
- }
- .into());
- }
-
- let normalized_value = normalizer(&PhpMixed::String(values[0].clone()));
-
- if key == "disable-tls" {
- let config = self.config.borrow().as_ref().unwrap().clone();
- if !normalized_value.as_bool().unwrap_or(false)
- && config
- .borrow()
- .get("disable-tls")
- .as_bool()
- .unwrap_or(false)
- {
- self.get_io().write_error(
- "<info>You are now running Composer with SSL/TLS protection enabled.</info>",
- );
- } else if normalized_value.as_bool().unwrap_or(false)
- && !config
- .borrow()
- .get("disable-tls")
- .as_bool()
- .unwrap_or(false)
- {
- self.get_io().write_error("<warning>You are now running Composer with SSL/TLS protection disabled.</warning>");
- }
- }
-
- let mut config_source = self.config_source.borrow_mut();
- let config_source = config_source.as_mut().unwrap();
- match method {
- "addConfigSetting" => config_source.add_config_setting(key, normalized_value)?,
- "addProperty" => config_source.add_property(key, normalized_value)?,
- _ => unreachable!(),
- }
- Ok(())
- }
-
- pub(crate) fn handle_multi_value(
- &self,
- key: &str,
- callbacks: &(ValidatorFn, NormalizerFn),
- values: &[String],
- method: &str,
- ) -> anyhow::Result<()> {
- let (validator, normalizer) = callbacks;
- let values_mixed =
- PhpMixed::List(values.iter().map(|s| PhpMixed::String(s.clone())).collect());
- let validation = validator(&values_mixed);
- if validation.as_bool() != Some(true) {
- let suffix = if !validation.is_null() && validation.as_bool() != Some(false) {
- format!(" ({})", validation.as_string().unwrap_or(""))
- } else {
- String::new()
- };
- return Err(RuntimeException {
- message: format!(
- "{} is an invalid value{}",
- PhpMixed::from(json_encode(&values_mixed).ok()),
- suffix
- ),
- code: 0,
- }
- .into());
- }
-
- let mut config_source = self.config_source.borrow_mut();
- let config_source = config_source.as_mut().unwrap();
- match method {
- "addConfigSetting" => {
- config_source.add_config_setting(key, normalizer(&values_mixed))?
- }
- "addProperty" => config_source.add_property(key, normalizer(&values_mixed))?,
- _ => unreachable!(),
- }
- Ok(())
- }
-
- /// Display the contents of the file in a pretty formatted way
- pub(crate) fn list_configuration(
- &self,
- contents: PhpMixed,
- raw_contents: PhpMixed,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- k: Option<String>,
- show_source: bool,
- ) {
- let orig_k = k.clone();
- let contents_arr = contents.as_array().cloned().unwrap_or_default();
- let raw_contents_arr = raw_contents.as_array().cloned().unwrap_or_default();
- let mut k = k;
- for (key, value) in &contents_arr {
- if k.is_none() && !matches!(key.as_str(), "config" | "repositories") {
- continue;
- }
-
- let raw_val = raw_contents_arr.get(key).cloned().unwrap_or(PhpMixed::Null);
-
- let value_inner = value.clone();
-
- if is_array(&value_inner)
- && (!is_numeric(&key_first_key(&value_inner).unwrap_or_default().into())
- || (key == "repositories" && k.is_none()))
- {
- let mut new_k = k.clone().unwrap_or_default();
- new_k.push_str(&Preg::replace(
- php_regex!("{^config\\.}"),
- "",
- &format!("{}.", key),
- ));
- k = Some(new_k);
- self.list_configuration(
- value_inner,
- raw_val,
- output.clone(),
- k.clone(),
- show_source,
- );
- k = orig_k.clone();
-
- continue;
- }
-
- let value_display: String = if is_array(&value_inner) {
- let arr_strs: Vec<String> = value_inner
- .as_list()
- .map(|l| {
- l.iter()
- .map(|val| {
- if is_array(val) {
- json_encode(val).unwrap_or_default()
- } else {
- val.as_string().unwrap_or("").to_string()
- }
- })
- .collect::<Vec<_>>()
- })
- .unwrap_or_default();
- format!("[{}]", implode(", ", &arr_strs))
- } else if is_bool(&value_inner) {
- var_export(&value_inner, true)
- } else {
- value_inner.as_string().unwrap_or("").to_string()
- };
-
- let source = if show_source {
- format!(
- " ({})",
- self.config
- .borrow()
- .as_ref()
- .unwrap()
- .borrow_mut()
- .get_source_of_value(&format!("{}{}", k.clone().unwrap_or_default(), key))
- )
- } else {
- String::new()
- };
-
- let link: String =
- if k.is_some() && strpos(k.as_ref().unwrap(), "repositories") == Some(0) {
- "https://getcomposer.org/doc/05-repositories.md".to_string()
- } else {
- let id_source = if k.as_deref() == Some("") || k.is_none() {
- key.clone()
- } else {
- k.clone().unwrap()
- };
- let id = Preg::replace(php_regex!("{\\..*$}"), "", &id_source);
- let id = Preg::replace(
- php_regex!("{[^a-z0-9]}i"),
- "-",
- &strtolower(&shirabe_php_shim::trim(&id, Some(" \t\n\r\0\u{0B}"))),
- );
- let id = Preg::replace(php_regex!("{-+}"), "-", &id);
- format!("https://getcomposer.org/doc/06-config.md#{}", id)
- };
- if is_string(&raw_val)
- && raw_val
- .as_string()
- .map(|s| s.to_string())
- .unwrap_or_default()
- != value_display
- {
- self.get_io().write3(
- &format!(
- "[<fg=yellow;href={}>{}{}</>] <info>{} ({})</info>{}",
- link,
- k.clone().unwrap_or_default(),
- key,
- raw_val.as_string().unwrap_or(""),
- value_display,
- source
- ),
- true,
- io_interface::QUIET,
- );
- } else {
- self.get_io().write3(
- &format!(
- "[<fg=yellow;href={}>{}{}</>] <info>{}</info>{}",
- link,
- k.clone().unwrap_or_default(),
- key,
- value_display,
- source
- ),
- true,
- io_interface::QUIET,
- );
- }
- }
- }
-
- /// Suggest setting-keys, while taking given options in account.
- fn suggest_setting_keys(&self) -> crate::console::input::SuggestedValues {
- crate::console::input::SuggestedValues::Closure(Box::new(|this, input, _suggestions| {
- if input.get_option("list")?.to_bool()
- || input.get_option("editor")?.to_bool()
- || input.get_option("auth")?.to_bool()
- {
- return Ok(vec![]);
- }
-
- let this = this
- .as_any()
- .downcast_ref::<ConfigCommand>()
- .expect("suggestSettingKeys is bound to ConfigCommand");
- // PHP passes the CompletionInput itself; the accessors only read from it, so a
- // clone behind a fresh handle is equivalent.
- let input_handle: std::rc::Rc<
- std::cell::RefCell<
- dyn shirabe_external_packages::symfony::console::input::InputInterface,
- >,
- > = std::rc::Rc::new(std::cell::RefCell::new(input.clone()));
-
- // initialize configuration
- let mut config = Factory::create_config(None, None)?;
-
- // load configuration
- let config_file = JsonFile::new(
- this.get_composer_config_file(input_handle.clone(), &config)?,
- None,
- None,
- )?;
- if config_file.exists() {
- let path = config_file.get_path().to_string();
- let data = config_file.read()?.as_array().cloned().unwrap_or_default();
- config.merge(&data, &path);
- }
-
- // load auth-configuration
- let auth_config_file = JsonFile::new(
- this.get_auth_config_file(input_handle.clone(), &config)?,
- None,
- None,
- )?;
- if auth_config_file.exists() {
- let path = auth_config_file.get_path().to_string();
- let mut data = IndexMap::new();
- data.insert("config".to_string(), auth_config_file.read()?);
- config.merge(&data, &path);
- }
-
- // collect all configuration setting-keys
- let raw_config = config.raw();
- let mut keys = flatten_setting_keys(
- raw_config.get("config").cloned().unwrap_or(PhpMixed::Null),
- "",
- );
- keys.extend(flatten_setting_keys(
- raw_config
- .get("repositories")
- .cloned()
- .unwrap_or(PhpMixed::Null),
- "repositories.",
- ));
-
- // if unsetting …
- if input.get_option("unset")?.to_bool() {
- // … keep only the currently customized setting-keys …
- let sources = [
- config_file.get_path().to_string(),
- auth_config_file.get_path().to_string(),
- ];
- keys.retain(|key| sources.contains(&config.get_source_of_value(key)));
-
- // … else if showing or setting a value …
- } else {
- // … add all configurable package-properties, no matter if it exist
- keys.extend(
- Self::CONFIGURABLE_PACKAGE_PROPERTIES
- .iter()
- .map(|property| property.to_string()),
- );
-
- // it would be nice to distinguish between showing and setting
- // a value, but that makes the implementation much more complex
- // and partially impossible because symfony's implementation
- // does not complete arguments followed by other arguments
- }
-
- // add all existing configurable package-properties
- if config_file.exists() {
- let properties: IndexMap<String, PhpMixed> = config_file
- .read()?
- .as_array()
- .cloned()
- .unwrap_or_default()
- .into_iter()
- .filter(|(key, _)| {
- Self::CONFIGURABLE_PACKAGE_PROPERTIES.contains(&key.as_str())
- })
- .collect();
-
- keys.extend(flatten_setting_keys(PhpMixed::Array(properties), ""));
- }
-
- // filter settings-keys by completion value
- let completion_value = input.get_completion_value();
-
- if !completion_value.is_empty() {
- keys.retain(|key| key.starts_with(&completion_value));
- }
-
- keys.sort();
-
- keys.dedup();
- Ok(keys)
- }))
- }
-}
-
// PHP signature: function ($val): bool / ($val) -> bool/string
pub type ValidatorFn = Box<dyn Fn(&PhpMixed) -> PhpMixed>;
pub type NormalizerFn = Box<dyn Fn(&PhpMixed) -> PhpMixed>;
diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs
index 260685c7..dcf0e0f8 100644
--- a/crates/shirabe/src/command/create_project_command.rs
+++ b/crates/shirabe/src/command/create_project_command.rs
@@ -81,213 +81,7 @@ impl CreateProjectCommand {
.expect("CreateProjectCommand::configure uses static, valid metadata");
command
}
-}
-
-impl Command for CreateProjectCommand {
- fn configure(&self) -> anyhow::Result<()> {
- self.set_name("create-project")?;
- self.set_description("Creates new project from a package into given directory");
- self.set_definition(&[
- InputArgument::new5("package", Some(InputArgument::OPTIONAL), "Package name to be installed", None, self.suggest_available_package(99)).unwrap().into(),
- InputArgument::new("directory", Some(InputArgument::OPTIONAL), "Directory where the files should be created", None).unwrap().into(),
- InputArgument::new("version", Some(InputArgument::OPTIONAL), "Version, will default to latest", None).unwrap().into(),
- InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), "Minimum-stability allowed (unless a version is specified).", None).unwrap().into(),
- InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(),
- InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(),
- InputOption::new6("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None, self.suggest_prefer_install()).unwrap().into(),
- InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories to look the package up, either by URL or using JSON arrays", None).unwrap().into(),
- InputOption::new("repository-url", None, Some(InputOption::VALUE_REQUIRED), "DEPRECATED: Use --repository instead.", None).unwrap().into(),
- InputOption::new("add-repository", None, Some(InputOption::VALUE_NONE), "Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.", None).unwrap().into(),
- InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Enables installation of require-dev packages (enabled by default, only present for BC).", None).unwrap().into(),
- InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables installation of require-dev packages.", None).unwrap().into(),
- InputOption::new("no-custom-installers", None, Some(InputOption::VALUE_NONE), "DEPRECATED: Use no-plugins instead.", None).unwrap().into(),
- InputOption::new("no-scripts", None, Some(InputOption::VALUE_NONE), "Whether to prevent execution of all defined scripts in the root package.", None).unwrap().into(),
- InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(),
- InputOption::new("no-secure-http", None, Some(InputOption::VALUE_NONE), "Disable the secure-http config option temporarily while installing the root package. Use at your own risk. Using this flag is a bad idea.", None).unwrap().into(),
- InputOption::new("keep-vcs", None, Some(InputOption::VALUE_NONE), "Whether to prevent deleting the vcs folder.", None).unwrap().into(),
- InputOption::new("remove-vcs", None, Some(InputOption::VALUE_NONE), "Whether to force deletion of the vcs folder without prompting.", None).unwrap().into(),
- InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Whether to skip installation of the package dependencies.", None).unwrap().into(),
- InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Whether to skip auditing of the installed package dependencies (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(),
- InputOption::new6("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\" or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).unwrap().into(),
- InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(),
- InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(),
- InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(),
- InputOption::new("ask", None, Some(InputOption::VALUE_NONE), "Whether to ask for project directory.", None).unwrap().into(),
- ]);
- self.set_help(
- "The <info>create-project</info> command creates a new project from a given\n\
- package into a new directory. If executed without params and in a directory\n\
- with a composer.json file it installs the packages for the current project.\n\n\
- You can use this command to bootstrap new projects or setup a clean\n\
- version-controlled installation for developers of your project.\n\n\
- <info>shirabe create-project vendor/project target-directory [version]</info>\n\n\
- You can also specify the version with the package name using = or : as separator.\n\n\
- <info>shirabe create-project vendor/project:version target-directory</info>\n\n\
- To install unstable packages, either specify the version you want, or use the\n\
- --stability=dev (where dev can be one of RC, beta, alpha or dev).\n\n\
- To setup a developer workable version you should create the project using the source\n\
- controlled code by appending the <info>'--prefer-source'</info> flag.\n\n\
- To install a package from another repository than the default one you\n\
- can pass the <info>'--repository=https://myrepository.org'</info> flag.\n\n\
- Read more at https://getcomposer.org/doc/03-cli.md#create-project"
- );
- Ok(())
- }
-
- fn execute(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?));
- let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io();
-
- let (prefer_source, prefer_dist) =
- self.get_preferred_install_options(&config.borrow(), input.clone(), true)?;
-
- if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
- io.write_error("<warning>You are using the deprecated option \"dev\". Dev packages are installed by default now.</warning>");
- }
- if input
- .borrow()
- .get_option("no-custom-installers")?
- .as_bool()
- .unwrap_or(false)
- {
- io.write_error("<warning>You are using the deprecated option \"no-custom-installers\". Use \"no-plugins\" instead.</warning>");
- input
- .borrow_mut()
- .set_option("no-plugins", PhpMixed::Bool(true));
- }
-
- if input.borrow().is_interactive()
- && input.borrow().get_option("ask")?.as_bool().unwrap_or(false)
- {
- let package = input.borrow().get_argument("package")?;
- if package.is_null() {
- return Err(RuntimeException {
- message: "Not enough arguments (missing: \"package\").".to_string(),
- code: 0,
- }
- .into());
- }
- let mut parts =
- explode_with_limit("/", &strtolower(package.as_string().unwrap_or("")), 2);
- let prompt = format!(
- "New project directory [<comment>{}</comment>]: ",
- array_pop(&mut parts).unwrap_or_default()
- );
- input
- .borrow_mut()
- .set_argument("directory", io.ask(prompt, PhpMixed::Null)?);
- }
-
- let repository_opt = input.borrow().get_option("repository")?;
- let repository_url_opt = input.borrow().get_option("repository-url")?;
- let repositories = if repository_opt
- .as_list()
- .map(|l| !l.is_empty())
- .unwrap_or(false)
- {
- Some(repository_opt)
- } else {
- Some(repository_url_opt)
- };
-
- self.install_project(
- io,
- config,
- input.clone(),
- input
- .borrow()
- .get_argument("package")?
- .as_string()
- .map(|s| s.to_string()),
- input
- .borrow()
- .get_argument("directory")?
- .as_string()
- .map(|s| s.to_string()),
- input
- .borrow()
- .get_argument("version")?
- .as_string()
- .map(|s| s.to_string()),
- input
- .borrow()
- .get_option("stability")?
- .as_string()
- .map(|s| s.to_string()),
- prefer_source,
- prefer_dist,
- !input
- .borrow()
- .get_option("no-dev")?
- .as_bool()
- .unwrap_or(false),
- repositories,
- input
- .borrow()
- .get_option("no-plugins")?
- .as_bool()
- .unwrap_or(false),
- input
- .borrow()
- .get_option("no-scripts")?
- .as_bool()
- .unwrap_or(false),
- input
- .borrow()
- .get_option("no-progress")?
- .as_bool()
- .unwrap_or(false),
- input
- .borrow()
- .get_option("no-install")?
- .as_bool()
- .unwrap_or(false),
- Some(self.get_platform_requirement_filter(input.clone())?),
- !input
- .borrow()
- .get_option("no-secure-http")?
- .as_bool()
- .unwrap_or(false),
- input
- .borrow()
- .get_option("add-repository")?
- .as_bool()
- .unwrap_or(false),
- )
- }
-
- fn initialize(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<()> {
- base_command_initialize(self, input, output)
- }
-
- fn complete(
- &self,
- input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
- suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
- ) -> anyhow::Result<()> {
- crate::command::base_command::base_command_complete(self, input, suggestions)
- }
-
- shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
-}
-
-impl BaseCommand for CreateProjectCommand {
- fn base_command_data(&self) -> &crate::command::BaseCommandData {
- &self.base_command_data
- }
-
- crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
-}
-impl CreateProjectCommand {
/// @throws \Exception
#[allow(clippy::too_many_arguments)]
pub fn install_project(
@@ -1057,3 +851,207 @@ impl CreateProjectCommand {
Ok(installed_from_vcs)
}
}
+
+impl Command for CreateProjectCommand {
+ fn configure(&self) -> anyhow::Result<()> {
+ self.set_name("create-project")?;
+ self.set_description("Creates new project from a package into given directory");
+ self.set_definition(&[
+ InputArgument::new5("package", Some(InputArgument::OPTIONAL), "Package name to be installed", None, self.suggest_available_package(99)).unwrap().into(),
+ InputArgument::new("directory", Some(InputArgument::OPTIONAL), "Directory where the files should be created", None).unwrap().into(),
+ InputArgument::new("version", Some(InputArgument::OPTIONAL), "Version, will default to latest", None).unwrap().into(),
+ InputOption::new("stability", Some(PhpMixed::String("s".to_string())), Some(InputOption::VALUE_REQUIRED), "Minimum-stability allowed (unless a version is specified).", None).unwrap().into(),
+ InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(),
+ InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(),
+ InputOption::new6("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None, self.suggest_prefer_install()).unwrap().into(),
+ InputOption::new("repository", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Add custom repositories to look the package up, either by URL or using JSON arrays", None).unwrap().into(),
+ InputOption::new("repository-url", None, Some(InputOption::VALUE_REQUIRED), "DEPRECATED: Use --repository instead.", None).unwrap().into(),
+ InputOption::new("add-repository", None, Some(InputOption::VALUE_NONE), "Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.", None).unwrap().into(),
+ InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Enables installation of require-dev packages (enabled by default, only present for BC).", None).unwrap().into(),
+ InputOption::new("no-dev", None, Some(InputOption::VALUE_NONE), "Disables installation of require-dev packages.", None).unwrap().into(),
+ InputOption::new("no-custom-installers", None, Some(InputOption::VALUE_NONE), "DEPRECATED: Use no-plugins instead.", None).unwrap().into(),
+ InputOption::new("no-scripts", None, Some(InputOption::VALUE_NONE), "Whether to prevent execution of all defined scripts in the root package.", None).unwrap().into(),
+ InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(),
+ InputOption::new("no-secure-http", None, Some(InputOption::VALUE_NONE), "Disable the secure-http config option temporarily while installing the root package. Use at your own risk. Using this flag is a bad idea.", None).unwrap().into(),
+ InputOption::new("keep-vcs", None, Some(InputOption::VALUE_NONE), "Whether to prevent deleting the vcs folder.", None).unwrap().into(),
+ InputOption::new("remove-vcs", None, Some(InputOption::VALUE_NONE), "Whether to force deletion of the vcs folder without prompting.", None).unwrap().into(),
+ InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Whether to skip installation of the package dependencies.", None).unwrap().into(),
+ InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Whether to skip auditing of the installed package dependencies (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(),
+ InputOption::new6("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\" or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).unwrap().into(),
+ InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(),
+ InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(),
+ InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(),
+ InputOption::new("ask", None, Some(InputOption::VALUE_NONE), "Whether to ask for project directory.", None).unwrap().into(),
+ ]);
+ self.set_help(
+ "The <info>create-project</info> command creates a new project from a given\n\
+ package into a new directory. If executed without params and in a directory\n\
+ with a composer.json file it installs the packages for the current project.\n\n\
+ You can use this command to bootstrap new projects or setup a clean\n\
+ version-controlled installation for developers of your project.\n\n\
+ <info>shirabe create-project vendor/project target-directory [version]</info>\n\n\
+ You can also specify the version with the package name using = or : as separator.\n\n\
+ <info>shirabe create-project vendor/project:version target-directory</info>\n\n\
+ To install unstable packages, either specify the version you want, or use the\n\
+ --stability=dev (where dev can be one of RC, beta, alpha or dev).\n\n\
+ To setup a developer workable version you should create the project using the source\n\
+ controlled code by appending the <info>'--prefer-source'</info> flag.\n\n\
+ To install a package from another repository than the default one you\n\
+ can pass the <info>'--repository=https://myrepository.org'</info> flag.\n\n\
+ Read more at https://getcomposer.org/doc/03-cli.md#create-project"
+ );
+ Ok(())
+ }
+
+ fn execute(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ let config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?));
+ let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io();
+
+ let (prefer_source, prefer_dist) =
+ self.get_preferred_install_options(&config.borrow(), input.clone(), true)?;
+
+ if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
+ io.write_error("<warning>You are using the deprecated option \"dev\". Dev packages are installed by default now.</warning>");
+ }
+ if input
+ .borrow()
+ .get_option("no-custom-installers")?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ io.write_error("<warning>You are using the deprecated option \"no-custom-installers\". Use \"no-plugins\" instead.</warning>");
+ input
+ .borrow_mut()
+ .set_option("no-plugins", PhpMixed::Bool(true));
+ }
+
+ if input.borrow().is_interactive()
+ && input.borrow().get_option("ask")?.as_bool().unwrap_or(false)
+ {
+ let package = input.borrow().get_argument("package")?;
+ if package.is_null() {
+ return Err(RuntimeException {
+ message: "Not enough arguments (missing: \"package\").".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+ let mut parts =
+ explode_with_limit("/", &strtolower(package.as_string().unwrap_or("")), 2);
+ let prompt = format!(
+ "New project directory [<comment>{}</comment>]: ",
+ array_pop(&mut parts).unwrap_or_default()
+ );
+ input
+ .borrow_mut()
+ .set_argument("directory", io.ask(prompt, PhpMixed::Null)?);
+ }
+
+ let repository_opt = input.borrow().get_option("repository")?;
+ let repository_url_opt = input.borrow().get_option("repository-url")?;
+ let repositories = if repository_opt
+ .as_list()
+ .map(|l| !l.is_empty())
+ .unwrap_or(false)
+ {
+ Some(repository_opt)
+ } else {
+ Some(repository_url_opt)
+ };
+
+ self.install_project(
+ io,
+ config,
+ input.clone(),
+ input
+ .borrow()
+ .get_argument("package")?
+ .as_string()
+ .map(|s| s.to_string()),
+ input
+ .borrow()
+ .get_argument("directory")?
+ .as_string()
+ .map(|s| s.to_string()),
+ input
+ .borrow()
+ .get_argument("version")?
+ .as_string()
+ .map(|s| s.to_string()),
+ input
+ .borrow()
+ .get_option("stability")?
+ .as_string()
+ .map(|s| s.to_string()),
+ prefer_source,
+ prefer_dist,
+ !input
+ .borrow()
+ .get_option("no-dev")?
+ .as_bool()
+ .unwrap_or(false),
+ repositories,
+ input
+ .borrow()
+ .get_option("no-plugins")?
+ .as_bool()
+ .unwrap_or(false),
+ input
+ .borrow()
+ .get_option("no-scripts")?
+ .as_bool()
+ .unwrap_or(false),
+ input
+ .borrow()
+ .get_option("no-progress")?
+ .as_bool()
+ .unwrap_or(false),
+ input
+ .borrow()
+ .get_option("no-install")?
+ .as_bool()
+ .unwrap_or(false),
+ Some(self.get_platform_requirement_filter(input.clone())?),
+ !input
+ .borrow()
+ .get_option("no-secure-http")?
+ .as_bool()
+ .unwrap_or(false),
+ input
+ .borrow()
+ .get_option("add-repository")?
+ .as_bool()
+ .unwrap_or(false),
+ )
+ }
+
+ fn initialize(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<()> {
+ base_command_initialize(self, input, output)
+ }
+
+ fn complete(
+ &self,
+ input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
+ suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ crate::command::base_command::base_command_complete(self, input, suggestions)
+ }
+
+ shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl BaseCommand for CreateProjectCommand {
+ fn base_command_data(&self) -> &crate::command::BaseCommandData {
+ &self.base_command_data
+ }
+
+ crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
+}
diff --git a/crates/shirabe/src/command/diagnose_command.rs b/crates/shirabe/src/command/diagnose_command.rs
index 1674e10e..8483eb56 100644
--- a/crates/shirabe/src/command/diagnose_command.rs
+++ b/crates/shirabe/src/command/diagnose_command.rs
@@ -77,396 +77,7 @@ impl DiagnoseCommand {
.expect("DiagnoseCommand::configure uses static, valid metadata");
command
}
-}
-
-impl Command for DiagnoseCommand {
- fn configure(&self) -> anyhow::Result<()> {
- self.set_name("diagnose")?;
- self.set_description("Diagnoses the system to identify common errors");
- self.set_help(
- "The <info>diagnose</info> command checks common errors to help debugging problems.\n\n\
- The process exit code will be 1 in case of warnings and 2 for errors.\n\n\
- Read more at https://getcomposer.org/doc/03-cli.md#diagnose",
- );
- Ok(())
- }
-
- fn execute(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- let mut composer = self.try_composer(None, None);
- let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io().clone();
-
- let config: std::rc::Rc<std::cell::RefCell<Config>>;
- if let Some(ref mut c) = composer {
- let c = crate::composer::composer_full(c);
- config = c.get_config();
-
- let command_event = CommandEvent::new6(
- PluginEvents::COMMAND,
- "diagnose",
- input,
- output,
- vec![],
- IndexMap::new(),
- );
- c.get_event_dispatcher()
- .borrow_mut()
- .dispatch(Some(command_event.get_name()), None);
- *self.process.borrow_mut() = Some(
- c.get_loop()
- .borrow()
- .get_process_executor()
- .map(std::rc::Rc::clone)
- .unwrap_or_else(|| {
- std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
- io.clone(),
- ))))
- }),
- );
- } else {
- config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?));
-
- *self.process.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(
- ProcessExecutor::new(Some(io.clone())),
- )));
- }
- let mut config_inner = IndexMap::new();
- config_inner.insert("secure-http".to_string(), PhpMixed::Bool(false));
- let mut secure_http_wrap: IndexMap<String, PhpMixed> = IndexMap::new();
- secure_http_wrap.insert("config".to_string(), PhpMixed::Array(config_inner));
- let config = config;
- config
- .borrow_mut()
- .merge(&secure_http_wrap, Config::SOURCE_COMMAND);
- let _ = config.borrow_mut().prohibit_url_by_config(
- "http://repo.packagist.org",
- Some(std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()))),
- &IndexMap::new(),
- );
-
- *self.http_downloader.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(
- Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?,
- )));
-
- if strpos(file!(), "phar:") == Some(0) {
- io.write_no_newline("Checking pubkeys: ");
- let r = self.check_pub_keys(&config.borrow())?;
- self.output_result(r);
-
- io.write_no_newline("Checking Composer version: ");
- let r = self.check_version(&config)?;
- self.output_result(r);
- }
-
- io.write(&format!(
- "Composer version: <comment>{}</comment>",
- composer::get_version()
- ));
-
- io.write_no_newline("Checking Composer and its dependencies for vulnerabilities: ");
- let r = self.check_composer_audit(&config)?;
- self.output_result(r);
-
- let platform_overrides = config
- .borrow_mut()
- .get("platform")
- .as_array()
- .cloned()
- .unwrap_or_default();
- let platform_overrides_unboxed: indexmap::IndexMap<String, PhpMixed> =
- platform_overrides.into_iter().collect();
- let mut platform_repo =
- PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap();
- let php_pkg = <PlatformRepository as crate::repository::RepositoryInterface>::find_package(
- &mut platform_repo,
- "php",
- crate::repository::FindPackageConstraint::String("*".to_string()),
- )?
- .unwrap();
- let mut php_version = php_pkg.get_pretty_version();
- if let Some(cp) = php_pkg.as_complete()
- && str_contains(&cp.get_description().unwrap_or_default(), "overridden")
- {
- php_version = format!(
- "{} - {}",
- php_version,
- cp.get_description().unwrap_or_default()
- );
- }
-
- io.write(&format!("PHP version: <comment>{}</comment>", php_version));
-
- let diagnostics = shirabe_php_rpc::get_diagnostics();
-
- if let Some(php_binary) = &diagnostics.php_binary {
- io.write(&format!(
- "PHP binary path: <comment>{}</comment>",
- php_binary
- ));
- }
-
- io.write(&format!(
- "OpenSSL version: {}",
- match &diagnostics.openssl_version_text {
- Some(text) => format!("<comment>{}</comment>", text),
- None => "<error>missing</error>".to_string(),
- }
- ));
- io.write(&format!("curl version: {}", self.get_curl_version()));
-
- let finder = ExecutableFinder::new();
- let has_system_unzip = finder.find("unzip", None, &[]).is_some();
- let mut bin_7zip = String::new();
- let has_system_7zip = if finder
- .find("7z", None, &["C:\\Program Files\\7-Zip".to_string()])
- .is_some()
- {
- bin_7zip = "7z".to_string();
- true
- } else if !Platform::is_windows() && finder.find("7zz", None, &[]).is_some() {
- bin_7zip = "7zz".to_string();
- true
- } else if !Platform::is_windows() && finder.find("7za", None, &[]).is_some() {
- bin_7zip = "7za".to_string();
- true
- } else {
- false
- };
-
- io.write(&format!(
- "zip: {}, {}, {}{}",
- if diagnostics.extension_loaded("zip") {
- "<comment>extension present</comment>"
- } else {
- "<comment>extension not loaded</comment>"
- },
- if has_system_unzip {
- "<comment>unzip present</comment>".to_string()
- } else {
- "<comment>unzip not available</comment>".to_string()
- },
- if has_system_7zip {
- format!("<comment>7-Zip present ({})</comment>", bin_7zip)
- } else {
- "<comment>7-Zip not available</comment>".to_string()
- },
- if (has_system_7zip || has_system_unzip) && !diagnostics.function_exists("proc_open") {
- ", <warning>proc_open is disabled or not present, unzip/7-z will not be usable</warning>"
- } else {
- ""
- }
- ));
-
- if let Some(ref mut c) = composer {
- let c = crate::composer::composer_full(c);
- io.write(&format!(
- "Active plugins: {}",
- implode(
- ", ",
- &c.get_plugin_manager().borrow().get_registered_plugins()
- )
- ));
-
- io.write_no_newline("Checking composer.json: ");
- let r = self.check_composer_schema()?;
- self.output_result(r);
-
- if c.get_locker().borrow_mut().is_locked() {
- io.write_no_newline("Checking composer.lock: ");
- let locker = c.get_locker().clone();
- let locker = locker.borrow();
- let r = self.check_composer_lock_schema(&*locker)?;
- self.output_result(r);
- }
- }
-
- io.write_no_newline("Checking platform settings: ");
- let r = self.check_platform()?;
- self.output_result(r);
-
- io.write_no_newline("Checking git settings: ");
- let r = self.check_git();
- self.output_result(PhpMixed::String(r));
-
- io.write_no_newline("Checking http connectivity to packagist: ");
- let r = self.check_http("http", &config)?;
- self.output_result(r);
-
- io.write_no_newline("Checking https connectivity to packagist: ");
- let r = self.check_http("https", &config)?;
- self.output_result(r);
-
- let repositories = config.borrow().get_repositories();
- for repo in repositories {
- let repo_arr = repo.1.as_array().cloned().unwrap_or_default();
- if repo_arr.get("type").and_then(|v| v.as_string()) == Some("composer")
- && repo_arr.get("url").is_some()
- {
- let repo_arr_unboxed: indexmap::IndexMap<String, PhpMixed> = repo_arr
- .iter()
- .map(|(k, v)| (k.clone(), v.clone()))
- .collect();
- let composer_repo = ComposerRepository::new(
- repo_arr_unboxed,
- self.get_io().clone(),
- &config.borrow(),
- self.http_downloader.borrow().clone().unwrap(),
- None,
- )
- .unwrap();
- // PHP: ReflectionMethod($composerRepo, 'getPackagesJsonUrl')
- // We surface the same internal call by directly invoking the equivalent method.
- // TODO(plugin): support reflection-based access if plugin code requires it.
- let url = composer_repo.get_packages_json_url();
- if !str_starts_with(&url, "http") {
- continue;
- }
- if str_starts_with(&url, "https://repo.packagist.org") {
- continue;
- }
- io.write_no_newline(&format!(
- "Checking connectivity to {}: ",
- repo_arr
- .get("url")
- .and_then(|v| v.as_string())
- .unwrap_or("")
- ));
- let r = self.check_composer_repo(&url, &config)?;
- self.output_result(r);
- }
- }
-
- let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) {
- vec!["http"]
- } else {
- vec!["http", "https"]
- };
- let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> {
- for proto in &protos {
- // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively
- // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader /
- // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not
- // still be held when that call happens.
- let proxy = ProxyManager::get_instance()
- .as_ref()
- .unwrap()
- .get_proxy_for_request(&format!("{}://repo.packagist.org", proto))
- .map_err(|e| anyhow::anyhow!(e))?;
- if !proxy.get_status(None)?.is_empty() {
- let r#type = if proxy.is_secure() { "HTTPS" } else { "HTTP" };
- io.write_no_newline(&format!("Checking {} proxy with {}: ", r#type, proto));
- let r = self.check_http_proxy(&proxy, proto)?;
- self.output_result(r);
- }
- }
- Ok(())
- })();
- if let Err(e) = proxy_check_result {
- if let Some(_te) = e.downcast_ref::<TransportException>() {
- io.write_no_newline("Checking HTTP proxy: ");
- let status = self.check_connectivity_and_composer_network_http_enablement();
- self.output_result(if is_string(&status) {
- status
- } else {
- PhpMixed::String(format!("<error>[{}] {}</error>", get_class_err(&e), e))
- });
- } else {
- return Err(e);
- }
- }
-
- let oauth = config
- .borrow_mut()
- .get("github-oauth")
- .as_array()
- .cloned()
- .unwrap_or_default();
- if oauth.len() as i64 > 0 {
- for (domain, token) in &oauth {
- io.write_no_newline(&format!("Checking {} oauth access: ", domain));
- let r = self.check_github_oauth(domain, token.as_string().unwrap_or(""))?;
- self.output_result(r);
- }
- } else {
- io.write_no_newline("Checking github.com rate limit: ");
- match self.get_github_rate_limit("github.com", None) {
- Ok(rate) => {
- if !is_array(&rate) {
- self.output_result(rate);
- } else if let Some(arr) = rate.as_array() {
- let remaining = arr.get("remaining").and_then(|v| v.as_int()).unwrap_or(0);
- let limit = arr.get("limit").and_then(|v| v.as_int()).unwrap_or(0);
- if 10 > remaining {
- io.write("<warning>WARNING</warning>");
- io.write(&format!(
- "<comment>GitHub has a rate limit on their API. You currently have <options=bold>{}</options=bold> out of <options=bold>{}</options=bold> requests left.\nSee https://developer.github.com/v3/#rate-limiting and also\n https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens</comment>",
- remaining, limit,
- ));
- } else {
- self.output_result(PhpMixed::Bool(true));
- }
- }
- }
- Err(e) => {
- if let Some(te) = e.downcast_ref::<TransportException>() {
- if te.get_code() == 401 {
- self.output_result(PhpMixed::String("<comment>The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it</comment>".to_string()));
- } else {
- self.output_result(PhpMixed::String(format!(
- "<error>[{}] {}</error>",
- get_class_err(&e),
- e
- )));
- }
- } else {
- self.output_result(PhpMixed::String(format!(
- "<error>[{}] {}</error>",
- get_class_err(&e),
- e
- )));
- }
- }
- }
- }
-
- io.write_no_newline("Checking disk free space: ");
- let r = self.check_disk_space(&config.borrow());
- self.output_result(r);
-
- Ok(self.exit_code.get())
- }
-
- fn initialize(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<()> {
- base_command_initialize(self, input, output)
- }
-
- fn complete(
- &self,
- input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
- suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
- ) -> anyhow::Result<()> {
- crate::command::base_command::base_command_complete(self, input, suggestions)
- }
-
- shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
-}
-
-impl BaseCommand for DiagnoseCommand {
- fn base_command_data(&self) -> &crate::command::BaseCommandData {
- &self.base_command_data
- }
-
- crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
-}
-impl DiagnoseCommand {
fn check_composer_schema(&self) -> anyhow::Result<PhpMixed> {
let validator = ConfigValidator::new(self.get_io().clone());
let (errors, _, warnings) = validator.validate(&Factory::get_composer_file()?, 0, 0);
@@ -1511,3 +1122,390 @@ impl DiagnoseCommand {
PhpMixed::Bool(true)
}
}
+
+impl Command for DiagnoseCommand {
+ fn configure(&self) -> anyhow::Result<()> {
+ self.set_name("diagnose")?;
+ self.set_description("Diagnoses the system to identify common errors");
+ self.set_help(
+ "The <info>diagnose</info> command checks common errors to help debugging problems.\n\n\
+ The process exit code will be 1 in case of warnings and 2 for errors.\n\n\
+ Read more at https://getcomposer.org/doc/03-cli.md#diagnose",
+ );
+ Ok(())
+ }
+
+ fn execute(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ let mut composer = self.try_composer(None, None);
+ let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = self.get_io().clone();
+
+ let config: std::rc::Rc<std::cell::RefCell<Config>>;
+ if let Some(ref mut c) = composer {
+ let c = crate::composer::composer_full(c);
+ config = c.get_config();
+
+ let command_event = CommandEvent::new6(
+ PluginEvents::COMMAND,
+ "diagnose",
+ input,
+ output,
+ vec![],
+ IndexMap::new(),
+ );
+ c.get_event_dispatcher()
+ .borrow_mut()
+ .dispatch(Some(command_event.get_name()), None);
+ *self.process.borrow_mut() = Some(
+ c.get_loop()
+ .borrow()
+ .get_process_executor()
+ .map(std::rc::Rc::clone)
+ .unwrap_or_else(|| {
+ std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
+ io.clone(),
+ ))))
+ }),
+ );
+ } else {
+ config = std::rc::Rc::new(std::cell::RefCell::new(Factory::create_config(None, None)?));
+
+ *self.process.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(
+ ProcessExecutor::new(Some(io.clone())),
+ )));
+ }
+ let mut config_inner = IndexMap::new();
+ config_inner.insert("secure-http".to_string(), PhpMixed::Bool(false));
+ let mut secure_http_wrap: IndexMap<String, PhpMixed> = IndexMap::new();
+ secure_http_wrap.insert("config".to_string(), PhpMixed::Array(config_inner));
+ let config = config;
+ config
+ .borrow_mut()
+ .merge(&secure_http_wrap, Config::SOURCE_COMMAND);
+ let _ = config.borrow_mut().prohibit_url_by_config(
+ "http://repo.packagist.org",
+ Some(std::rc::Rc::new(std::cell::RefCell::new(NullIO::new()))),
+ &IndexMap::new(),
+ );
+
+ *self.http_downloader.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(
+ Factory::create_http_downloader(io.clone(), &config, indexmap::IndexMap::new())?,
+ )));
+
+ if strpos(file!(), "phar:") == Some(0) {
+ io.write_no_newline("Checking pubkeys: ");
+ let r = self.check_pub_keys(&config.borrow())?;
+ self.output_result(r);
+
+ io.write_no_newline("Checking Composer version: ");
+ let r = self.check_version(&config)?;
+ self.output_result(r);
+ }
+
+ io.write(&format!(
+ "Composer version: <comment>{}</comment>",
+ composer::get_version()
+ ));
+
+ io.write_no_newline("Checking Composer and its dependencies for vulnerabilities: ");
+ let r = self.check_composer_audit(&config)?;
+ self.output_result(r);
+
+ let platform_overrides = config
+ .borrow_mut()
+ .get("platform")
+ .as_array()
+ .cloned()
+ .unwrap_or_default();
+ let platform_overrides_unboxed: indexmap::IndexMap<String, PhpMixed> =
+ platform_overrides.into_iter().collect();
+ let mut platform_repo =
+ PlatformRepository::new(vec![], platform_overrides_unboxed).unwrap();
+ let php_pkg = <PlatformRepository as crate::repository::RepositoryInterface>::find_package(
+ &mut platform_repo,
+ "php",
+ crate::repository::FindPackageConstraint::String("*".to_string()),
+ )?
+ .unwrap();
+ let mut php_version = php_pkg.get_pretty_version();
+ if let Some(cp) = php_pkg.as_complete()
+ && str_contains(&cp.get_description().unwrap_or_default(), "overridden")
+ {
+ php_version = format!(
+ "{} - {}",
+ php_version,
+ cp.get_description().unwrap_or_default()
+ );
+ }
+
+ io.write(&format!("PHP version: <comment>{}</comment>", php_version));
+
+ let diagnostics = shirabe_php_rpc::get_diagnostics();
+
+ if let Some(php_binary) = &diagnostics.php_binary {
+ io.write(&format!(
+ "PHP binary path: <comment>{}</comment>",
+ php_binary
+ ));
+ }
+
+ io.write(&format!(
+ "OpenSSL version: {}",
+ match &diagnostics.openssl_version_text {
+ Some(text) => format!("<comment>{}</comment>", text),
+ None => "<error>missing</error>".to_string(),
+ }
+ ));
+ io.write(&format!("curl version: {}", self.get_curl_version()));
+
+ let finder = ExecutableFinder::new();
+ let has_system_unzip = finder.find("unzip", None, &[]).is_some();
+ let mut bin_7zip = String::new();
+ let has_system_7zip = if finder
+ .find("7z", None, &["C:\\Program Files\\7-Zip".to_string()])
+ .is_some()
+ {
+ bin_7zip = "7z".to_string();
+ true
+ } else if !Platform::is_windows() && finder.find("7zz", None, &[]).is_some() {
+ bin_7zip = "7zz".to_string();
+ true
+ } else if !Platform::is_windows() && finder.find("7za", None, &[]).is_some() {
+ bin_7zip = "7za".to_string();
+ true
+ } else {
+ false
+ };
+
+ io.write(&format!(
+ "zip: {}, {}, {}{}",
+ if diagnostics.extension_loaded("zip") {
+ "<comment>extension present</comment>"
+ } else {
+ "<comment>extension not loaded</comment>"
+ },
+ if has_system_unzip {
+ "<comment>unzip present</comment>".to_string()
+ } else {
+ "<comment>unzip not available</comment>".to_string()
+ },
+ if has_system_7zip {
+ format!("<comment>7-Zip present ({})</comment>", bin_7zip)
+ } else {
+ "<comment>7-Zip not available</comment>".to_string()
+ },
+ if (has_system_7zip || has_system_unzip) && !diagnostics.function_exists("proc_open") {
+ ", <warning>proc_open is disabled or not present, unzip/7-z will not be usable</warning>"
+ } else {
+ ""
+ }
+ ));
+
+ if let Some(ref mut c) = composer {
+ let c = crate::composer::composer_full(c);
+ io.write(&format!(
+ "Active plugins: {}",
+ implode(
+ ", ",
+ &c.get_plugin_manager().borrow().get_registered_plugins()
+ )
+ ));
+
+ io.write_no_newline("Checking composer.json: ");
+ let r = self.check_composer_schema()?;
+ self.output_result(r);
+
+ if c.get_locker().borrow_mut().is_locked() {
+ io.write_no_newline("Checking composer.lock: ");
+ let locker = c.get_locker().clone();
+ let locker = locker.borrow();
+ let r = self.check_composer_lock_schema(&*locker)?;
+ self.output_result(r);
+ }
+ }
+
+ io.write_no_newline("Checking platform settings: ");
+ let r = self.check_platform()?;
+ self.output_result(r);
+
+ io.write_no_newline("Checking git settings: ");
+ let r = self.check_git();
+ self.output_result(PhpMixed::String(r));
+
+ io.write_no_newline("Checking http connectivity to packagist: ");
+ let r = self.check_http("http", &config)?;
+ self.output_result(r);
+
+ io.write_no_newline("Checking https connectivity to packagist: ");
+ let r = self.check_http("https", &config)?;
+ self.output_result(r);
+
+ let repositories = config.borrow().get_repositories();
+ for repo in repositories {
+ let repo_arr = repo.1.as_array().cloned().unwrap_or_default();
+ if repo_arr.get("type").and_then(|v| v.as_string()) == Some("composer")
+ && repo_arr.get("url").is_some()
+ {
+ let repo_arr_unboxed: indexmap::IndexMap<String, PhpMixed> = repo_arr
+ .iter()
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect();
+ let composer_repo = ComposerRepository::new(
+ repo_arr_unboxed,
+ self.get_io().clone(),
+ &config.borrow(),
+ self.http_downloader.borrow().clone().unwrap(),
+ None,
+ )
+ .unwrap();
+ // PHP: ReflectionMethod($composerRepo, 'getPackagesJsonUrl')
+ // We surface the same internal call by directly invoking the equivalent method.
+ // TODO(plugin): support reflection-based access if plugin code requires it.
+ let url = composer_repo.get_packages_json_url();
+ if !str_starts_with(&url, "http") {
+ continue;
+ }
+ if str_starts_with(&url, "https://repo.packagist.org") {
+ continue;
+ }
+ io.write_no_newline(&format!(
+ "Checking connectivity to {}: ",
+ repo_arr
+ .get("url")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ ));
+ let r = self.check_composer_repo(&url, &config)?;
+ self.output_result(r);
+ }
+ }
+
+ let protos: Vec<&str> = if config.borrow_mut().get("disable-tls").as_bool() == Some(true) {
+ vec!["http"]
+ } else {
+ vec!["http", "https"]
+ };
+ let proxy_check_result: anyhow::Result<(), anyhow::Error> = (|| -> anyhow::Result<()> {
+ for proto in &protos {
+ // Compute the proxy under a short-lived lock: `check_http_proxy` below transitively
+ // re-enters `ProxyManager::get_instance()` (via HttpDownloader -> CurlDownloader /
+ // RemoteFilesystem), and `std::sync::Mutex` is not reentrant, so the guard must not
+ // still be held when that call happens.
+ let proxy = ProxyManager::get_instance()
+ .as_ref()
+ .unwrap()
+ .get_proxy_for_request(&format!("{}://repo.packagist.org", proto))
+ .map_err(|e| anyhow::anyhow!(e))?;
+ if !proxy.get_status(None)?.is_empty() {
+ let r#type = if proxy.is_secure() { "HTTPS" } else { "HTTP" };
+ io.write_no_newline(&format!("Checking {} proxy with {}: ", r#type, proto));
+ let r = self.check_http_proxy(&proxy, proto)?;
+ self.output_result(r);
+ }
+ }
+ Ok(())
+ })();
+ if let Err(e) = proxy_check_result {
+ if let Some(_te) = e.downcast_ref::<TransportException>() {
+ io.write_no_newline("Checking HTTP proxy: ");
+ let status = self.check_connectivity_and_composer_network_http_enablement();
+ self.output_result(if is_string(&status) {
+ status
+ } else {
+ PhpMixed::String(format!("<error>[{}] {}</error>", get_class_err(&e), e))
+ });
+ } else {
+ return Err(e);
+ }
+ }
+
+ let oauth = config
+ .borrow_mut()
+ .get("github-oauth")
+ .as_array()
+ .cloned()
+ .unwrap_or_default();
+ if oauth.len() as i64 > 0 {
+ for (domain, token) in &oauth {
+ io.write_no_newline(&format!("Checking {} oauth access: ", domain));
+ let r = self.check_github_oauth(domain, token.as_string().unwrap_or(""))?;
+ self.output_result(r);
+ }
+ } else {
+ io.write_no_newline("Checking github.com rate limit: ");
+ match self.get_github_rate_limit("github.com", None) {
+ Ok(rate) => {
+ if !is_array(&rate) {
+ self.output_result(rate);
+ } else if let Some(arr) = rate.as_array() {
+ let remaining = arr.get("remaining").and_then(|v| v.as_int()).unwrap_or(0);
+ let limit = arr.get("limit").and_then(|v| v.as_int()).unwrap_or(0);
+ if 10 > remaining {
+ io.write("<warning>WARNING</warning>");
+ io.write(&format!(
+ "<comment>GitHub has a rate limit on their API. You currently have <options=bold>{}</options=bold> out of <options=bold>{}</options=bold> requests left.\nSee https://developer.github.com/v3/#rate-limiting and also\n https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens</comment>",
+ remaining, limit,
+ ));
+ } else {
+ self.output_result(PhpMixed::Bool(true));
+ }
+ }
+ }
+ Err(e) => {
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ if te.get_code() == 401 {
+ self.output_result(PhpMixed::String("<comment>The oauth token for github.com seems invalid, run \"composer config --global --unset github-oauth.github.com\" to remove it</comment>".to_string()));
+ } else {
+ self.output_result(PhpMixed::String(format!(
+ "<error>[{}] {}</error>",
+ get_class_err(&e),
+ e
+ )));
+ }
+ } else {
+ self.output_result(PhpMixed::String(format!(
+ "<error>[{}] {}</error>",
+ get_class_err(&e),
+ e
+ )));
+ }
+ }
+ }
+ }
+
+ io.write_no_newline("Checking disk free space: ");
+ let r = self.check_disk_space(&config.borrow());
+ self.output_result(r);
+
+ Ok(self.exit_code.get())
+ }
+
+ fn initialize(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<()> {
+ base_command_initialize(self, input, output)
+ }
+
+ fn complete(
+ &self,
+ input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
+ suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ crate::command::base_command::base_command_complete(self, input, suggestions)
+ }
+
+ shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl BaseCommand for DiagnoseCommand {
+ fn base_command_data(&self) -> &crate::command::BaseCommandData {
+ &self.base_command_data
+ }
+
+ crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
+}
diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs
index 99a7de00..f2bf19d7 100644
--- a/crates/shirabe/src/command/init_command.rs
+++ b/crates/shirabe/src/command/init_command.rs
@@ -84,6 +84,370 @@ impl InitCommand {
.expect("InitCommand::configure uses static, valid metadata");
command
}
+
+ fn parse_author_string(
+ &self,
+ author: &str,
+ ) -> anyhow::Result<IndexMap<String, Option<String>>> {
+ let mut m: IndexMap<CaptureKey, String> = IndexMap::new();
+ if Preg::is_match3(
+ php_regex!(r#"/^(?P<name>[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P<email>.+?)>)?$/u"#),
+ author,
+ Some(&mut m),
+ ) {
+ let email = m.get(&CaptureKey::ByName("email".to_string())).cloned();
+ if let Some(ref email) = email
+ && !self.is_valid_email(email)
+ {
+ return Err(InvalidArgumentException {
+ message: format!("Invalid email \"{}\"", email),
+ code: 0,
+ }
+ .into());
+ }
+
+ let mut result: IndexMap<String, Option<String>> = IndexMap::new();
+ result.insert(
+ "name".to_string(),
+ Some(trim(
+ &m.get(&CaptureKey::ByName("name".to_string()))
+ .cloned()
+ .unwrap_or_default(),
+ None,
+ )),
+ );
+ result.insert("email".to_string(), email);
+
+ return Ok(result);
+ }
+
+ Err(InvalidArgumentException {
+ message: "Invalid author string. Must be in the formats: Jane Doe or John Smith <john@example.com>"
+ .to_string(),
+ code: 0,
+ }
+ .into())
+ }
+
+ pub(crate) fn format_authors(
+ &self,
+ author: &str,
+ ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> {
+ let parsed = self.parse_author_string(author)?;
+ let mut author_map: IndexMap<String, PhpMixed> = IndexMap::new();
+ let name = parsed.get("name").cloned().unwrap_or(None);
+ let email = parsed.get("email").cloned().unwrap_or(None);
+ if let Some(name) = name {
+ author_map.insert("name".to_string(), PhpMixed::String(name));
+ }
+ if let Some(email) = email {
+ author_map.insert("email".to_string(), PhpMixed::String(email));
+ }
+
+ Ok(vec![author_map])
+ }
+
+ /// Extract namespace from package's vendor name.
+ ///
+ /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName"
+ pub fn namespace_from_package_name(&self, package_name: &str) -> Option<String> {
+ if package_name.is_empty() || strpos(package_name, "/").is_none() {
+ return None;
+ }
+
+ let namespace: Vec<String> = array_map(
+ |part: &String| {
+ let part = Preg::replace(php_regex!(r"/[^a-z0-9]/i"), " ", part);
+ let part = ucwords(&part);
+ str_replace(" ", "", &part)
+ },
+ &explode("/", package_name),
+ );
+
+ Some(implode("\\", &namespace))
+ }
+
+ pub(crate) fn get_git_config(&self) -> IndexMap<String, String> {
+ if self.git_config.borrow().is_some() {
+ return self.git_config.borrow().clone().unwrap_or_default();
+ }
+
+ let mut process = ProcessExecutor::new(Some(self.get_io().clone()));
+
+ let mut output = String::new();
+ if process.execute_args(
+ &["git".to_string(), "config".to_string(), "-l".to_string()],
+ &mut output,
+ None,
+ ) == 0
+ {
+ *self.git_config.borrow_mut() = Some(IndexMap::new());
+ let mut m: IndexMap<CaptureKey, Vec<String>> = IndexMap::new();
+ if Preg::is_match_all3(php_regex!(r"{^([^=]+)=(.*)$}m"), &output, Some(&mut m)) {
+ let keys: Vec<String> = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default();
+ let values: Vec<String> =
+ m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default();
+ for (key, value) in keys.iter().zip(values.iter()) {
+ self.git_config
+ .borrow_mut()
+ .as_mut()
+ .unwrap()
+ .insert(key.clone(), value.clone());
+ }
+ }
+
+ return self.git_config.borrow().clone().unwrap_or_default();
+ }
+
+ *self.git_config.borrow_mut() = Some(IndexMap::new());
+ IndexMap::new()
+ }
+
+ /// Checks the local .gitignore file for the Composer vendor directory.
+ ///
+ /// Tested patterns include:
+ /// "/$vendor"
+ /// "$vendor"
+ /// "$vendor/"
+ /// "/$vendor/"
+ /// "/$vendor/*"
+ /// "$vendor/*"
+ pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool {
+ if !file_exists(ignore_file) {
+ return false;
+ }
+
+ let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None));
+
+ let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default();
+ for line in &lines {
+ if Preg::is_match(&pattern, line) {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) {
+ let mut contents = String::new();
+ if file_exists(ignore_file) {
+ contents = file_get_contents(ignore_file).unwrap_or_default();
+
+ if strpos(&contents, "\n") != Some(0) {
+ contents.push('\n');
+ }
+ }
+
+ file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes());
+ }
+
+ /// For testing only: invoke the private `parse_author_string`.
+ pub fn __parse_author_string(
+ &self,
+ author: &str,
+ ) -> anyhow::Result<IndexMap<String, Option<String>>> {
+ self.parse_author_string(author)
+ }
+
+ /// For testing only: invoke the crate-private `format_authors`.
+ pub fn __format_authors(
+ &self,
+ author: &str,
+ ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> {
+ self.format_authors(author)
+ }
+
+ /// For testing only: invoke the crate-private `get_git_config`.
+ pub fn __get_git_config(&self) -> IndexMap<String, String> {
+ self.get_git_config()
+ }
+
+ /// For testing only: invoke the crate-private `has_vendor_ignore`.
+ pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool {
+ self.has_vendor_ignore(ignore_file, vendor)
+ }
+
+ /// For testing only: invoke the crate-private `add_vendor_ignore`.
+ pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) {
+ self.add_vendor_ignore(ignore_file, vendor)
+ }
+
+ pub(crate) fn is_valid_email(&self, email: &str) -> bool {
+ shirabe_php_shim::filter_var_email(email)
+ }
+
+ fn update_dependencies(&self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) {
+ let result = (|| -> anyhow::Result<i64> {
+ let application = self
+ .get_application()
+ .expect("a Composer command's application is always set");
+ let update_command = application.borrow_mut().find("update")?;
+ self.reset_composer()?;
+ let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?));
+ let command = update_command.borrow();
+ command.run(input, output)
+ })();
+
+ if result.is_err() {
+ self.get_io().borrow().write_error(
+ "Could not update dependencies. Run `composer update` to see more information.",
+ );
+ }
+ }
+
+ fn run_dump_autoload_command(
+ &self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) {
+ let result = (|| -> anyhow::Result<i64> {
+ let application = self
+ .get_application()
+ .expect("a Composer command's application is always set");
+ let command = application.borrow_mut().find("dump-autoload")?;
+ self.reset_composer()?;
+ let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?));
+ let command = command.borrow();
+ command.run(input, output)
+ })();
+
+ if result.is_err() {
+ self.get_io()
+ .borrow()
+ .write_error("Could not run dump-autoload.");
+ }
+ }
+
+ fn has_dependencies(&self, options: &IndexMap<String, PhpMixed>) -> bool {
+ let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null);
+ let requires_arr_empty = match &requires {
+ PhpMixed::Array(m) => m.is_empty(),
+ PhpMixed::List(l) => l.is_empty(),
+ PhpMixed::Null => true,
+ _ => false,
+ };
+ let dev_requires = options.get("require-dev").cloned();
+ let dev_requires_arr_empty = match &dev_requires {
+ Some(PhpMixed::Array(m)) => m.is_empty(),
+ Some(PhpMixed::List(l)) => l.is_empty(),
+ Some(PhpMixed::Null) | None => true,
+ _ => false,
+ };
+
+ !requires_arr_empty || !dev_requires_arr_empty
+ }
+
+ fn sanitize_package_name_component(&self, name: &str) -> String {
+ let name = Preg::replace(
+ php_regex!(r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"),
+ "$1$3-$2$4",
+ name,
+ );
+ let name = strtolower(&name);
+ let name = Preg::replace(php_regex!(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u"), "", &name);
+
+ Preg::replace(php_regex!(r"{([_.-]){2,}}u"), "$1", &name)
+ }
+
+ fn get_default_package_name(&self) -> String {
+ let git = self.get_git_config();
+ let cwd = realpath(".").unwrap_or_default();
+ let name = basename(&cwd);
+ let name = self.sanitize_package_name_component(&name);
+
+ let mut vendor = name.clone();
+ let composer_default_vendor = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_VENDOR")
+ .map(|value| value.to_string_lossy().into_owned());
+ let server_username = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("USERNAME")
+ .map(|value| value.to_string_lossy().into_owned());
+ let server_user = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("USER")
+ .map(|value| value.to_string_lossy().into_owned());
+ if !empty(
+ &composer_default_vendor
+ .clone()
+ .map(PhpMixed::String)
+ .unwrap_or(PhpMixed::Null),
+ ) {
+ vendor = composer_default_vendor.unwrap_or_default();
+ } else if git.contains_key("github.user") {
+ vendor = git.get("github.user").cloned().unwrap_or_default();
+ } else if !empty(
+ &server_username
+ .clone()
+ .map(PhpMixed::String)
+ .unwrap_or(PhpMixed::Null),
+ ) {
+ vendor = server_username.unwrap_or_default();
+ } else if !empty(
+ &server_user
+ .clone()
+ .map(PhpMixed::String)
+ .unwrap_or(PhpMixed::Null),
+ ) {
+ vendor = server_user.unwrap_or_default();
+ } else if !get_current_user().is_empty() {
+ vendor = get_current_user();
+ }
+
+ let vendor = self.sanitize_package_name_component(&vendor);
+
+ format!("{}/{}", vendor, name)
+ }
+
+ fn get_default_author(&self) -> Option<String> {
+ let git = self.get_git_config();
+
+ let mut author_name: Option<String> = None;
+ let composer_default_author = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_AUTHOR")
+ .map(|value| value.to_string_lossy().into_owned());
+ if !empty(
+ &composer_default_author
+ .clone()
+ .map(PhpMixed::String)
+ .unwrap_or(PhpMixed::Null),
+ ) {
+ author_name = composer_default_author;
+ } else if git.contains_key("user.name") {
+ author_name = git.get("user.name").cloned();
+ }
+
+ let mut author_email: Option<String> = None;
+ let composer_default_email = PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("COMPOSER_DEFAULT_EMAIL")
+ .map(|value| value.to_string_lossy().into_owned());
+ if !empty(
+ &composer_default_email
+ .clone()
+ .map(PhpMixed::String)
+ .unwrap_or(PhpMixed::Null),
+ ) {
+ author_email = composer_default_email;
+ } else if git.contains_key("user.email") {
+ author_email = git.get("user.email").cloned();
+ }
+
+ if let (Some(name), Some(email)) = (author_name, author_email) {
+ return Some(format!("{} <{}>", name, email));
+ }
+
+ None
+ }
}
impl Command for InitCommand {
@@ -921,369 +1285,3 @@ impl BaseCommand for InitCommand {
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
-
-impl InitCommand {
- fn parse_author_string(
- &self,
- author: &str,
- ) -> anyhow::Result<IndexMap<String, Option<String>>> {
- let mut m: IndexMap<CaptureKey, String> = IndexMap::new();
- if Preg::is_match3(
- php_regex!(r#"/^(?P<name>[- .,\p{L}\p{N}\p{Mn}\'’\"()]+)(?:\s+<(?P<email>.+?)>)?$/u"#),
- author,
- Some(&mut m),
- ) {
- let email = m.get(&CaptureKey::ByName("email".to_string())).cloned();
- if let Some(ref email) = email
- && !self.is_valid_email(email)
- {
- return Err(InvalidArgumentException {
- message: format!("Invalid email \"{}\"", email),
- code: 0,
- }
- .into());
- }
-
- let mut result: IndexMap<String, Option<String>> = IndexMap::new();
- result.insert(
- "name".to_string(),
- Some(trim(
- &m.get(&CaptureKey::ByName("name".to_string()))
- .cloned()
- .unwrap_or_default(),
- None,
- )),
- );
- result.insert("email".to_string(), email);
-
- return Ok(result);
- }
-
- Err(InvalidArgumentException {
- message: "Invalid author string. Must be in the formats: Jane Doe or John Smith <john@example.com>"
- .to_string(),
- code: 0,
- }
- .into())
- }
-
- pub(crate) fn format_authors(
- &self,
- author: &str,
- ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> {
- let parsed = self.parse_author_string(author)?;
- let mut author_map: IndexMap<String, PhpMixed> = IndexMap::new();
- let name = parsed.get("name").cloned().unwrap_or(None);
- let email = parsed.get("email").cloned().unwrap_or(None);
- if let Some(name) = name {
- author_map.insert("name".to_string(), PhpMixed::String(name));
- }
- if let Some(email) = email {
- author_map.insert("email".to_string(), PhpMixed::String(email));
- }
-
- Ok(vec![author_map])
- }
-
- /// Extract namespace from package's vendor name.
- ///
- /// new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName"
- pub fn namespace_from_package_name(&self, package_name: &str) -> Option<String> {
- if package_name.is_empty() || strpos(package_name, "/").is_none() {
- return None;
- }
-
- let namespace: Vec<String> = array_map(
- |part: &String| {
- let part = Preg::replace(php_regex!(r"/[^a-z0-9]/i"), " ", part);
- let part = ucwords(&part);
- str_replace(" ", "", &part)
- },
- &explode("/", package_name),
- );
-
- Some(implode("\\", &namespace))
- }
-
- pub(crate) fn get_git_config(&self) -> IndexMap<String, String> {
- if self.git_config.borrow().is_some() {
- return self.git_config.borrow().clone().unwrap_or_default();
- }
-
- let mut process = ProcessExecutor::new(Some(self.get_io().clone()));
-
- let mut output = String::new();
- if process.execute_args(
- &["git".to_string(), "config".to_string(), "-l".to_string()],
- &mut output,
- None,
- ) == 0
- {
- *self.git_config.borrow_mut() = Some(IndexMap::new());
- let mut m: IndexMap<CaptureKey, Vec<String>> = IndexMap::new();
- if Preg::is_match_all3(php_regex!(r"{^([^=]+)=(.*)$}m"), &output, Some(&mut m)) {
- let keys: Vec<String> = m.get(&CaptureKey::ByIndex(1)).cloned().unwrap_or_default();
- let values: Vec<String> =
- m.get(&CaptureKey::ByIndex(2)).cloned().unwrap_or_default();
- for (key, value) in keys.iter().zip(values.iter()) {
- self.git_config
- .borrow_mut()
- .as_mut()
- .unwrap()
- .insert(key.clone(), value.clone());
- }
- }
-
- return self.git_config.borrow().clone().unwrap_or_default();
- }
-
- *self.git_config.borrow_mut() = Some(IndexMap::new());
- IndexMap::new()
- }
-
- /// Checks the local .gitignore file for the Composer vendor directory.
- ///
- /// Tested patterns include:
- /// "/$vendor"
- /// "$vendor"
- /// "$vendor/"
- /// "/$vendor/"
- /// "/$vendor/*"
- /// "$vendor/*"
- pub(crate) fn has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool {
- if !file_exists(ignore_file) {
- return false;
- }
-
- let pattern = format!("{{^/?{}(/\\*?)?$}}", preg_quote(vendor, None));
-
- let lines = file(ignore_file, FILE_IGNORE_NEW_LINES).unwrap_or_default();
- for line in &lines {
- if Preg::is_match(&pattern, line) {
- return true;
- }
- }
-
- false
- }
-
- pub(crate) fn add_vendor_ignore(&self, ignore_file: &str, vendor: &str) {
- let mut contents = String::new();
- if file_exists(ignore_file) {
- contents = file_get_contents(ignore_file).unwrap_or_default();
-
- if strpos(&contents, "\n") != Some(0) {
- contents.push('\n');
- }
- }
-
- file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes());
- }
-
- /// For testing only: invoke the private `parse_author_string`.
- pub fn __parse_author_string(
- &self,
- author: &str,
- ) -> anyhow::Result<IndexMap<String, Option<String>>> {
- self.parse_author_string(author)
- }
-
- /// For testing only: invoke the crate-private `format_authors`.
- pub fn __format_authors(
- &self,
- author: &str,
- ) -> anyhow::Result<Vec<IndexMap<String, PhpMixed>>> {
- self.format_authors(author)
- }
-
- /// For testing only: invoke the crate-private `get_git_config`.
- pub fn __get_git_config(&self) -> IndexMap<String, String> {
- self.get_git_config()
- }
-
- /// For testing only: invoke the crate-private `has_vendor_ignore`.
- pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool {
- self.has_vendor_ignore(ignore_file, vendor)
- }
-
- /// For testing only: invoke the crate-private `add_vendor_ignore`.
- pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) {
- self.add_vendor_ignore(ignore_file, vendor)
- }
-
- pub(crate) fn is_valid_email(&self, email: &str) -> bool {
- shirabe_php_shim::filter_var_email(email)
- }
-
- fn update_dependencies(&self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) {
- let result = (|| -> anyhow::Result<i64> {
- let application = self
- .get_application()
- .expect("a Composer command's application is always set");
- let update_command = application.borrow_mut().find("update")?;
- self.reset_composer()?;
- let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> =
- std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?));
- let command = update_command.borrow();
- command.run(input, output)
- })();
-
- if result.is_err() {
- self.get_io().borrow().write_error(
- "Could not update dependencies. Run `composer update` to see more information.",
- );
- }
- }
-
- fn run_dump_autoload_command(
- &self,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) {
- let result = (|| -> anyhow::Result<i64> {
- let application = self
- .get_application()
- .expect("a Composer command's application is always set");
- let command = application.borrow_mut().find("dump-autoload")?;
- self.reset_composer()?;
- let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> =
- std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(vec![], None)?));
- let command = command.borrow();
- command.run(input, output)
- })();
-
- if result.is_err() {
- self.get_io()
- .borrow()
- .write_error("Could not run dump-autoload.");
- }
- }
-
- fn has_dependencies(&self, options: &IndexMap<String, PhpMixed>) -> bool {
- let requires = options.get("require").cloned().unwrap_or(PhpMixed::Null);
- let requires_arr_empty = match &requires {
- PhpMixed::Array(m) => m.is_empty(),
- PhpMixed::List(l) => l.is_empty(),
- PhpMixed::Null => true,
- _ => false,
- };
- let dev_requires = options.get("require-dev").cloned();
- let dev_requires_arr_empty = match &dev_requires {
- Some(PhpMixed::Array(m)) => m.is_empty(),
- Some(PhpMixed::List(l)) => l.is_empty(),
- Some(PhpMixed::Null) | None => true,
- _ => false,
- };
-
- !requires_arr_empty || !dev_requires_arr_empty
- }
-
- fn sanitize_package_name_component(&self, name: &str) -> String {
- let name = Preg::replace(
- php_regex!(r"{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"),
- "$1$3-$2$4",
- name,
- );
- let name = strtolower(&name);
- let name = Preg::replace(php_regex!(r"{^[_.-]+|[_.-]+$|[^a-z0-9_.-]}u"), "", &name);
-
- Preg::replace(php_regex!(r"{([_.-]){2,}}u"), "$1", &name)
- }
-
- fn get_default_package_name(&self) -> String {
- let git = self.get_git_config();
- let cwd = realpath(".").unwrap_or_default();
- let name = basename(&cwd);
- let name = self.sanitize_package_name_component(&name);
-
- let mut vendor = name.clone();
- let composer_default_vendor = PHP_SERVER
- .lock()
- .unwrap()
- .get("COMPOSER_DEFAULT_VENDOR")
- .map(|value| value.to_string_lossy().into_owned());
- let server_username = PHP_SERVER
- .lock()
- .unwrap()
- .get("USERNAME")
- .map(|value| value.to_string_lossy().into_owned());
- let server_user = PHP_SERVER
- .lock()
- .unwrap()
- .get("USER")
- .map(|value| value.to_string_lossy().into_owned());
- if !empty(
- &composer_default_vendor
- .clone()
- .map(PhpMixed::String)
- .unwrap_or(PhpMixed::Null),
- ) {
- vendor = composer_default_vendor.unwrap_or_default();
- } else if git.contains_key("github.user") {
- vendor = git.get("github.user").cloned().unwrap_or_default();
- } else if !empty(
- &server_username
- .clone()
- .map(PhpMixed::String)
- .unwrap_or(PhpMixed::Null),
- ) {
- vendor = server_username.unwrap_or_default();
- } else if !empty(
- &server_user
- .clone()
- .map(PhpMixed::String)
- .unwrap_or(PhpMixed::Null),
- ) {
- vendor = server_user.unwrap_or_default();
- } else if !get_current_user().is_empty() {
- vendor = get_current_user();
- }
-
- let vendor = self.sanitize_package_name_component(&vendor);
-
- format!("{}/{}", vendor, name)
- }
-
- fn get_default_author(&self) -> Option<String> {
- let git = self.get_git_config();
-
- let mut author_name: Option<String> = None;
- let composer_default_author = PHP_SERVER
- .lock()
- .unwrap()
- .get("COMPOSER_DEFAULT_AUTHOR")
- .map(|value| value.to_string_lossy().into_owned());
- if !empty(
- &composer_default_author
- .clone()
- .map(PhpMixed::String)
- .unwrap_or(PhpMixed::Null),
- ) {
- author_name = composer_default_author;
- } else if git.contains_key("user.name") {
- author_name = git.get("user.name").cloned();
- }
-
- let mut author_email: Option<String> = None;
- let composer_default_email = PHP_SERVER
- .lock()
- .unwrap()
- .get("COMPOSER_DEFAULT_EMAIL")
- .map(|value| value.to_string_lossy().into_owned());
- if !empty(
- &composer_default_email
- .clone()
- .map(PhpMixed::String)
- .unwrap_or(PhpMixed::Null),
- ) {
- author_email = composer_default_email;
- } else if git.contains_key("user.email") {
- author_email = git.get("user.email").cloned();
- }
-
- if let (Some(name), Some(email)) = (author_name, author_email) {
- return Some(format!("{} <{}>", name, email));
- }
-
- None
- }
-}
diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs
index d2b8360d..f4410c19 100644
--- a/crates/shirabe/src/command/require_command.rs
+++ b/crates/shirabe/src/command/require_command.rs
@@ -89,590 +89,7 @@ impl RequireCommand {
.expect("RequireCommand::configure uses static, valid metadata");
command
}
-}
-
-impl PackageDiscoveryTrait for RequireCommand {
- fn get_repos_mut(
- &self,
- ) -> std::cell::RefMut<'_, Option<crate::repository::RepositoryInterfaceHandle>> {
- self.repos.borrow_mut()
- }
-
- fn get_repository_sets_mut(
- &self,
- ) -> std::cell::RefMut<'_, IndexMap<String, std::rc::Rc<std::cell::RefCell<RepositorySet>>>>
- {
- self.repository_sets.borrow_mut()
- }
-}
-
-impl Command for RequireCommand {
- fn configure(&self) -> anyhow::Result<()> {
- self.set_name("require")?;
- self.set_aliases(vec!["r".to_string()])?;
- self.set_description("Adds required packages to your composer.json and installs them");
- self.set_definition(&[
- InputArgument::new5("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None, self.suggest_available_package_incl_platform()).unwrap().into(),
- InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Add requirement to require-dev.", None).unwrap().into(),
- InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(),
- InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(),
- InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(),
- InputOption::new6("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None, self.suggest_prefer_install()).unwrap().into(),
- InputOption::new("fixed", None, Some(InputOption::VALUE_NONE), "Write fixed version to the composer.json.", None).unwrap().into(),
- InputOption::new("no-suggest", None, Some(InputOption::VALUE_NONE), "DEPRECATED: This flag does not exist anymore.", None).unwrap().into(),
- InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(),
- InputOption::new("no-update", None, Some(InputOption::VALUE_NONE), "Disables the automatic update of the dependencies (implies --no-install).", None).unwrap().into(),
- InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Skip the install step after updating the composer.lock file.", None).unwrap().into(),
- InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Skip the audit step after updating the composer.lock file (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(),
- InputOption::new6("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).unwrap().into(),
- InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(),
- InputOption::new("update-no-dev", None, Some(InputOption::VALUE_NONE), "Run the dependency update with the --no-dev option.", None).unwrap().into(),
- InputOption::new("update-with-dependencies", Some(PhpMixed::String("w".to_string())), Some(InputOption::VALUE_NONE), "Allows inherited dependencies to be updated, except those that are root requirements (can also be set via the COMPOSER_WITH_DEPENDENCIES=1 env var).", None).unwrap().into(),
- InputOption::new("update-with-all-dependencies", Some(PhpMixed::String("W".to_string())), Some(InputOption::VALUE_NONE), "Allows all inherited dependencies to be updated, including those that are root requirements (can also be set via the COMPOSER_WITH_ALL_DEPENDENCIES=1 env var).", None).unwrap().into(),
- InputOption::new("with-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-dependencies", None).unwrap().into(),
- InputOption::new("with-all-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-all-dependencies", None).unwrap().into(),
- InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(),
- InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(),
- InputOption::new("prefer-stable", None, Some(InputOption::VALUE_NONE), "Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).", None).unwrap().into(),
- InputOption::new("prefer-lowest", None, Some(InputOption::VALUE_NONE), "Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).", None).unwrap().into(),
- InputOption::new("minimal-changes", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "During an update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).", None).unwrap().into(),
- InputOption::new("sort-packages", None, Some(InputOption::VALUE_NONE), "Sorts packages when adding/updating a new dependency", None).unwrap().into(),
- InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(),
- InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(),
- InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(),
- InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(),
- ]);
- self.set_help(
- "The require command adds required packages to your composer.json and installs them.\n\
- \n\
- If you do not specify a package, composer will prompt you to search for a package, and given results, provide a list of\n\
- matches to require.\n\
- \n\
- If you do not specify a version constraint, composer will choose a suitable one based on the available package versions.\n\
- \n\
- If you do not want to install the new dependencies immediately you can call it with --no-update\n\
- \n\
- Read more at https://getcomposer.org/doc/03-cli.md#require-r"
- );
- Ok(())
- }
-
- /// @throws \Seld\JsonLint\ParsingException
- fn execute(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- *self.file.borrow_mut() = Factory::get_composer_file()?;
-
- if input
- .borrow()
- .get_option("no-suggest")?
- .as_bool()
- .unwrap_or(false)
- {
- self.get_io().write_error3("<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>", true, io_interface::NORMAL);
- }
-
- let file = self.file.borrow().clone();
- self.newly_created.set(!file_exists(&file));
- let write_failed =
- self.newly_created.get() && file_put_contents(&file, b"{\n}\n").is_none();
- if write_failed {
- let msg = format!("<error>{} could not be created.</error>", file);
- self.get_io().write_error3(&msg, true, io_interface::NORMAL);
-
- return Ok(1);
- }
- if !Filesystem::is_readable(&file) {
- let msg = format!("<error>{} is not readable.</error>", file);
- self.get_io().write_error3(&msg, true, io_interface::NORMAL);
-
- return Ok(1);
- }
- if filesize(&file) == Some(0) {
- file_put_contents(&file, b"{\n}\n");
- }
-
- *self.json.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(JsonFile::new(
- file.clone(),
- None,
- None,
- )?)));
- *self.lock.borrow_mut() = Factory::get_lock_file(&file);
- let json = self.json.borrow().as_ref().unwrap().clone();
- *self.composer_backup.borrow_mut() =
- file_get_contents(json.borrow().get_path()).unwrap_or_default();
- let lock = self.lock.borrow().clone();
- *self.lock_backup.borrow_mut() = if file_exists(&lock) {
- file_get_contents(&lock)
- } else {
- None
- };
-
- // PHP: function ($signal, $handler) use ($io, $self) {
- // $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
- // $self->revertComposerFile(); $handler->exitWithLastSignal(); }
- // TODO(phase-c): SignalHandler::create takes a `Box<dyn Fn> + 'static` handler that cannot
- // borrow &self, but the body must call self.revert_composer_file() (which mutates the
- // command's composer.json backup state) and self.get_io(). Faithfully wiring this needs the
- // revert state + io shared into the closure (Rc<RefCell<...>>), i.e. the shared-ownership
- // rework of the command — the same pattern as InstallationManager::execute's signal handler.
- let signal_handler = SignalHandler::create(
- vec![
- SignalHandler::SIGINT.to_string(),
- SignalHandler::SIGTERM.to_string(),
- SignalHandler::SIGHUP.to_string(),
- ],
- Box::new(move |signal: String, handler: &SignalHandler| {
- let _ = signal;
- handler.exit_with_last_signal();
- }),
- );
-
- // check for writability by writing to the file as is_writable can not be trusted on network-mounts
- // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
- let file_path = file.clone();
- let backup_contents = self.composer_backup.borrow().clone();
- if !is_writable(&file)
- && Silencer::call(|| {
- shirabe_php_shim::file_put_contents(&file_path, backup_contents.as_bytes());
- Ok::<bool, anyhow::Error>(false)
- })
- .ok()
- == Some(false)
- {
- let msg = format!("<error>{} is not writable.</error>", file);
- self.get_io().write_error3(&msg, true, io_interface::NORMAL);
-
- return Ok(1);
- }
-
- if input.borrow().get_option("fixed")?.as_bool() == Some(true) {
- let config = json.borrow_mut().read()?;
-
- let package_type = if empty(&config.get("type").cloned().unwrap_or(PhpMixed::Null)) {
- "library".to_string()
- } else {
- config
- .get("type")
- .and_then(|v| v.as_string())
- .unwrap_or("")
- .to_string()
- };
-
- // @see https://github.com/composer/composer/pull/8313#issuecomment-532637955
- if package_type != "project"
- && !input.borrow().get_option("dev")?.as_bool().unwrap_or(false)
- {
- self.get_io().write_error3("<error>The \"--fixed\" option is only allowed for packages with a \"project\" type or for dev dependencies to prevent possible misuses.</error>", true, io_interface::NORMAL);
-
- if config.get("type").is_none() {
- self.get_io().write_error3("<error>If your package is not a library, you can explicitly specify the \"type\" by using \"composer config type project\".</error>", true, io_interface::NORMAL);
- }
-
- return Ok(1);
- }
- }
-
- let composer = self.require_composer(None, None)?;
- let composer = crate::composer::composer_full(&composer);
- let repository_manager = composer.get_repository_manager().clone();
- let repository_manager = repository_manager.borrow();
- let repos = repository_manager.get_repositories();
-
- let platform_overrides = composer.get_config().borrow_mut().get("platform");
- let platform_overrides_map: IndexMap<String, PhpMixed> = platform_overrides
- .as_array()
- .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
- .unwrap_or_default();
- // initialize self.repos as it is used by the PackageDiscoveryTrait
- let platform_repo =
- PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides_map)?);
- let mut combined: Vec<crate::repository::RepositoryInterfaceHandle> =
- vec![platform_repo.clone().into()];
- for repo in repos {
- combined.push(repo.clone());
- }
- *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new(
- CompositeRepository::new(combined),
- ));
-
- let preferred_stability = if composer.get_package().get_prefer_stable() {
- "stable".to_string()
- } else {
- composer.get_package().get_minimum_stability()
- };
-
- // Hoist argument computations into locals so no borrow of `input` is held across the
- // call: `determine_requirements` may prompt via ConsoleIO, which mutably borrows the
- // same input RefCell.
- let packages: Vec<String> = input
- .borrow()
- .get_argument("packages")?
- .as_list()
- .map(|l| {
- l.iter()
- .filter_map(|v| v.as_string().map(|s| s.to_string()))
- .collect()
- })
- .unwrap_or_default();
- // if there is no update, we need to use the best possible version constraint directly as we cannot rely on the solver to guess the best constraint
- let no_update = input
- .borrow()
- .get_option("no-update")?
- .as_bool()
- .unwrap_or(false);
- let fixed = input
- .borrow()
- .get_option("fixed")?
- .as_bool()
- .unwrap_or(false);
- let requirements_result = self.determine_requirements(
- input.clone(),
- output.clone(),
- packages,
- Some(&platform_repo),
- &preferred_stability,
- no_update,
- fixed,
- );
-
- let requirements = match requirements_result {
- Ok(r) => r,
- Err(e) => {
- if self.newly_created.get() {
- self.revert_composer_file();
-
- return Err(RuntimeException {
- message: format!(
- "No composer.json present in the current directory ({}), this may be the cause of the following exception.",
- self.file.borrow()
- ),
- code: 0,
- }
- .into());
- }
-
- return Err(e);
- }
- };
-
- let mut requirements = self.format_requirements(requirements)?;
-
- if !input.borrow().get_option("dev")?.as_bool().unwrap_or(false)
- && self.get_io().is_interactive()
- && !composer.is_global()
- {
- let mut dev_packages: Vec<Vec<String>> = vec![];
- let dev_tags: Vec<String> = vec![
- "dev".to_string(),
- "testing".to_string(),
- "static analysis".to_string(),
- ];
- let current_requires_by_key = self.get_packages_by_require_key();
- for (name, _version) in &requirements {
- // skip packages which are already in the composer.json as those have already been decided
- if current_requires_by_key.contains_key(name) {
- continue;
- }
-
- let found_packages: Vec<crate::package::PackageInterfaceHandle> = self
- .get_repos()
- .find_packages(name, None)?
- .into_iter()
- .collect();
- let pkg: Option<crate::package::PackageInterfaceHandle> =
- PackageSorter::get_most_current_version(found_packages);
- let pkg_as_complete: Option<crate::package::CompletePackageInterfaceHandle> =
- pkg.as_ref().and_then(|p| p.as_complete());
- if let Some(pkg_complete) = pkg_as_complete {
- let lowered: Vec<String> =
- array_map(|s: &String| strtolower(s), &pkg_complete.get_keywords());
- let pkg_dev_tags: Vec<String> = array_intersect(&dev_tags, &lowered);
- if (pkg_dev_tags.len() as i64) > 0 {
- dev_packages.push(pkg_dev_tags);
- }
- }
- let _ = pkg;
- }
-
- if (dev_packages.len() as i64) == (requirements.len() as i64) {
- let plural = if (requirements.len() as i64) > 1 {
- "s"
- } else {
- ""
- };
- let plural2 = if (requirements.len() as i64) > 1 {
- "are"
- } else {
- "is"
- };
- let plural3 = if (requirements.len() as i64) > 1 {
- "they are"
- } else {
- "it is"
- };
- let merged: Vec<String> = dev_packages.iter().flatten().cloned().collect();
- let pkg_dev_tags: Vec<String> = array_unique(&merged);
- let warn_msg = format!(
- "The package{} you required {} recommended to be placed in require-dev (because {} tagged as \"{}\") but you did not use --dev.",
- plural,
- plural2,
- plural3,
- implode("\", \"", &pkg_dev_tags),
- );
- self.get_io().warning(&warn_msg, &[]);
- if self.get_io().ask_confirmation(
- "<info>Do you want to re-run the command with --dev?</> [<comment>yes</>]? "
- .to_string(),
- true,
- ) {
- input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?;
- }
- }
-
- // unset($devPackages, $pkgDevTags);
- }
-
- let mut require_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
- "require-dev"
- } else {
- "require"
- };
- let mut remove_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
- "require"
- } else {
- "require-dev"
- };
-
- // check which requirements need the version guessed
- let mut requirements_to_guess: Vec<String> = vec![];
- for (package, constraint) in requirements.clone().iter() {
- if constraint == "guess" {
- requirements.insert(package.clone(), "*".to_string());
- requirements_to_guess.push(package.clone());
- }
- }
-
- // validate requirements format
- let version_parser = VersionParser::new();
- for (package, constraint) in &requirements {
- if strtolower(package) == composer.get_package().get_name() {
- let msg = format!(
- "<error>Root package '{}' cannot require itself in its composer.json</error>",
- package.clone(),
- );
- self.get_io().write_error3(&msg, true, io_interface::NORMAL);
-
- return Ok(1);
- }
- if constraint == "self.version" {
- continue;
- }
- version_parser.parse_constraints(constraint)?;
- }
-
- let inconsistent_require_keys =
- self.get_inconsistent_require_keys(&requirements, require_key);
- if (inconsistent_require_keys.len() as i64) > 0 {
- for package in &inconsistent_require_keys {
- let warn_msg = format!(
- "{} is currently present in the {} key and you ran the command {} the --dev flag, which will move it to the {} key.",
- package.clone(),
- remove_key,
- if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
- "with"
- } else {
- "without"
- },
- require_key,
- );
- self.get_io().warning(&warn_msg, &[]);
- }
-
- if self.get_io().is_interactive() {
- let q1 = format!(
- "<info>Do you want to move {}?</info> [<comment>no</comment>]? ",
- if (inconsistent_require_keys.len() as i64) > 1 {
- "these requirements"
- } else {
- "this requirement"
- },
- );
- if !self.get_io().ask_confirmation(q1, false) {
- let q2 = format!(
- "<info>Do you want to re-run the command {} --dev?</info> [<comment>yes</comment>]? ",
- if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
- "without"
- } else {
- "with"
- },
- );
- if !self.get_io().ask_confirmation(q2, true) {
- return Ok(0);
- }
-
- input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?;
- std::mem::swap(&mut require_key, &mut remove_key);
- }
- }
- }
-
- let sort_packages = input
- .borrow()
- .get_option("sort-packages")?
- .as_bool()
- .unwrap_or(false)
- || composer
- .get_config()
- .borrow()
- .get("sort-packages")
- .as_bool()
- .unwrap_or(false);
-
- self.first_require.set(self.newly_created.get());
- if !self.first_require.get() {
- let composer_definition = json.borrow_mut().read()?;
- let require_count = composer_definition
- .get("require")
- .and_then(|v| v.as_array())
- .map(|m| m.len() as i64)
- .unwrap_or(0);
- let require_dev_count = composer_definition
- .get("require-dev")
- .and_then(|v| v.as_array())
- .map(|m| m.len() as i64)
- .unwrap_or(0);
- if require_count == 0 && require_dev_count == 0 {
- self.first_require.set(true);
- }
- }
-
- if !input
- .borrow()
- .get_option("dry-run")?
- .as_bool()
- .unwrap_or(false)
- {
- self.update_file(&json, &requirements, require_key, remove_key, sort_packages);
- }
-
- let updated_msg = format!(
- "<info>{} has been {}</info>",
- file,
- if self.newly_created.get() {
- "created"
- } else {
- "updated"
- }
- );
- self.get_io()
- .write_error3(&updated_msg, true, io_interface::NORMAL);
-
- if input
- .borrow()
- .get_option("no-update")?
- .as_bool()
- .unwrap_or(false)
- {
- return Ok(0);
- }
-
- composer
- .get_plugin_manager()
- .borrow_mut()
- .deactivate_installed_plugins()?;
-
- let io = self.get_io().clone();
- let do_update_result = self.do_update(
- input.clone(),
- output,
- io,
- &requirements,
- require_key,
- remove_key,
- );
- let dry_run = input
- .borrow()
- .get_option("dry-run")?
- .as_bool()
- .unwrap_or(false);
-
- let result = match do_update_result {
- Ok(result) => {
- let final_result = if result == 0 && (requirements_to_guess.len() as i64) > 0 {
- let fixed = input
- .borrow()
- .get_option("fixed")?
- .as_bool()
- .unwrap_or(false);
- self.update_requirements_after_resolution(
- &requirements_to_guess,
- require_key,
- remove_key,
- sort_packages,
- dry_run,
- fixed,
- )?
- } else {
- result
- };
- Ok(final_result)
- }
- Err(e) => {
- if !self.dependency_resolution_completed.get() {
- self.revert_composer_file();
- }
- Err(e)
- }
- };
-
- // finally
- if dry_run && self.newly_created.get() {
- // @unlink($this->json->getPath());
- unlink(json.borrow().get_path());
- }
- signal_handler.unregister();
-
- result
- }
-
- fn interact(
- &self,
- _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) {
- }
-
- fn initialize(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<()> {
- base_command_initialize(self, input, output)
- }
-
- fn complete(
- &self,
- input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
- suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
- ) -> anyhow::Result<()> {
- crate::command::base_command::base_command_complete(self, input, suggestions)
- }
-
- shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
-}
-
-impl BaseCommand for RequireCommand {
- fn base_command_data(&self) -> &crate::command::BaseCommandData {
- &self.base_command_data
- }
-
- crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
-}
-impl RequireCommand {
fn get_inconsistent_require_keys(
&self,
new_requirements: &IndexMap<String, String>,
@@ -1302,3 +719,584 @@ impl RequireCommand {
}
}
}
+
+impl PackageDiscoveryTrait for RequireCommand {
+ fn get_repos_mut(
+ &self,
+ ) -> std::cell::RefMut<'_, Option<crate::repository::RepositoryInterfaceHandle>> {
+ self.repos.borrow_mut()
+ }
+
+ fn get_repository_sets_mut(
+ &self,
+ ) -> std::cell::RefMut<'_, IndexMap<String, std::rc::Rc<std::cell::RefCell<RepositorySet>>>>
+ {
+ self.repository_sets.borrow_mut()
+ }
+}
+
+impl Command for RequireCommand {
+ fn configure(&self) -> anyhow::Result<()> {
+ self.set_name("require")?;
+ self.set_aliases(vec!["r".to_string()])?;
+ self.set_description("Adds required packages to your composer.json and installs them");
+ self.set_definition(&[
+ InputArgument::new5("packages", Some(InputArgument::IS_ARRAY | InputArgument::OPTIONAL), "Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or \"foo/bar 1.0.0\"", None, self.suggest_available_package_incl_platform()).unwrap().into(),
+ InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Add requirement to require-dev.", None).unwrap().into(),
+ InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(),
+ InputOption::new("prefer-source", None, Some(InputOption::VALUE_NONE), "Forces installation from package sources when possible, including VCS information.", None).unwrap().into(),
+ InputOption::new("prefer-dist", None, Some(InputOption::VALUE_NONE), "Forces installation from package dist (default behavior).", None).unwrap().into(),
+ InputOption::new6("prefer-install", None, Some(InputOption::VALUE_REQUIRED), "Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).", None, self.suggest_prefer_install()).unwrap().into(),
+ InputOption::new("fixed", None, Some(InputOption::VALUE_NONE), "Write fixed version to the composer.json.", None).unwrap().into(),
+ InputOption::new("no-suggest", None, Some(InputOption::VALUE_NONE), "DEPRECATED: This flag does not exist anymore.", None).unwrap().into(),
+ InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(),
+ InputOption::new("no-update", None, Some(InputOption::VALUE_NONE), "Disables the automatic update of the dependencies (implies --no-install).", None).unwrap().into(),
+ InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Skip the install step after updating the composer.lock file.", None).unwrap().into(),
+ InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Skip the audit step after updating the composer.lock file (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(),
+ InputOption::new6("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string())), SuggestedValues::List(Auditor::FORMATS.iter().map(|s| s.to_string()).collect())).unwrap().into(),
+ InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(),
+ InputOption::new("update-no-dev", None, Some(InputOption::VALUE_NONE), "Run the dependency update with the --no-dev option.", None).unwrap().into(),
+ InputOption::new("update-with-dependencies", Some(PhpMixed::String("w".to_string())), Some(InputOption::VALUE_NONE), "Allows inherited dependencies to be updated, except those that are root requirements (can also be set via the COMPOSER_WITH_DEPENDENCIES=1 env var).", None).unwrap().into(),
+ InputOption::new("update-with-all-dependencies", Some(PhpMixed::String("W".to_string())), Some(InputOption::VALUE_NONE), "Allows all inherited dependencies to be updated, including those that are root requirements (can also be set via the COMPOSER_WITH_ALL_DEPENDENCIES=1 env var).", None).unwrap().into(),
+ InputOption::new("with-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-dependencies", None).unwrap().into(),
+ InputOption::new("with-all-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-all-dependencies", None).unwrap().into(),
+ InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(),
+ InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(),
+ InputOption::new("prefer-stable", None, Some(InputOption::VALUE_NONE), "Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).", None).unwrap().into(),
+ InputOption::new("prefer-lowest", None, Some(InputOption::VALUE_NONE), "Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).", None).unwrap().into(),
+ InputOption::new("minimal-changes", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "During an update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).", None).unwrap().into(),
+ InputOption::new("sort-packages", None, Some(InputOption::VALUE_NONE), "Sorts packages when adding/updating a new dependency", None).unwrap().into(),
+ InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(),
+ InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(),
+ InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(),
+ InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(),
+ ]);
+ self.set_help(
+ "The require command adds required packages to your composer.json and installs them.\n\
+ \n\
+ If you do not specify a package, composer will prompt you to search for a package, and given results, provide a list of\n\
+ matches to require.\n\
+ \n\
+ If you do not specify a version constraint, composer will choose a suitable one based on the available package versions.\n\
+ \n\
+ If you do not want to install the new dependencies immediately you can call it with --no-update\n\
+ \n\
+ Read more at https://getcomposer.org/doc/03-cli.md#require-r"
+ );
+ Ok(())
+ }
+
+ /// @throws \Seld\JsonLint\ParsingException
+ fn execute(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ *self.file.borrow_mut() = Factory::get_composer_file()?;
+
+ if input
+ .borrow()
+ .get_option("no-suggest")?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ self.get_io().write_error3("<warning>You are using the deprecated option \"--no-suggest\". It has no effect and will break in Composer 3.</warning>", true, io_interface::NORMAL);
+ }
+
+ let file = self.file.borrow().clone();
+ self.newly_created.set(!file_exists(&file));
+ let write_failed =
+ self.newly_created.get() && file_put_contents(&file, b"{\n}\n").is_none();
+ if write_failed {
+ let msg = format!("<error>{} could not be created.</error>", file);
+ self.get_io().write_error3(&msg, true, io_interface::NORMAL);
+
+ return Ok(1);
+ }
+ if !Filesystem::is_readable(&file) {
+ let msg = format!("<error>{} is not readable.</error>", file);
+ self.get_io().write_error3(&msg, true, io_interface::NORMAL);
+
+ return Ok(1);
+ }
+ if filesize(&file) == Some(0) {
+ file_put_contents(&file, b"{\n}\n");
+ }
+
+ *self.json.borrow_mut() = Some(std::rc::Rc::new(std::cell::RefCell::new(JsonFile::new(
+ file.clone(),
+ None,
+ None,
+ )?)));
+ *self.lock.borrow_mut() = Factory::get_lock_file(&file);
+ let json = self.json.borrow().as_ref().unwrap().clone();
+ *self.composer_backup.borrow_mut() =
+ file_get_contents(json.borrow().get_path()).unwrap_or_default();
+ let lock = self.lock.borrow().clone();
+ *self.lock_backup.borrow_mut() = if file_exists(&lock) {
+ file_get_contents(&lock)
+ } else {
+ None
+ };
+
+ // PHP: function ($signal, $handler) use ($io, $self) {
+ // $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
+ // $self->revertComposerFile(); $handler->exitWithLastSignal(); }
+ // TODO(phase-c): SignalHandler::create takes a `Box<dyn Fn> + 'static` handler that cannot
+ // borrow &self, but the body must call self.revert_composer_file() (which mutates the
+ // command's composer.json backup state) and self.get_io(). Faithfully wiring this needs the
+ // revert state + io shared into the closure (Rc<RefCell<...>>), i.e. the shared-ownership
+ // rework of the command — the same pattern as InstallationManager::execute's signal handler.
+ let signal_handler = SignalHandler::create(
+ vec![
+ SignalHandler::SIGINT.to_string(),
+ SignalHandler::SIGTERM.to_string(),
+ SignalHandler::SIGHUP.to_string(),
+ ],
+ Box::new(move |signal: String, handler: &SignalHandler| {
+ let _ = signal;
+ handler.exit_with_last_signal();
+ }),
+ );
+
+ // check for writability by writing to the file as is_writable can not be trusted on network-mounts
+ // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
+ let file_path = file.clone();
+ let backup_contents = self.composer_backup.borrow().clone();
+ if !is_writable(&file)
+ && Silencer::call(|| {
+ shirabe_php_shim::file_put_contents(&file_path, backup_contents.as_bytes());
+ Ok::<bool, anyhow::Error>(false)
+ })
+ .ok()
+ == Some(false)
+ {
+ let msg = format!("<error>{} is not writable.</error>", file);
+ self.get_io().write_error3(&msg, true, io_interface::NORMAL);
+
+ return Ok(1);
+ }
+
+ if input.borrow().get_option("fixed")?.as_bool() == Some(true) {
+ let config = json.borrow_mut().read()?;
+
+ let package_type = if empty(&config.get("type").cloned().unwrap_or(PhpMixed::Null)) {
+ "library".to_string()
+ } else {
+ config
+ .get("type")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string()
+ };
+
+ // @see https://github.com/composer/composer/pull/8313#issuecomment-532637955
+ if package_type != "project"
+ && !input.borrow().get_option("dev")?.as_bool().unwrap_or(false)
+ {
+ self.get_io().write_error3("<error>The \"--fixed\" option is only allowed for packages with a \"project\" type or for dev dependencies to prevent possible misuses.</error>", true, io_interface::NORMAL);
+
+ if config.get("type").is_none() {
+ self.get_io().write_error3("<error>If your package is not a library, you can explicitly specify the \"type\" by using \"composer config type project\".</error>", true, io_interface::NORMAL);
+ }
+
+ return Ok(1);
+ }
+ }
+
+ let composer = self.require_composer(None, None)?;
+ let composer = crate::composer::composer_full(&composer);
+ let repository_manager = composer.get_repository_manager().clone();
+ let repository_manager = repository_manager.borrow();
+ let repos = repository_manager.get_repositories();
+
+ let platform_overrides = composer.get_config().borrow_mut().get("platform");
+ let platform_overrides_map: IndexMap<String, PhpMixed> = platform_overrides
+ .as_array()
+ .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
+ .unwrap_or_default();
+ // initialize self.repos as it is used by the PackageDiscoveryTrait
+ let platform_repo =
+ PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides_map)?);
+ let mut combined: Vec<crate::repository::RepositoryInterfaceHandle> =
+ vec![platform_repo.clone().into()];
+ for repo in repos {
+ combined.push(repo.clone());
+ }
+ *self.get_repos_mut() = Some(crate::repository::RepositoryInterfaceHandle::new(
+ CompositeRepository::new(combined),
+ ));
+
+ let preferred_stability = if composer.get_package().get_prefer_stable() {
+ "stable".to_string()
+ } else {
+ composer.get_package().get_minimum_stability()
+ };
+
+ // Hoist argument computations into locals so no borrow of `input` is held across the
+ // call: `determine_requirements` may prompt via ConsoleIO, which mutably borrows the
+ // same input RefCell.
+ let packages: Vec<String> = input
+ .borrow()
+ .get_argument("packages")?
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default();
+ // if there is no update, we need to use the best possible version constraint directly as we cannot rely on the solver to guess the best constraint
+ let no_update = input
+ .borrow()
+ .get_option("no-update")?
+ .as_bool()
+ .unwrap_or(false);
+ let fixed = input
+ .borrow()
+ .get_option("fixed")?
+ .as_bool()
+ .unwrap_or(false);
+ let requirements_result = self.determine_requirements(
+ input.clone(),
+ output.clone(),
+ packages,
+ Some(&platform_repo),
+ &preferred_stability,
+ no_update,
+ fixed,
+ );
+
+ let requirements = match requirements_result {
+ Ok(r) => r,
+ Err(e) => {
+ if self.newly_created.get() {
+ self.revert_composer_file();
+
+ return Err(RuntimeException {
+ message: format!(
+ "No composer.json present in the current directory ({}), this may be the cause of the following exception.",
+ self.file.borrow()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ return Err(e);
+ }
+ };
+
+ let mut requirements = self.format_requirements(requirements)?;
+
+ if !input.borrow().get_option("dev")?.as_bool().unwrap_or(false)
+ && self.get_io().is_interactive()
+ && !composer.is_global()
+ {
+ let mut dev_packages: Vec<Vec<String>> = vec![];
+ let dev_tags: Vec<String> = vec![
+ "dev".to_string(),
+ "testing".to_string(),
+ "static analysis".to_string(),
+ ];
+ let current_requires_by_key = self.get_packages_by_require_key();
+ for (name, _version) in &requirements {
+ // skip packages which are already in the composer.json as those have already been decided
+ if current_requires_by_key.contains_key(name) {
+ continue;
+ }
+
+ let found_packages: Vec<crate::package::PackageInterfaceHandle> = self
+ .get_repos()
+ .find_packages(name, None)?
+ .into_iter()
+ .collect();
+ let pkg: Option<crate::package::PackageInterfaceHandle> =
+ PackageSorter::get_most_current_version(found_packages);
+ let pkg_as_complete: Option<crate::package::CompletePackageInterfaceHandle> =
+ pkg.as_ref().and_then(|p| p.as_complete());
+ if let Some(pkg_complete) = pkg_as_complete {
+ let lowered: Vec<String> =
+ array_map(|s: &String| strtolower(s), &pkg_complete.get_keywords());
+ let pkg_dev_tags: Vec<String> = array_intersect(&dev_tags, &lowered);
+ if (pkg_dev_tags.len() as i64) > 0 {
+ dev_packages.push(pkg_dev_tags);
+ }
+ }
+ let _ = pkg;
+ }
+
+ if (dev_packages.len() as i64) == (requirements.len() as i64) {
+ let plural = if (requirements.len() as i64) > 1 {
+ "s"
+ } else {
+ ""
+ };
+ let plural2 = if (requirements.len() as i64) > 1 {
+ "are"
+ } else {
+ "is"
+ };
+ let plural3 = if (requirements.len() as i64) > 1 {
+ "they are"
+ } else {
+ "it is"
+ };
+ let merged: Vec<String> = dev_packages.iter().flatten().cloned().collect();
+ let pkg_dev_tags: Vec<String> = array_unique(&merged);
+ let warn_msg = format!(
+ "The package{} you required {} recommended to be placed in require-dev (because {} tagged as \"{}\") but you did not use --dev.",
+ plural,
+ plural2,
+ plural3,
+ implode("\", \"", &pkg_dev_tags),
+ );
+ self.get_io().warning(&warn_msg, &[]);
+ if self.get_io().ask_confirmation(
+ "<info>Do you want to re-run the command with --dev?</> [<comment>yes</>]? "
+ .to_string(),
+ true,
+ ) {
+ input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?;
+ }
+ }
+
+ // unset($devPackages, $pkgDevTags);
+ }
+
+ let mut require_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
+ "require-dev"
+ } else {
+ "require"
+ };
+ let mut remove_key = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
+ "require"
+ } else {
+ "require-dev"
+ };
+
+ // check which requirements need the version guessed
+ let mut requirements_to_guess: Vec<String> = vec![];
+ for (package, constraint) in requirements.clone().iter() {
+ if constraint == "guess" {
+ requirements.insert(package.clone(), "*".to_string());
+ requirements_to_guess.push(package.clone());
+ }
+ }
+
+ // validate requirements format
+ let version_parser = VersionParser::new();
+ for (package, constraint) in &requirements {
+ if strtolower(package) == composer.get_package().get_name() {
+ let msg = format!(
+ "<error>Root package '{}' cannot require itself in its composer.json</error>",
+ package.clone(),
+ );
+ self.get_io().write_error3(&msg, true, io_interface::NORMAL);
+
+ return Ok(1);
+ }
+ if constraint == "self.version" {
+ continue;
+ }
+ version_parser.parse_constraints(constraint)?;
+ }
+
+ let inconsistent_require_keys =
+ self.get_inconsistent_require_keys(&requirements, require_key);
+ if (inconsistent_require_keys.len() as i64) > 0 {
+ for package in &inconsistent_require_keys {
+ let warn_msg = format!(
+ "{} is currently present in the {} key and you ran the command {} the --dev flag, which will move it to the {} key.",
+ package.clone(),
+ remove_key,
+ if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
+ "with"
+ } else {
+ "without"
+ },
+ require_key,
+ );
+ self.get_io().warning(&warn_msg, &[]);
+ }
+
+ if self.get_io().is_interactive() {
+ let q1 = format!(
+ "<info>Do you want to move {}?</info> [<comment>no</comment>]? ",
+ if (inconsistent_require_keys.len() as i64) > 1 {
+ "these requirements"
+ } else {
+ "this requirement"
+ },
+ );
+ if !self.get_io().ask_confirmation(q1, false) {
+ let q2 = format!(
+ "<info>Do you want to re-run the command {} --dev?</info> [<comment>yes</comment>]? ",
+ if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) {
+ "without"
+ } else {
+ "with"
+ },
+ );
+ if !self.get_io().ask_confirmation(q2, true) {
+ return Ok(0);
+ }
+
+ input.borrow_mut().set_option("dev", PhpMixed::Bool(true))?;
+ std::mem::swap(&mut require_key, &mut remove_key);
+ }
+ }
+ }
+
+ let sort_packages = input
+ .borrow()
+ .get_option("sort-packages")?
+ .as_bool()
+ .unwrap_or(false)
+ || composer
+ .get_config()
+ .borrow()
+ .get("sort-packages")
+ .as_bool()
+ .unwrap_or(false);
+
+ self.first_require.set(self.newly_created.get());
+ if !self.first_require.get() {
+ let composer_definition = json.borrow_mut().read()?;
+ let require_count = composer_definition
+ .get("require")
+ .and_then(|v| v.as_array())
+ .map(|m| m.len() as i64)
+ .unwrap_or(0);
+ let require_dev_count = composer_definition
+ .get("require-dev")
+ .and_then(|v| v.as_array())
+ .map(|m| m.len() as i64)
+ .unwrap_or(0);
+ if require_count == 0 && require_dev_count == 0 {
+ self.first_require.set(true);
+ }
+ }
+
+ if !input
+ .borrow()
+ .get_option("dry-run")?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ self.update_file(&json, &requirements, require_key, remove_key, sort_packages);
+ }
+
+ let updated_msg = format!(
+ "<info>{} has been {}</info>",
+ file,
+ if self.newly_created.get() {
+ "created"
+ } else {
+ "updated"
+ }
+ );
+ self.get_io()
+ .write_error3(&updated_msg, true, io_interface::NORMAL);
+
+ if input
+ .borrow()
+ .get_option("no-update")?
+ .as_bool()
+ .unwrap_or(false)
+ {
+ return Ok(0);
+ }
+
+ composer
+ .get_plugin_manager()
+ .borrow_mut()
+ .deactivate_installed_plugins()?;
+
+ let io = self.get_io().clone();
+ let do_update_result = self.do_update(
+ input.clone(),
+ output,
+ io,
+ &requirements,
+ require_key,
+ remove_key,
+ );
+ let dry_run = input
+ .borrow()
+ .get_option("dry-run")?
+ .as_bool()
+ .unwrap_or(false);
+
+ let result = match do_update_result {
+ Ok(result) => {
+ let final_result = if result == 0 && (requirements_to_guess.len() as i64) > 0 {
+ let fixed = input
+ .borrow()
+ .get_option("fixed")?
+ .as_bool()
+ .unwrap_or(false);
+ self.update_requirements_after_resolution(
+ &requirements_to_guess,
+ require_key,
+ remove_key,
+ sort_packages,
+ dry_run,
+ fixed,
+ )?
+ } else {
+ result
+ };
+ Ok(final_result)
+ }
+ Err(e) => {
+ if !self.dependency_resolution_completed.get() {
+ self.revert_composer_file();
+ }
+ Err(e)
+ }
+ };
+
+ // finally
+ if dry_run && self.newly_created.get() {
+ // @unlink($this->json->getPath());
+ unlink(json.borrow().get_path());
+ }
+ signal_handler.unregister();
+
+ result
+ }
+
+ fn interact(
+ &self,
+ _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) {
+ }
+
+ fn initialize(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<()> {
+ base_command_initialize(self, input, output)
+ }
+
+ fn complete(
+ &self,
+ input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
+ suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ crate::command::base_command::base_command_complete(self, input, suggestions)
+ }
+
+ shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl BaseCommand for RequireCommand {
+ fn base_command_data(&self) -> &crate::command::BaseCommandData {
+ &self.base_command_data
+ }
+
+ crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
+}
diff --git a/crates/shirabe/src/command/show_command.rs b/crates/shirabe/src/command/show_command.rs
index fb663d86..44733531 100644
--- a/crates/shirabe/src/command/show_command.rs
+++ b/crates/shirabe/src/command/show_command.rs
@@ -81,1382 +81,7 @@ impl ShowCommand {
.expect("ShowCommand::configure uses static, valid metadata");
command
}
-}
-
-impl Command for ShowCommand {
- fn configure(&self) -> anyhow::Result<()> {
- self.set_name("show")?;
- self.set_aliases(vec!["info".to_string()])?;
- self.set_description("Shows information about packages");
- let opt_none = |name: &str, shortcut: Option<&str>, description: &str| {
- InputOption::new(
- name,
- shortcut.map(|s| PhpMixed::String(s.to_string())),
- Some(InputOption::VALUE_NONE),
- description,
- None,
- )
- .unwrap()
- .into()
- };
- self.set_definition(&[
- InputArgument::new5(
- "package",
- Some(InputArgument::OPTIONAL),
- "Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.",
- None,
- self.suggest_package_based_on_mode(),
- )
- .unwrap()
- .into(),
- InputArgument::new(
- "version",
- Some(InputArgument::OPTIONAL),
- "Version or version constraint to inspect",
- None,
- )
- .unwrap()
- .into(),
- opt_none("all", None, "List all packages"),
- opt_none("locked", None, "List all locked packages"),
- opt_none(
- "installed",
- Some("i"),
- "List installed packages only (enabled by default, only present for BC).",
- ),
- opt_none("platform", Some("p"), "List platform packages only"),
- opt_none("available", Some("a"), "List available packages only"),
- opt_none("self", Some("s"), "Show the root package information"),
- opt_none("name-only", Some("N"), "List package names only"),
- opt_none("path", Some("P"), "Show package paths"),
- opt_none("tree", Some("t"), "List the dependencies as a tree"),
- opt_none("latest", Some("l"), "Show the latest version"),
- opt_none(
- "outdated",
- Some("o"),
- "Show the latest version but only for packages that are outdated",
- ),
- InputOption::new6(
- "ignore",
- None,
- Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY),
- "Ignore specified package(s). Can contain wildcards (*). Use it with the --outdated option if you don't want to be informed about new versions of some packages.",
- None,
- self.suggest_installed_package(false, false),
- )
- .unwrap()
- .into(),
- opt_none(
- "major-only",
- Some("M"),
- "Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.",
- ),
- opt_none(
- "minor-only",
- Some("m"),
- "Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.",
- ),
- opt_none(
- "patch-only",
- None,
- "Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.",
- ),
- opt_none(
- "sort-by-age",
- Some("A"),
- "Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option.",
- ),
- opt_none(
- "direct",
- Some("D"),
- "Shows only packages that are directly required by the root package",
- ),
- opt_none(
- "strict",
- None,
- "Return a non-zero exit code when there are outdated packages",
- ),
- InputOption::new6(
- "format",
- Some(PhpMixed::String("f".to_string())),
- Some(InputOption::VALUE_REQUIRED),
- "Format of the output: text or json",
- Some(PhpMixed::String("text".to_string())),
- SuggestedValues::List(vec!["json".to_string(), "text".to_string()]),
- )
- .unwrap()
- .into(),
- opt_none("no-dev", None, "Disables search in require-dev packages."),
- InputOption::new(
- "ignore-platform-req",
- None,
- Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY),
- "Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option",
- None,
- )
- .unwrap()
- .into(),
- opt_none(
- "ignore-platform-reqs",
- None,
- "Ignore all platform requirements (php & ext- packages). Use with the --outdated option",
- ),
- ]);
- self.set_help(
- "The show command displays detailed information about a package, or\n\
- lists all packages available.\n\n\
- Read more at https://getcomposer.org/doc/03-cli.md#show-info",
- );
- Ok(())
- }
-
- fn execute(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i64> {
- *self.version_parser.borrow_mut() = VersionParser::new();
- if input.borrow().get_option("tree")?.as_bool() == Some(true) {
- self.init_styles(output.clone());
- }
-
- let composer = self.try_composer(None, None);
-
- if input.borrow().get_option("installed")?.as_bool() == Some(true)
- && input.borrow().get_option("self")?.as_bool() != Some(true)
- {
- self.get_io().write_error("<warning>You are using the deprecated option \"installed\". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>");
- }
-
- if input.borrow().get_option("outdated")?.as_bool() == Some(true) {
- input
- .borrow_mut()
- .set_option("latest", PhpMixed::Bool(true));
- } else if input
- .borrow()
- .get_option("ignore")?
- .as_list()
- .map_or(0, |l| l.len())
- > 0
- {
- self.get_io().write_error("<warning>You are using the option \"ignore\" for action other than \"outdated\", it will be ignored.</warning>");
- }
-
- if input.borrow().get_option("direct")?.as_bool() == Some(true)
- && (input.borrow().get_option("all")?.as_bool() == Some(true)
- || input.borrow().get_option("available")?.as_bool() == Some(true)
- || input.borrow().get_option("platform")?.as_bool() == Some(true))
- {
- self.get_io().write_error("The --direct (-D) option is not usable in combination with --all, --platform (-p) or --available (-a)");
-
- return Ok(1);
- }
-
- if input.borrow().get_option("tree")?.as_bool() == Some(true)
- && (input.borrow().get_option("all")?.as_bool() == Some(true)
- || input.borrow().get_option("available")?.as_bool() == Some(true))
- {
- self.get_io().write_error("The --tree (-t) option is not usable in combination with --all or --available (-a)");
-
- return Ok(1);
- }
-
- let only_count: usize = [
- input.borrow().get_option("patch-only")?.as_bool() == Some(true),
- input.borrow().get_option("minor-only")?.as_bool() == Some(true),
- input.borrow().get_option("major-only")?.as_bool() == Some(true),
- ]
- .iter()
- .filter(|b| **b)
- .count();
- if only_count > 1 {
- self.get_io().write_error(
- "Only one of --major-only, --minor-only or --patch-only can be used at once",
- );
-
- return Ok(1);
- }
-
- if input.borrow().get_option("tree")?.as_bool() == Some(true)
- && input.borrow().get_option("latest")?.as_bool() == Some(true)
- {
- self.get_io().write_error(
- "The --tree (-t) option is not usable in combination with --latest (-l)",
- );
-
- return Ok(1);
- }
-
- if input.borrow().get_option("tree")?.as_bool() == Some(true)
- && input.borrow().get_option("path")?.as_bool() == Some(true)
- {
- self.get_io().write_error(
- "The --tree (-t) option is not usable in combination with --path (-P)",
- );
-
- return Ok(1);
- }
-
- let format = input
- .borrow()
- .get_option("format")?
- .as_string()
- .unwrap_or("text")
- .to_string();
- if !in_array_loose(
- format.clone(),
- &[
- PhpMixed::String("text".to_string()),
- PhpMixed::String("json".to_string()),
- ],
- ) {
- self.get_io().write_error(&format!(
- "Unsupported format \"{}\". See help for supported formats.",
- format
- ));
-
- return Ok(1);
- }
-
- let platform_req_filter = self.get_platform_requirement_filter(input.clone())?;
-
- // init repos
- let mut platform_overrides: IndexMap<String, PhpMixed> = IndexMap::new();
- if let Some(ref composer) = composer {
- let composer = crate::composer::composer_full(composer);
- if let Some(p) = composer
- .get_config()
- .borrow()
- .get("platform")
- .as_array()
- .cloned()
- {
- platform_overrides = p.into_iter().collect();
- }
- }
- let platform_repo =
- PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides)?);
- let mut locked_repo: Option<RepositoryInterfaceHandle> = None;
-
- // The single-package $package binding from PHP gets surfaced here.
- let mut single_package: Option<crate::package::CompletePackageInterfaceHandle> = None;
- let mut versions_map: IndexMap<String, String> = IndexMap::new();
- let installed_repo: RepositoryInterfaceHandle;
- let repos: RepositoryInterfaceHandle;
-
- if input.borrow().get_option("self")?.as_bool() == Some(true)
- && input.borrow().get_option("installed")?.as_bool() != Some(true)
- && input.borrow().get_option("locked")?.as_bool() != Some(true)
- {
- let composer = self.require_composer(None, None)?;
- let package = crate::package::RootPackageInterfaceHandle::dup(
- composer.borrow_partial().get_package(),
- );
- if input.borrow().get_option("name-only")?.as_bool() == Some(true) {
- self.get_io().write(&package.get_name());
-
- return Ok(0);
- }
- if input
- .borrow()
- .get_argument("package")?
- .as_string()
- .is_some()
- {
- return Err(InvalidArgumentException {
- message: "You cannot use --self together with a package name".to_string(),
- code: 0,
- }
- .into());
- }
- installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())),
- ]));
- repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())),
- ]));
- single_package = Some(package.into());
- } else if input.borrow().get_option("platform")?.as_bool() == Some(true) {
- installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- platform_repo.clone().into(),
- ]));
- repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- platform_repo.clone().into(),
- ]));
- } else if input.borrow().get_option("available")?.as_bool() == Some(true) {
- let mut ir = InstalledRepository::new(vec![platform_repo.clone().into()]);
- if let Some(ref composer) = composer {
- let composer = crate::composer::composer_full(composer);
- repos = RepositoryInterfaceHandle::new(CompositeRepository::new(
- composer
- .get_repository_manager()
- .borrow()
- .get_repositories()
- .to_vec(),
- ));
- ir.add_repository(
- composer
- .get_repository_manager()
- .borrow()
- .get_local_repository(),
- );
- installed_repo = RepositoryInterfaceHandle::new(ir);
- } else {
- let default_repos =
- RepositoryFactory::default_repos_with_default_manager(self.get_io())?;
- let names: Vec<String> = default_repos.keys().cloned().collect();
- repos = RepositoryInterfaceHandle::new(CompositeRepository::new(
- default_repos.into_values().collect(),
- ));
- self.get_io().write_error(&format!(
- "No composer.json found in the current directory, showing available packages from {}",
- names.join(", ")
- ));
- installed_repo = RepositoryInterfaceHandle::new(ir);
- }
- } else if input.borrow().get_option("all")?.as_bool() == Some(true) && composer.is_some() {
- let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap());
- let local_repo = composer_ref
- .get_repository_manager()
- .borrow()
- .get_local_repository();
- let locker_rc = composer_ref.get_locker().clone();
- let mut locker = locker_rc.borrow_mut();
- if locker.is_locked() {
- let lr_handle: RepositoryInterfaceHandle =
- locker.get_locked_repository(true)?.into();
- installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- lr_handle.clone(),
- local_repo,
- platform_repo.clone().into(),
- ]));
- locked_repo = Some(lr_handle);
- } else {
- installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- local_repo,
- platform_repo.clone().into(),
- ]));
- }
- let mut composite_input: Vec<RepositoryInterfaceHandle> =
- vec![RepositoryInterfaceHandle::new(FilterRepository::new(
- installed_repo.clone(),
- {
- let mut m = IndexMap::new();
- m.insert("canonical".to_string(), PhpMixed::Bool(false));
- m
- },
- )?)];
- for r in composer_ref
- .get_repository_manager()
- .borrow()
- .get_repositories()
- {
- composite_input.push(r.clone());
- }
- repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input));
- } else if input.borrow().get_option("all")?.as_bool() == Some(true) {
- let default_repos =
- RepositoryFactory::default_repos_with_default_manager(self.get_io())?;
- let names: Vec<String> = default_repos.keys().cloned().collect();
- self.get_io().write_error(&format!(
- "No composer.json found in the current directory, showing available packages from {}",
- names.join(", ")
- ));
- installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- platform_repo.clone().into(),
- ]));
- let mut composite_input: Vec<RepositoryInterfaceHandle> = vec![installed_repo.clone()];
- for (_k, v) in default_repos.into_iter() {
- composite_input.push(v);
- }
- repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input));
- } else if input.borrow().get_option("locked")?.as_bool() == Some(true) {
- if composer.is_none()
- || !crate::composer::composer_full(composer.as_ref().unwrap())
- .get_locker()
- .borrow_mut()
- .is_locked()
- {
- return Err(UnexpectedValueException {
- message: "A valid composer.json and composer.lock files is required to run this command with --locked".to_string(),
- code: 0,
- }
- .into());
- }
- let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap());
- let locker_rc = composer_ref.get_locker().clone();
- let mut locker = locker_rc.borrow_mut();
- let lr = locker.get_locked_repository(
- input.borrow().get_option("no-dev")?.as_bool() != Some(true),
- )?;
- if input.borrow().get_option("self")?.as_bool() == Some(true) {
- lr.add_package(
- crate::package::RootPackageInterfaceHandle::dup(composer_ref.get_package())
- .into(),
- )?;
- }
- let lr_handle: RepositoryInterfaceHandle = lr.into();
- locked_repo = Some(lr_handle.clone());
- let new_repo =
- RepositoryInterfaceHandle::new(InstalledRepository::new(vec![lr_handle]));
- installed_repo = new_repo.clone();
- repos = new_repo;
- } else {
- // --installed / default case
- let composer_local_owned;
- let _guard_from_existing;
- let composer_local = match composer.as_ref() {
- Some(c) => {
- _guard_from_existing = crate::composer::composer_full(c);
- &*_guard_from_existing
- }
- None => {
- composer_local_owned = self.require_composer(None, None)?;
- _guard_from_existing = crate::composer::composer_full(&composer_local_owned);
- &*_guard_from_existing
- }
- };
- let root_pkg = composer_local.get_package();
-
- let root_repo: RepositoryInterfaceHandle =
- if input.borrow().get_option("self")?.as_bool() == Some(true) {
- RepositoryInterfaceHandle::new(RootPackageRepository::new(
- crate::package::RootPackageInterfaceHandle::dup(
- composer_local.get_package(),
- ),
- ))
- } else {
- RepositoryInterfaceHandle::new(InstalledArrayRepository::new()?)
- };
- if input.borrow().get_option("no-dev")?.as_bool() == Some(true) {
- let local_packages = composer_local
- .get_repository_manager()
- .borrow()
- .get_local_repository()
- .get_packages()?;
- let packages = RepositoryUtils::filter_required_packages(
- &local_packages,
- root_pkg.clone().into(),
- false,
- Vec::new(),
- );
- let cloned: Vec<crate::package::PackageInterfaceHandle> = packages
- .iter()
- .map(crate::package::PackageInterfaceHandle::dup)
- .collect();
- let new_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- root_repo,
- RepositoryInterfaceHandle::new(InstalledArrayRepository::new_with_packages(
- cloned,
- )?),
- ]));
- installed_repo = new_repo.clone();
- repos = new_repo;
- } else {
- let repository_manager = composer_local.get_repository_manager().clone();
- let repository_manager = repository_manager.borrow();
- let lr = repository_manager.get_local_repository();
- installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
- root_repo.clone(),
- lr.clone(),
- ]));
- repos =
- RepositoryInterfaceHandle::new(InstalledRepository::new(vec![root_repo, lr]));
- }
-
- if installed_repo.get_packages()?.is_empty() {
- let has_non_platform_reqs = |reqs: &IndexMap<String, Link>| -> bool {
- reqs.keys()
- .any(|name| !PlatformRepository::is_platform_package(name))
- };
-
- if has_non_platform_reqs(&root_pkg.get_requires())
- || has_non_platform_reqs(&root_pkg.get_dev_requires())
- {
- // Borrow is local; release composer_local borrow first.
- let _ = root_pkg;
- self.get_io().write_error("<warning>No dependencies installed. Try running composer install or update.</warning>");
- }
- }
- }
-
- if let Some(ref composer) = composer {
- let composer = crate::composer::composer_full(composer);
- let mut command_event = CommandEvent::new6(
- PluginEvents::COMMAND,
- "show",
- input.clone(),
- output,
- vec![],
- IndexMap::new(),
- );
- let command_event_name = command_event.get_name().to_string();
- composer
- .get_event_dispatcher()
- .borrow_mut()
- .dispatch(Some(&command_event_name), Some(&mut command_event))?;
- }
-
- if input.borrow().get_option("latest")?.as_bool() == Some(true) && composer.is_none() {
- self.get_io().write_error(
- "No composer.json found in the current directory, disabling \"latest\" option",
- );
- input
- .borrow_mut()
- .set_option("latest", PhpMixed::Bool(false));
- }
-
- let package_filter: Option<String> = input
- .borrow()
- .get_argument("package")?
- .as_string()
- .map(|s| s.to_string());
-
- // show single package or single version
- if let Some(ref pkg) = single_package {
- versions_map.insert(pkg.get_pretty_version(), pkg.get_version());
- } else if let Some(ref pf) = package_filter
- && !pf.contains('*')
- {
- let (matched_package, vers) = self.get_package(
- &installed_repo,
- &repos,
- pf,
- input.borrow().get_argument("version")?,
- )?;
-
- if let Some(ref pkg) = matched_package
- && input.borrow().get_option("direct")?.as_bool() == Some(true)
- && !in_array_strict(
- pkg.get_name(),
- &self
- .get_root_requires()
- .into_iter()
- .map(PhpMixed::String)
- .collect::<Vec<_>>(),
- )
- {
- return Err(InvalidArgumentException {
- message: format!(
- "Package \"{}\" is installed but not a direct dependent of the root package.",
- pkg.get_name()
- ),
- code: 0,
- }
- .into());
- }
-
- if matched_package.is_none() {
- let options = input.borrow().get_options();
- let mut hint = String::new();
- if input.borrow().get_option("locked")?.as_bool() == Some(true) {
- hint.push_str(" in lock file");
- }
- if let Some(working_dir) = options.get("working-dir").filter(|v| !v.is_null()) {
- hint.push_str(&format!(
- " in {}/composer.json",
- working_dir.as_string().unwrap_or("")
- ));
- }
- if PlatformRepository::is_platform_package(pf)
- && input.borrow().get_option("platform")?.as_bool() != Some(true)
- {
- hint.push_str(", try using --platform (-p) to show platform packages");
- }
- if input.borrow().get_option("all")?.as_bool() != Some(true)
- && input.borrow().get_option("available")?.as_bool() != Some(true)
- {
- hint.push_str(", try using --available (-a) to show all available packages");
- }
-
- return Err(InvalidArgumentException {
- message: format!("Package \"{}\" not found{}.", pf, hint),
- code: 0,
- }
- .into());
- }
- single_package = matched_package;
- versions_map = vers;
- }
-
- if let Some(ref package) = single_package {
- // assert(isset($versions));
-
- let mut exit_code: i64 = 0;
- if input.borrow().get_option("tree")?.as_bool() == Some(true) {
- let array_tree =
- self.generate_package_tree(package.clone().into(), &installed_repo, &repos);
-
- if format == "json" {
- let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new();
- wrapper.insert(
- "installed".to_string(),
- PhpMixed::List(vec![PhpMixed::Array(array_tree.into_iter().collect())]),
- );
- self.get_io().write(&JsonFile::encode(&PhpMixed::Array(
- wrapper.into_iter().collect(),
- ))?);
- } else {
- self.display_package_tree(vec![array_tree]);
- }
-
- return Ok(exit_code);
- }
-
- let mut latest_package: Option<crate::package::PackageInterfaceHandle> = None;
- if input.borrow().get_option("latest")?.as_bool() == Some(true) {
- latest_package = self.find_latest_package(
- package.clone().into(),
- composer.as_ref().unwrap(),
- &platform_repo,
- input
- .borrow()
- .get_option("major-only")?
- .as_bool()
- .unwrap_or(false),
- input
- .borrow()
- .get_option("minor-only")?
- .as_bool()
- .unwrap_or(false),
- input
- .borrow()
- .get_option("patch-only")?
- .as_bool()
- .unwrap_or(false),
- platform_req_filter.clone(),
- )?;
- }
- if input.borrow().get_option("outdated")?.as_bool() == Some(true)
- && input.borrow().get_option("strict")?.as_bool() == Some(true)
- && latest_package.is_some()
- && latest_package
- .as_ref()
- .unwrap()
- .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev)
- != package
- .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev)
- && (latest_package
- .as_ref()
- .unwrap()
- .as_complete()
- .is_none_or(|c| !c.is_abandoned()))
- {
- exit_code = 1;
- }
- if input.borrow().get_option("path")?.as_bool() == Some(true) {
- self.get_io().write_no_newline(&package.get_name());
- let path = {
- let composer_ref = composer.as_ref().unwrap();
- composer_ref
- .borrow_partial()
- .get_installation_manager()
- .borrow_mut()
- .get_install_path(package.clone().into())
- };
- if let Some(path) = path {
- let real = realpath(&path).unwrap_or_default();
- let trimmed = real.split(['\r', '\n']).next().unwrap_or("");
- self.get_io().write(&format!(" {}", trimmed));
- } else {
- self.get_io().write(" null");
- }
-
- return Ok(exit_code);
- }
-
- if format == "json" {
- self.print_package_info_as_json(
- package.clone(),
- &versions_map,
- &mut *installed_repo.borrow_mut(),
- latest_package,
- )?;
- } else {
- self.print_package_info(
- package.clone(),
- &versions_map,
- &mut *installed_repo.borrow_mut(),
- latest_package,
- )?;
- }
-
- return Ok(exit_code);
- }
-
- // show tree view if requested
- if input.borrow().get_option("tree")?.as_bool() == Some(true) {
- let root_requires = self.get_root_requires();
- let mut packages = installed_repo.get_packages()?;
- packages.sort_by(|a, b| {
- let sa: String = a.to_string();
- let sb: String = b.to_string();
- sa.cmp(&sb)
- });
- let mut array_tree: Vec<IndexMap<String, PhpMixed>> = Vec::new();
- for package in packages.iter() {
- if in_array_strict(
- package.get_name(),
- &root_requires
- .iter()
- .map(|s| PhpMixed::String(s.clone()))
- .collect::<Vec<_>>(),
- ) {
- array_tree.push(self.generate_package_tree(
- package.clone(),
- &installed_repo,
- &repos,
- ));
- }
- }
-
- if format == "json" {
- let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new();
- wrapper.insert(
- "installed".to_string(),
- PhpMixed::List(
- array_tree
- .into_iter()
- .map(|m| PhpMixed::Array(m.into_iter().collect()))
- .collect(),
- ),
- );
- self.get_io().write(&JsonFile::encode(&PhpMixed::Array(
- wrapper.into_iter().collect(),
- ))?);
- } else {
- self.display_package_tree(array_tree);
- }
-
- return Ok(0);
- }
-
- // list packages
- let mut packages: IndexMap<String, IndexMap<String, PackageOrName>> = IndexMap::new();
- let mut package_filter_regex: Option<String> = None;
- if let Some(ref pf) = package_filter {
- let escaped = shirabe_php_shim::preg_quote(pf, None);
- package_filter_regex = Some(format!("{{^{}$}}i", escaped.replace("\\*", ".*?")));
- }
-
- let mut package_list_filter: Option<Vec<String>> = None;
- if input.borrow().get_option("direct")?.as_bool() == Some(true) {
- package_list_filter = Some(self.get_root_requires());
- }
-
- if input.borrow().get_option("path")?.as_bool() == Some(true) && composer.is_none() {
- self.get_io().write_error(
- "No composer.json found in the current directory, disabling \"path\" option",
- );
- input.borrow_mut().set_option("path", PhpMixed::Bool(false));
- }
-
- for repo in RepositoryUtils::flatten_repositories(repos, true) {
- let r#type = if Self::same_repository(&repo, &platform_repo) {
- "platform"
- } else if locked_repo
- .as_ref()
- .is_some_and(|lr| Self::same_repository(&repo, lr))
- {
- "locked"
- } else if Self::same_repository(&repo, &installed_repo)
- || installed_repo
- .borrow()
- .as_any()
- .downcast_ref::<InstalledRepository>()
- .is_some_and(|ir| ir.get_repositories().iter().any(|r| r.ptr_eq(&repo)))
- {
- "installed"
- } else {
- "available"
- };
- let type_owned = r#type.to_string();
- 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_default()
- .insert(name.clone(), PackageOrName::Name(name));
- }
- } else {
- for package in repo.get_packages()? {
- let existing = packages
- .get(&type_owned)
- .and_then(|m| m.get(&package.get_name()));
- let need_replace = match existing {
- None => true,
- Some(PackageOrName::Name(_)) => true,
- Some(PackageOrName::Pkg(existing)) => {
- version_compare(&existing.get_version(), &package.get_version(), "<")
- }
- };
- if need_replace {
- let mut p: crate::package::PackageInterfaceHandle = package.clone();
- while let Some(alias) = p.as_alias() {
- p = alias.get_alias_of().into();
- }
- let matches_filter = match &package_filter_regex {
- None => true,
- Some(r) => Preg::is_match(r, &p.get_name()),
- };
- if matches_filter {
- let matches_list = match &package_list_filter {
- None => true,
- Some(list) => in_array_strict(
- p.get_name(),
- &list
- .iter()
- .map(|s| PhpMixed::String(s.clone()))
- .collect::<Vec<_>>(),
- ),
- };
- if matches_list {
- packages
- .entry(type_owned.clone())
- .or_default()
- .insert(p.get_name(), PackageOrName::Pkg(p));
- }
- }
- }
- }
- if Self::same_repository(&repo, &platform_repo) {
- for (name, p) in platform_repo.borrow().get_disabled_packages() {
- packages
- .entry(type_owned.clone())
- .or_default()
- .insert(name.clone(), PackageOrName::Pkg(p.clone().into()));
- }
- }
- }
- }
-
- let show_all_types = input.borrow().get_option("all")?.as_bool() == Some(true);
- let show_latest = input.borrow().get_option("latest")?.as_bool() == Some(true);
- let show_major_only = input.borrow().get_option("major-only")?.as_bool() == Some(true);
- let show_minor_only = input.borrow().get_option("minor-only")?.as_bool() == Some(true);
- let show_patch_only = input.borrow().get_option("patch-only")?.as_bool() == Some(true);
- let ignored_packages_regex = base_package::package_names_to_regexp(
- &input
- .borrow()
- .get_option("ignore")?
- .as_list()
- .map(|l| {
- l.iter()
- .filter_map(|v| v.as_string().map(strtolower))
- .collect::<Vec<_>>()
- })
- .unwrap_or_default(),
- "{^(?:%s)$}iD",
- );
- let indent = if show_all_types { " " } else { "" };
- let mut latest_packages: IndexMap<String, crate::package::PackageInterfaceHandle> =
- IndexMap::new();
- let mut exit_code: i64 = 0;
- let mut view_data: IndexMap<String, Vec<IndexMap<String, PhpMixed>>> = IndexMap::new();
- let mut view_meta_data: IndexMap<String, ViewMetaData> = IndexMap::new();
-
- let mut write_version = false;
- let mut write_description = false;
-
- let type_order: Vec<(&str, bool)> = vec![
- ("platform", true),
- ("locked", true),
- ("available", false),
- ("installed", true),
- ];
- for (r#type, show_version) in type_order.iter() {
- if let Some(type_packages) = packages.get_mut(*r#type) {
- type_packages.sort_keys();
-
- let mut name_length: usize = 0;
- let mut version_length: usize = 0;
- let mut latest_length: usize = 0;
- let mut release_date_length: usize = 0;
-
- if show_latest && *show_version {
- for package_or_name in type_packages.values() {
- if let PackageOrName::Pkg(package) = package_or_name
- && !Preg::is_match(&ignored_packages_regex, &package.get_pretty_name())
- {
- let latest = self.find_latest_package(
- package.clone(),
- composer.as_ref().unwrap(),
- &platform_repo,
- show_major_only,
- show_minor_only,
- show_patch_only,
- platform_req_filter.clone(),
- )?;
- if latest.is_none() {
- continue;
- }
-
- latest_packages.insert(package.get_pretty_name(), latest.unwrap());
- }
- }
- }
-
- let write_path = input.borrow().get_option("name-only")?.as_bool() != Some(true)
- && input.borrow().get_option("path")?.as_bool() == Some(true);
- write_version = input.borrow().get_option("name-only")?.as_bool() != Some(true)
- && input.borrow().get_option("path")?.as_bool() != Some(true)
- && *show_version;
- let write_latest = write_version && show_latest;
- write_description = input.borrow().get_option("name-only")?.as_bool() != Some(true)
- && input.borrow().get_option("path")?.as_bool() != Some(true);
- let write_release_date = write_latest
- && (input.borrow().get_option("sort-by-age")?.as_bool() == Some(true)
- || format == "json");
-
- let mut has_outdated_packages = false;
-
- if input.borrow().get_option("sort-by-age")?.as_bool() == Some(true) {
- type_packages.sort_by(|_ka, a, _kb, b| match (a, b) {
- (PackageOrName::Pkg(a), PackageOrName::Pkg(b)) => {
- a.get_release_date().cmp(&b.get_release_date())
- }
- _ => std::cmp::Ordering::Equal,
- });
- }
-
- let mut view_type: Vec<IndexMap<String, PhpMixed>> = Vec::new();
- for package_or_name in type_packages.values() {
- let mut package_view_data: IndexMap<String, PhpMixed> = IndexMap::new();
- if let PackageOrName::Pkg(package) = package_or_name {
- let latest_package = if show_latest
- && latest_packages.contains_key(&package.get_pretty_name())
- {
- latest_packages.get(&package.get_pretty_name())
- } else {
- None
- };
-
- // Determine if Composer is checking outdated dependencies and if current package should trigger non-default exit code
- let mut package_is_up_to_date = if let Some(latest) = latest_package {
- latest.get_full_pretty_version(
- true,
- crate::package::DisplayMode::SourceRefIfDev,
- ) == package.get_full_pretty_version(
- true,
- crate::package::DisplayMode::SourceRefIfDev,
- ) && latest.as_complete().is_none_or(|c| !c.is_abandoned())
- } else {
- false
- };
- // When using --major-only, and no bigger version than current major is found then it is considered up to date
- package_is_up_to_date =
- package_is_up_to_date || (latest_package.is_none() && show_major_only);
- let package_is_ignored =
- Preg::is_match(&ignored_packages_regex, &package.get_pretty_name());
- if input.borrow().get_option("outdated")?.as_bool() == Some(true)
- && (package_is_up_to_date || package_is_ignored)
- {
- continue;
- }
-
- if input.borrow().get_option("outdated")?.as_bool() == Some(true)
- || input.borrow().get_option("strict")?.as_bool() == Some(true)
- {
- has_outdated_packages = true;
- }
-
- package_view_data.insert(
- "name".to_string(),
- PhpMixed::String(package.get_pretty_name()),
- );
- package_view_data.insert(
- "direct-dependency".to_string(),
- PhpMixed::Bool(in_array_strict(
- package.get_name(),
- &self
- .get_root_requires()
- .into_iter()
- .map(PhpMixed::String)
- .collect::<Vec<_>>(),
- )),
- );
- if format != "json"
- || input.borrow().get_option("name-only")?.as_bool() != Some(true)
- {
- package_view_data.insert(
- "homepage".to_string(),
- match package.as_complete() {
- Some(c) => match c.get_homepage() {
- Some(h) => PhpMixed::String(h),
- None => PhpMixed::Null,
- },
- None => PhpMixed::Null,
- },
- );
- package_view_data.insert(
- "source".to_string(),
- match PackageInfo::get_view_source_url(package.clone()) {
- Some(s) => PhpMixed::String(s),
- None => PhpMixed::Null,
- },
- );
- }
- name_length = name_length.max(package.get_pretty_name().len());
- if write_version {
- let mut version_str = package.get_full_pretty_version(
- true,
- crate::package::DisplayMode::SourceRefIfDev,
- );
- if format == "text" {
- version_str = version_str.trim_start_matches('v').to_string();
- }
- version_length = version_length.max(version_str.len());
- package_view_data
- .insert("version".to_string(), PhpMixed::String(version_str));
- }
- if write_release_date {
- if let Some(release_date) = package.get_release_date() {
- let mut age = self
- .get_relative_time(&release_date)
- .replace(" ago", " old");
- if !age.contains(" old") {
- age = format!("from {}", age);
- }
- release_date_length = release_date_length.max(age.len());
- package_view_data
- .insert("release-age".to_string(), PhpMixed::String(age));
- package_view_data.insert(
- "release-date".to_string(),
- PhpMixed::String(release_date.format(DATE_ATOM).to_string()),
- );
- } else {
- package_view_data.insert(
- "release-age".to_string(),
- PhpMixed::String(String::new()),
- );
- package_view_data.insert(
- "release-date".to_string(),
- PhpMixed::String(String::new()),
- );
- }
- }
- if write_latest && let Some(latest) = latest_package {
- let mut latest_version_str = latest.get_full_pretty_version(
- true,
- crate::package::DisplayMode::SourceRefIfDev,
- );
- if format == "text" {
- latest_version_str =
- latest_version_str.trim_start_matches('v').to_string();
- }
- let update_status =
- Self::get_update_status(latest.clone(), package.clone())?;
- latest_length = latest_length.max(latest_version_str.len());
- package_view_data
- .insert("latest".to_string(), PhpMixed::String(latest_version_str));
- package_view_data.insert(
- "latest-status".to_string(),
- PhpMixed::String(update_status),
- );
-
- if let Some(rd) = latest.get_release_date() {
- package_view_data.insert(
- "latest-release-date".to_string(),
- PhpMixed::String(rd.format(DATE_ATOM).to_string()),
- );
- } else {
- package_view_data.insert(
- "latest-release-date".to_string(),
- PhpMixed::String(String::new()),
- );
- }
- } else if write_latest {
- package_view_data.insert(
- "latest".to_string(),
- PhpMixed::String("[none matched]".to_string()),
- );
- package_view_data.insert(
- "latest-status".to_string(),
- PhpMixed::String("up-to-date".to_string()),
- );
- latest_length = latest_length.max("[none matched]".len());
- }
- if write_description && let Some(c) = package.as_complete() {
- package_view_data.insert(
- "description".to_string(),
- match c.get_description() {
- Some(d) => PhpMixed::String(d),
- None => PhpMixed::Null,
- },
- );
- }
- if write_path {
- let installation_manager = composer
- .as_ref()
- .unwrap()
- .borrow_partial()
- .get_installation_manager();
- let path: Option<String> = installation_manager
- .borrow_mut()
- .get_install_path(package.clone());
- if let Some(p) = path {
- let r = realpath(&p).unwrap_or_default();
- let trimmed = r.split(['\r', '\n']).next().unwrap_or("");
- package_view_data.insert(
- "path".to_string(),
- PhpMixed::String(trimmed.to_string()),
- );
- } else {
- package_view_data.insert("path".to_string(), PhpMixed::Null);
- }
- }
-
- let mut package_is_abandoned: PhpMixed = PhpMixed::Bool(false);
- if let Some(latest) = latest_package
- && let Some(c) = latest.as_complete()
- && c.is_abandoned()
- {
- let replacement_package_name = c.get_replacement_package();
- let replacement = if let Some(ref rp) = replacement_package_name {
- format!("Use {} instead", rp)
- } else {
- "No replacement was suggested".to_string()
- };
- let package_warning = format!(
- "Package {} is abandoned, you should avoid using it. {}.",
- package.get_pretty_name(),
- replacement
- );
- package_view_data
- .insert("warning".to_string(), PhpMixed::String(package_warning));
- package_is_abandoned = match replacement_package_name {
- Some(rp) => PhpMixed::String(rp),
- None => PhpMixed::Bool(true),
- };
- }
-
- package_view_data.insert("abandoned".to_string(), package_is_abandoned);
- } else if let PackageOrName::Name(name) = package_or_name {
- package_view_data
- .insert("name".to_string(), PhpMixed::String(name.clone()));
- name_length = name_length.max(name.len());
- }
- view_type.push(package_view_data);
- }
- view_data.insert(r#type.to_string(), view_type);
- view_meta_data.insert(
- r#type.to_string(),
- ViewMetaData {
- name_length,
- version_length,
- latest_length,
- release_date_length,
- write_latest,
- write_release_date,
- },
- );
- if input.borrow().get_option("strict")?.as_bool() == Some(true)
- && has_outdated_packages
- {
- exit_code = 1;
- break;
- }
- }
- }
-
- if format == "json" {
- let mut json_map: IndexMap<String, PhpMixed> = IndexMap::new();
- for (k, v) in view_data.iter() {
- json_map.insert(
- k.clone(),
- PhpMixed::List(
- v.iter()
- .map(|m| {
- PhpMixed::Array(
- m.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
- )
- })
- .collect(),
- ),
- );
- }
- let io = self.get_io();
- io.write(&JsonFile::encode(&PhpMixed::Array(
- json_map.into_iter().collect(),
- ))?);
- } else {
- if input.borrow().get_option("latest")?.as_bool() == Some(true)
- && view_data.values().any(|v| !v.is_empty())
- {
- let io = self.get_io();
- if !io.is_decorated() {
- io.write_error("Legend:");
- io.write_error("! patch or minor release available - update recommended");
- io.write_error("~ major release available - update possible");
- if input.borrow().get_option("outdated")?.as_bool() != Some(true) {
- io.write_error("= up to date version");
- }
- } else {
- io.write_error("<info>Color legend:</info>");
- io.write_error("- <highlight>patch or minor</highlight> release available - update recommended");
- io.write_error(
- "- <comment>major</comment> release available - update possible",
- );
- if input.borrow().get_option("outdated")?.as_bool() != Some(true) {
- io.write_error("- <info>up to date</info> version");
- }
- }
- }
-
- let width = self.get_terminal_width();
-
- for (r#type, packages) in view_data.iter() {
- let meta = match view_meta_data.get(r#type) {
- Some(m) => m.clone(),
- None => continue,
- };
- let name_length = meta.name_length;
- let version_length = meta.version_length;
- let mut latest_length = meta.latest_length;
- let release_date_length = meta.release_date_length;
- let write_latest = meta.write_latest;
- let write_release_date = meta.write_release_date;
-
- let width_usize = width as usize;
- let version_fits = name_length + version_length + 3 <= width_usize;
- let latest_fits = name_length + version_length + latest_length + 3 <= width_usize;
- let release_date_fits =
- name_length + version_length + latest_length + release_date_length + 3
- <= width_usize;
- let description_fits =
- name_length + version_length + latest_length + release_date_length + 24
- <= width_usize;
-
- if latest_fits && !self.get_io().is_decorated() {
- latest_length += 2;
- }
-
- if show_all_types {
- if r#type == "available" {
- self.get_io()
- .write(&format!("<comment>{}</comment>:", r#type));
- } else {
- self.get_io().write(&format!("<info>{}</info>:", r#type));
- }
- }
-
- if write_latest && input.borrow().get_option("direct")?.as_bool() != Some(true) {
- let mut direct_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new();
- let mut transitive_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new();
- for pkg in packages.iter() {
- let is_direct = pkg
- .get("direct-dependency")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
- if is_direct {
- direct_deps.push(pkg.clone());
- } else {
- transitive_deps.push(pkg.clone());
- }
- }
-
- self.get_io().write_error("");
- self.get_io()
- .write_error("<info>Direct dependencies required in composer.json:</>");
- if !direct_deps.is_empty() {
- self.print_packages(
- &direct_deps,
- indent,
- write_version && version_fits,
- latest_fits,
- write_description && description_fits,
- width_usize,
- version_length,
- name_length,
- latest_length,
- write_release_date && release_date_fits,
- release_date_length,
- );
- } else {
- self.get_io().write_error("Everything up to date");
- }
- self.get_io().write_error("");
- self.get_io().write_error(
- "<info>Transitive dependencies not required in composer.json:</>",
- );
- if !transitive_deps.is_empty() {
- self.print_packages(
- &transitive_deps,
- indent,
- write_version && version_fits,
- latest_fits,
- write_description && description_fits,
- width_usize,
- version_length,
- name_length,
- latest_length,
- write_release_date && release_date_fits,
- release_date_length,
- );
- } else {
- self.get_io().write_error("Everything up to date");
- }
- } else {
- if write_latest && packages.is_empty() {
- self.get_io()
- .write_error("All your direct dependencies are up to date");
- } else {
- self.print_packages(
- packages,
- indent,
- write_version && version_fits,
- write_latest && latest_fits,
- write_description && description_fits,
- width_usize,
- version_length,
- name_length,
- latest_length,
- write_release_date && release_date_fits,
- release_date_length,
- );
- }
- }
-
- if show_all_types {
- self.get_io().write("");
- }
- }
- }
-
- Ok(exit_code)
- }
-
- fn initialize(
- &self,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<()> {
- base_command_initialize(self, input, output)
- }
-
- fn complete(
- &self,
- input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
- suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
- ) -> anyhow::Result<()> {
- crate::command::base_command::base_command_complete(self, input, suggestions)
- }
-
- shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
-}
-
-impl BaseCommand for ShowCommand {
- fn base_command_data(&self) -> &crate::command::BaseCommandData {
- &self.base_command_data
- }
-
- crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
-}
-impl ShowCommand {
/// PHP: protected function suggestPackageBasedOnMode(): \Closure
pub(crate) fn suggest_package_based_on_mode(&self) -> crate::console::input::SuggestedValues {
crate::console::input::SuggestedValues::Closure(Box::new(|this, input, suggestions| {
@@ -2908,6 +1533,1379 @@ impl ShowCommand {
}
}
+impl Command for ShowCommand {
+ fn configure(&self) -> anyhow::Result<()> {
+ self.set_name("show")?;
+ self.set_aliases(vec!["info".to_string()])?;
+ self.set_description("Shows information about packages");
+ let opt_none = |name: &str, shortcut: Option<&str>, description: &str| {
+ InputOption::new(
+ name,
+ shortcut.map(|s| PhpMixed::String(s.to_string())),
+ Some(InputOption::VALUE_NONE),
+ description,
+ None,
+ )
+ .unwrap()
+ .into()
+ };
+ self.set_definition(&[
+ InputArgument::new5(
+ "package",
+ Some(InputArgument::OPTIONAL),
+ "Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.",
+ None,
+ self.suggest_package_based_on_mode(),
+ )
+ .unwrap()
+ .into(),
+ InputArgument::new(
+ "version",
+ Some(InputArgument::OPTIONAL),
+ "Version or version constraint to inspect",
+ None,
+ )
+ .unwrap()
+ .into(),
+ opt_none("all", None, "List all packages"),
+ opt_none("locked", None, "List all locked packages"),
+ opt_none(
+ "installed",
+ Some("i"),
+ "List installed packages only (enabled by default, only present for BC).",
+ ),
+ opt_none("platform", Some("p"), "List platform packages only"),
+ opt_none("available", Some("a"), "List available packages only"),
+ opt_none("self", Some("s"), "Show the root package information"),
+ opt_none("name-only", Some("N"), "List package names only"),
+ opt_none("path", Some("P"), "Show package paths"),
+ opt_none("tree", Some("t"), "List the dependencies as a tree"),
+ opt_none("latest", Some("l"), "Show the latest version"),
+ opt_none(
+ "outdated",
+ Some("o"),
+ "Show the latest version but only for packages that are outdated",
+ ),
+ InputOption::new6(
+ "ignore",
+ None,
+ Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY),
+ "Ignore specified package(s). Can contain wildcards (*). Use it with the --outdated option if you don't want to be informed about new versions of some packages.",
+ None,
+ self.suggest_installed_package(false, false),
+ )
+ .unwrap()
+ .into(),
+ opt_none(
+ "major-only",
+ Some("M"),
+ "Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.",
+ ),
+ opt_none(
+ "minor-only",
+ Some("m"),
+ "Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.",
+ ),
+ opt_none(
+ "patch-only",
+ None,
+ "Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.",
+ ),
+ opt_none(
+ "sort-by-age",
+ Some("A"),
+ "Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option.",
+ ),
+ opt_none(
+ "direct",
+ Some("D"),
+ "Shows only packages that are directly required by the root package",
+ ),
+ opt_none(
+ "strict",
+ None,
+ "Return a non-zero exit code when there are outdated packages",
+ ),
+ InputOption::new6(
+ "format",
+ Some(PhpMixed::String("f".to_string())),
+ Some(InputOption::VALUE_REQUIRED),
+ "Format of the output: text or json",
+ Some(PhpMixed::String("text".to_string())),
+ SuggestedValues::List(vec!["json".to_string(), "text".to_string()]),
+ )
+ .unwrap()
+ .into(),
+ opt_none("no-dev", None, "Disables search in require-dev packages."),
+ InputOption::new(
+ "ignore-platform-req",
+ None,
+ Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY),
+ "Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option",
+ None,
+ )
+ .unwrap()
+ .into(),
+ opt_none(
+ "ignore-platform-reqs",
+ None,
+ "Ignore all platform requirements (php & ext- packages). Use with the --outdated option",
+ ),
+ ]);
+ self.set_help(
+ "The show command displays detailed information about a package, or\n\
+ lists all packages available.\n\n\
+ Read more at https://getcomposer.org/doc/03-cli.md#show-info",
+ );
+ Ok(())
+ }
+
+ fn execute(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i64> {
+ *self.version_parser.borrow_mut() = VersionParser::new();
+ if input.borrow().get_option("tree")?.as_bool() == Some(true) {
+ self.init_styles(output.clone());
+ }
+
+ let composer = self.try_composer(None, None);
+
+ if input.borrow().get_option("installed")?.as_bool() == Some(true)
+ && input.borrow().get_option("self")?.as_bool() != Some(true)
+ {
+ self.get_io().write_error("<warning>You are using the deprecated option \"installed\". Only installed packages are shown by default now. The --all option can be used to show all packages.</warning>");
+ }
+
+ if input.borrow().get_option("outdated")?.as_bool() == Some(true) {
+ input
+ .borrow_mut()
+ .set_option("latest", PhpMixed::Bool(true));
+ } else if input
+ .borrow()
+ .get_option("ignore")?
+ .as_list()
+ .map_or(0, |l| l.len())
+ > 0
+ {
+ self.get_io().write_error("<warning>You are using the option \"ignore\" for action other than \"outdated\", it will be ignored.</warning>");
+ }
+
+ if input.borrow().get_option("direct")?.as_bool() == Some(true)
+ && (input.borrow().get_option("all")?.as_bool() == Some(true)
+ || input.borrow().get_option("available")?.as_bool() == Some(true)
+ || input.borrow().get_option("platform")?.as_bool() == Some(true))
+ {
+ self.get_io().write_error("The --direct (-D) option is not usable in combination with --all, --platform (-p) or --available (-a)");
+
+ return Ok(1);
+ }
+
+ if input.borrow().get_option("tree")?.as_bool() == Some(true)
+ && (input.borrow().get_option("all")?.as_bool() == Some(true)
+ || input.borrow().get_option("available")?.as_bool() == Some(true))
+ {
+ self.get_io().write_error("The --tree (-t) option is not usable in combination with --all or --available (-a)");
+
+ return Ok(1);
+ }
+
+ let only_count: usize = [
+ input.borrow().get_option("patch-only")?.as_bool() == Some(true),
+ input.borrow().get_option("minor-only")?.as_bool() == Some(true),
+ input.borrow().get_option("major-only")?.as_bool() == Some(true),
+ ]
+ .iter()
+ .filter(|b| **b)
+ .count();
+ if only_count > 1 {
+ self.get_io().write_error(
+ "Only one of --major-only, --minor-only or --patch-only can be used at once",
+ );
+
+ return Ok(1);
+ }
+
+ if input.borrow().get_option("tree")?.as_bool() == Some(true)
+ && input.borrow().get_option("latest")?.as_bool() == Some(true)
+ {
+ self.get_io().write_error(
+ "The --tree (-t) option is not usable in combination with --latest (-l)",
+ );
+
+ return Ok(1);
+ }
+
+ if input.borrow().get_option("tree")?.as_bool() == Some(true)
+ && input.borrow().get_option("path")?.as_bool() == Some(true)
+ {
+ self.get_io().write_error(
+ "The --tree (-t) option is not usable in combination with --path (-P)",
+ );
+
+ return Ok(1);
+ }
+
+ let format = input
+ .borrow()
+ .get_option("format")?
+ .as_string()
+ .unwrap_or("text")
+ .to_string();
+ if !in_array_loose(
+ format.clone(),
+ &[
+ PhpMixed::String("text".to_string()),
+ PhpMixed::String("json".to_string()),
+ ],
+ ) {
+ self.get_io().write_error(&format!(
+ "Unsupported format \"{}\". See help for supported formats.",
+ format
+ ));
+
+ return Ok(1);
+ }
+
+ let platform_req_filter = self.get_platform_requirement_filter(input.clone())?;
+
+ // init repos
+ let mut platform_overrides: IndexMap<String, PhpMixed> = IndexMap::new();
+ if let Some(ref composer) = composer {
+ let composer = crate::composer::composer_full(composer);
+ if let Some(p) = composer
+ .get_config()
+ .borrow()
+ .get("platform")
+ .as_array()
+ .cloned()
+ {
+ platform_overrides = p.into_iter().collect();
+ }
+ }
+ let platform_repo =
+ PlatformRepositoryHandle::new(PlatformRepository::new(vec![], platform_overrides)?);
+ let mut locked_repo: Option<RepositoryInterfaceHandle> = None;
+
+ // The single-package $package binding from PHP gets surfaced here.
+ let mut single_package: Option<crate::package::CompletePackageInterfaceHandle> = None;
+ let mut versions_map: IndexMap<String, String> = IndexMap::new();
+ let installed_repo: RepositoryInterfaceHandle;
+ let repos: RepositoryInterfaceHandle;
+
+ if input.borrow().get_option("self")?.as_bool() == Some(true)
+ && input.borrow().get_option("installed")?.as_bool() != Some(true)
+ && input.borrow().get_option("locked")?.as_bool() != Some(true)
+ {
+ let composer = self.require_composer(None, None)?;
+ let package = crate::package::RootPackageInterfaceHandle::dup(
+ composer.borrow_partial().get_package(),
+ );
+ if input.borrow().get_option("name-only")?.as_bool() == Some(true) {
+ self.get_io().write(&package.get_name());
+
+ return Ok(0);
+ }
+ if input
+ .borrow()
+ .get_argument("package")?
+ .as_string()
+ .is_some()
+ {
+ return Err(InvalidArgumentException {
+ message: "You cannot use --self together with a package name".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+ installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())),
+ ]));
+ repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ RepositoryInterfaceHandle::new(RootPackageRepository::new(package.clone())),
+ ]));
+ single_package = Some(package.into());
+ } else if input.borrow().get_option("platform")?.as_bool() == Some(true) {
+ installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ platform_repo.clone().into(),
+ ]));
+ repos = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ platform_repo.clone().into(),
+ ]));
+ } else if input.borrow().get_option("available")?.as_bool() == Some(true) {
+ let mut ir = InstalledRepository::new(vec![platform_repo.clone().into()]);
+ if let Some(ref composer) = composer {
+ let composer = crate::composer::composer_full(composer);
+ repos = RepositoryInterfaceHandle::new(CompositeRepository::new(
+ composer
+ .get_repository_manager()
+ .borrow()
+ .get_repositories()
+ .to_vec(),
+ ));
+ ir.add_repository(
+ composer
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository(),
+ );
+ installed_repo = RepositoryInterfaceHandle::new(ir);
+ } else {
+ let default_repos =
+ RepositoryFactory::default_repos_with_default_manager(self.get_io())?;
+ let names: Vec<String> = default_repos.keys().cloned().collect();
+ repos = RepositoryInterfaceHandle::new(CompositeRepository::new(
+ default_repos.into_values().collect(),
+ ));
+ self.get_io().write_error(&format!(
+ "No composer.json found in the current directory, showing available packages from {}",
+ names.join(", ")
+ ));
+ installed_repo = RepositoryInterfaceHandle::new(ir);
+ }
+ } else if input.borrow().get_option("all")?.as_bool() == Some(true) && composer.is_some() {
+ let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap());
+ let local_repo = composer_ref
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository();
+ let locker_rc = composer_ref.get_locker().clone();
+ let mut locker = locker_rc.borrow_mut();
+ if locker.is_locked() {
+ let lr_handle: RepositoryInterfaceHandle =
+ locker.get_locked_repository(true)?.into();
+ installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ lr_handle.clone(),
+ local_repo,
+ platform_repo.clone().into(),
+ ]));
+ locked_repo = Some(lr_handle);
+ } else {
+ installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ local_repo,
+ platform_repo.clone().into(),
+ ]));
+ }
+ let mut composite_input: Vec<RepositoryInterfaceHandle> =
+ vec![RepositoryInterfaceHandle::new(FilterRepository::new(
+ installed_repo.clone(),
+ {
+ let mut m = IndexMap::new();
+ m.insert("canonical".to_string(), PhpMixed::Bool(false));
+ m
+ },
+ )?)];
+ for r in composer_ref
+ .get_repository_manager()
+ .borrow()
+ .get_repositories()
+ {
+ composite_input.push(r.clone());
+ }
+ repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input));
+ } else if input.borrow().get_option("all")?.as_bool() == Some(true) {
+ let default_repos =
+ RepositoryFactory::default_repos_with_default_manager(self.get_io())?;
+ let names: Vec<String> = default_repos.keys().cloned().collect();
+ self.get_io().write_error(&format!(
+ "No composer.json found in the current directory, showing available packages from {}",
+ names.join(", ")
+ ));
+ installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ platform_repo.clone().into(),
+ ]));
+ let mut composite_input: Vec<RepositoryInterfaceHandle> = vec![installed_repo.clone()];
+ for (_k, v) in default_repos.into_iter() {
+ composite_input.push(v);
+ }
+ repos = RepositoryInterfaceHandle::new(CompositeRepository::new(composite_input));
+ } else if input.borrow().get_option("locked")?.as_bool() == Some(true) {
+ if composer.is_none()
+ || !crate::composer::composer_full(composer.as_ref().unwrap())
+ .get_locker()
+ .borrow_mut()
+ .is_locked()
+ {
+ return Err(UnexpectedValueException {
+ message: "A valid composer.json and composer.lock files is required to run this command with --locked".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+ let composer_ref = crate::composer::composer_full(composer.as_ref().unwrap());
+ let locker_rc = composer_ref.get_locker().clone();
+ let mut locker = locker_rc.borrow_mut();
+ let lr = locker.get_locked_repository(
+ input.borrow().get_option("no-dev")?.as_bool() != Some(true),
+ )?;
+ if input.borrow().get_option("self")?.as_bool() == Some(true) {
+ lr.add_package(
+ crate::package::RootPackageInterfaceHandle::dup(composer_ref.get_package())
+ .into(),
+ )?;
+ }
+ let lr_handle: RepositoryInterfaceHandle = lr.into();
+ locked_repo = Some(lr_handle.clone());
+ let new_repo =
+ RepositoryInterfaceHandle::new(InstalledRepository::new(vec![lr_handle]));
+ installed_repo = new_repo.clone();
+ repos = new_repo;
+ } else {
+ // --installed / default case
+ let composer_local_owned;
+ let _guard_from_existing;
+ let composer_local = match composer.as_ref() {
+ Some(c) => {
+ _guard_from_existing = crate::composer::composer_full(c);
+ &*_guard_from_existing
+ }
+ None => {
+ composer_local_owned = self.require_composer(None, None)?;
+ _guard_from_existing = crate::composer::composer_full(&composer_local_owned);
+ &*_guard_from_existing
+ }
+ };
+ let root_pkg = composer_local.get_package();
+
+ let root_repo: RepositoryInterfaceHandle =
+ if input.borrow().get_option("self")?.as_bool() == Some(true) {
+ RepositoryInterfaceHandle::new(RootPackageRepository::new(
+ crate::package::RootPackageInterfaceHandle::dup(
+ composer_local.get_package(),
+ ),
+ ))
+ } else {
+ RepositoryInterfaceHandle::new(InstalledArrayRepository::new()?)
+ };
+ if input.borrow().get_option("no-dev")?.as_bool() == Some(true) {
+ let local_packages = composer_local
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository()
+ .get_packages()?;
+ let packages = RepositoryUtils::filter_required_packages(
+ &local_packages,
+ root_pkg.clone().into(),
+ false,
+ Vec::new(),
+ );
+ let cloned: Vec<crate::package::PackageInterfaceHandle> = packages
+ .iter()
+ .map(crate::package::PackageInterfaceHandle::dup)
+ .collect();
+ let new_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ root_repo,
+ RepositoryInterfaceHandle::new(InstalledArrayRepository::new_with_packages(
+ cloned,
+ )?),
+ ]));
+ installed_repo = new_repo.clone();
+ repos = new_repo;
+ } else {
+ let repository_manager = composer_local.get_repository_manager().clone();
+ let repository_manager = repository_manager.borrow();
+ let lr = repository_manager.get_local_repository();
+ installed_repo = RepositoryInterfaceHandle::new(InstalledRepository::new(vec![
+ root_repo.clone(),
+ lr.clone(),
+ ]));
+ repos =
+ RepositoryInterfaceHandle::new(InstalledRepository::new(vec![root_repo, lr]));
+ }
+
+ if installed_repo.get_packages()?.is_empty() {
+ let has_non_platform_reqs = |reqs: &IndexMap<String, Link>| -> bool {
+ reqs.keys()
+ .any(|name| !PlatformRepository::is_platform_package(name))
+ };
+
+ if has_non_platform_reqs(&root_pkg.get_requires())
+ || has_non_platform_reqs(&root_pkg.get_dev_requires())
+ {
+ // Borrow is local; release composer_local borrow first.
+ let _ = root_pkg;
+ self.get_io().write_error("<warning>No dependencies installed. Try running composer install or update.</warning>");
+ }
+ }
+ }
+
+ if let Some(ref composer) = composer {
+ let composer = crate::composer::composer_full(composer);
+ let mut command_event = CommandEvent::new6(
+ PluginEvents::COMMAND,
+ "show",
+ input.clone(),
+ output,
+ vec![],
+ IndexMap::new(),
+ );
+ let command_event_name = command_event.get_name().to_string();
+ composer
+ .get_event_dispatcher()
+ .borrow_mut()
+ .dispatch(Some(&command_event_name), Some(&mut command_event))?;
+ }
+
+ if input.borrow().get_option("latest")?.as_bool() == Some(true) && composer.is_none() {
+ self.get_io().write_error(
+ "No composer.json found in the current directory, disabling \"latest\" option",
+ );
+ input
+ .borrow_mut()
+ .set_option("latest", PhpMixed::Bool(false));
+ }
+
+ let package_filter: Option<String> = input
+ .borrow()
+ .get_argument("package")?
+ .as_string()
+ .map(|s| s.to_string());
+
+ // show single package or single version
+ if let Some(ref pkg) = single_package {
+ versions_map.insert(pkg.get_pretty_version(), pkg.get_version());
+ } else if let Some(ref pf) = package_filter
+ && !pf.contains('*')
+ {
+ let (matched_package, vers) = self.get_package(
+ &installed_repo,
+ &repos,
+ pf,
+ input.borrow().get_argument("version")?,
+ )?;
+
+ if let Some(ref pkg) = matched_package
+ && input.borrow().get_option("direct")?.as_bool() == Some(true)
+ && !in_array_strict(
+ pkg.get_name(),
+ &self
+ .get_root_requires()
+ .into_iter()
+ .map(PhpMixed::String)
+ .collect::<Vec<_>>(),
+ )
+ {
+ return Err(InvalidArgumentException {
+ message: format!(
+ "Package \"{}\" is installed but not a direct dependent of the root package.",
+ pkg.get_name()
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ if matched_package.is_none() {
+ let options = input.borrow().get_options();
+ let mut hint = String::new();
+ if input.borrow().get_option("locked")?.as_bool() == Some(true) {
+ hint.push_str(" in lock file");
+ }
+ if let Some(working_dir) = options.get("working-dir").filter(|v| !v.is_null()) {
+ hint.push_str(&format!(
+ " in {}/composer.json",
+ working_dir.as_string().unwrap_or("")
+ ));
+ }
+ if PlatformRepository::is_platform_package(pf)
+ && input.borrow().get_option("platform")?.as_bool() != Some(true)
+ {
+ hint.push_str(", try using --platform (-p) to show platform packages");
+ }
+ if input.borrow().get_option("all")?.as_bool() != Some(true)
+ && input.borrow().get_option("available")?.as_bool() != Some(true)
+ {
+ hint.push_str(", try using --available (-a) to show all available packages");
+ }
+
+ return Err(InvalidArgumentException {
+ message: format!("Package \"{}\" not found{}.", pf, hint),
+ code: 0,
+ }
+ .into());
+ }
+ single_package = matched_package;
+ versions_map = vers;
+ }
+
+ if let Some(ref package) = single_package {
+ // assert(isset($versions));
+
+ let mut exit_code: i64 = 0;
+ if input.borrow().get_option("tree")?.as_bool() == Some(true) {
+ let array_tree =
+ self.generate_package_tree(package.clone().into(), &installed_repo, &repos);
+
+ if format == "json" {
+ let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new();
+ wrapper.insert(
+ "installed".to_string(),
+ PhpMixed::List(vec![PhpMixed::Array(array_tree.into_iter().collect())]),
+ );
+ self.get_io().write(&JsonFile::encode(&PhpMixed::Array(
+ wrapper.into_iter().collect(),
+ ))?);
+ } else {
+ self.display_package_tree(vec![array_tree]);
+ }
+
+ return Ok(exit_code);
+ }
+
+ let mut latest_package: Option<crate::package::PackageInterfaceHandle> = None;
+ if input.borrow().get_option("latest")?.as_bool() == Some(true) {
+ latest_package = self.find_latest_package(
+ package.clone().into(),
+ composer.as_ref().unwrap(),
+ &platform_repo,
+ input
+ .borrow()
+ .get_option("major-only")?
+ .as_bool()
+ .unwrap_or(false),
+ input
+ .borrow()
+ .get_option("minor-only")?
+ .as_bool()
+ .unwrap_or(false),
+ input
+ .borrow()
+ .get_option("patch-only")?
+ .as_bool()
+ .unwrap_or(false),
+ platform_req_filter.clone(),
+ )?;
+ }
+ if input.borrow().get_option("outdated")?.as_bool() == Some(true)
+ && input.borrow().get_option("strict")?.as_bool() == Some(true)
+ && latest_package.is_some()
+ && latest_package
+ .as_ref()
+ .unwrap()
+ .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev)
+ != package
+ .get_full_pretty_version(true, crate::package::DisplayMode::SourceRefIfDev)
+ && (latest_package
+ .as_ref()
+ .unwrap()
+ .as_complete()
+ .is_none_or(|c| !c.is_abandoned()))
+ {
+ exit_code = 1;
+ }
+ if input.borrow().get_option("path")?.as_bool() == Some(true) {
+ self.get_io().write_no_newline(&package.get_name());
+ let path = {
+ let composer_ref = composer.as_ref().unwrap();
+ composer_ref
+ .borrow_partial()
+ .get_installation_manager()
+ .borrow_mut()
+ .get_install_path(package.clone().into())
+ };
+ if let Some(path) = path {
+ let real = realpath(&path).unwrap_or_default();
+ let trimmed = real.split(['\r', '\n']).next().unwrap_or("");
+ self.get_io().write(&format!(" {}", trimmed));
+ } else {
+ self.get_io().write(" null");
+ }
+
+ return Ok(exit_code);
+ }
+
+ if format == "json" {
+ self.print_package_info_as_json(
+ package.clone(),
+ &versions_map,
+ &mut *installed_repo.borrow_mut(),
+ latest_package,
+ )?;
+ } else {
+ self.print_package_info(
+ package.clone(),
+ &versions_map,
+ &mut *installed_repo.borrow_mut(),
+ latest_package,
+ )?;
+ }
+
+ return Ok(exit_code);
+ }
+
+ // show tree view if requested
+ if input.borrow().get_option("tree")?.as_bool() == Some(true) {
+ let root_requires = self.get_root_requires();
+ let mut packages = installed_repo.get_packages()?;
+ packages.sort_by(|a, b| {
+ let sa: String = a.to_string();
+ let sb: String = b.to_string();
+ sa.cmp(&sb)
+ });
+ let mut array_tree: Vec<IndexMap<String, PhpMixed>> = Vec::new();
+ for package in packages.iter() {
+ if in_array_strict(
+ package.get_name(),
+ &root_requires
+ .iter()
+ .map(|s| PhpMixed::String(s.clone()))
+ .collect::<Vec<_>>(),
+ ) {
+ array_tree.push(self.generate_package_tree(
+ package.clone(),
+ &installed_repo,
+ &repos,
+ ));
+ }
+ }
+
+ if format == "json" {
+ let mut wrapper: IndexMap<String, PhpMixed> = IndexMap::new();
+ wrapper.insert(
+ "installed".to_string(),
+ PhpMixed::List(
+ array_tree
+ .into_iter()
+ .map(|m| PhpMixed::Array(m.into_iter().collect()))
+ .collect(),
+ ),
+ );
+ self.get_io().write(&JsonFile::encode(&PhpMixed::Array(
+ wrapper.into_iter().collect(),
+ ))?);
+ } else {
+ self.display_package_tree(array_tree);
+ }
+
+ return Ok(0);
+ }
+
+ // list packages
+ let mut packages: IndexMap<String, IndexMap<String, PackageOrName>> = IndexMap::new();
+ let mut package_filter_regex: Option<String> = None;
+ if let Some(ref pf) = package_filter {
+ let escaped = shirabe_php_shim::preg_quote(pf, None);
+ package_filter_regex = Some(format!("{{^{}$}}i", escaped.replace("\\*", ".*?")));
+ }
+
+ let mut package_list_filter: Option<Vec<String>> = None;
+ if input.borrow().get_option("direct")?.as_bool() == Some(true) {
+ package_list_filter = Some(self.get_root_requires());
+ }
+
+ if input.borrow().get_option("path")?.as_bool() == Some(true) && composer.is_none() {
+ self.get_io().write_error(
+ "No composer.json found in the current directory, disabling \"path\" option",
+ );
+ input.borrow_mut().set_option("path", PhpMixed::Bool(false));
+ }
+
+ for repo in RepositoryUtils::flatten_repositories(repos, true) {
+ let r#type = if Self::same_repository(&repo, &platform_repo) {
+ "platform"
+ } else if locked_repo
+ .as_ref()
+ .is_some_and(|lr| Self::same_repository(&repo, lr))
+ {
+ "locked"
+ } else if Self::same_repository(&repo, &installed_repo)
+ || installed_repo
+ .borrow()
+ .as_any()
+ .downcast_ref::<InstalledRepository>()
+ .is_some_and(|ir| ir.get_repositories().iter().any(|r| r.ptr_eq(&repo)))
+ {
+ "installed"
+ } else {
+ "available"
+ };
+ let type_owned = r#type.to_string();
+ 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_default()
+ .insert(name.clone(), PackageOrName::Name(name));
+ }
+ } else {
+ for package in repo.get_packages()? {
+ let existing = packages
+ .get(&type_owned)
+ .and_then(|m| m.get(&package.get_name()));
+ let need_replace = match existing {
+ None => true,
+ Some(PackageOrName::Name(_)) => true,
+ Some(PackageOrName::Pkg(existing)) => {
+ version_compare(&existing.get_version(), &package.get_version(), "<")
+ }
+ };
+ if need_replace {
+ let mut p: crate::package::PackageInterfaceHandle = package.clone();
+ while let Some(alias) = p.as_alias() {
+ p = alias.get_alias_of().into();
+ }
+ let matches_filter = match &package_filter_regex {
+ None => true,
+ Some(r) => Preg::is_match(r, &p.get_name()),
+ };
+ if matches_filter {
+ let matches_list = match &package_list_filter {
+ None => true,
+ Some(list) => in_array_strict(
+ p.get_name(),
+ &list
+ .iter()
+ .map(|s| PhpMixed::String(s.clone()))
+ .collect::<Vec<_>>(),
+ ),
+ };
+ if matches_list {
+ packages
+ .entry(type_owned.clone())
+ .or_default()
+ .insert(p.get_name(), PackageOrName::Pkg(p));
+ }
+ }
+ }
+ }
+ if Self::same_repository(&repo, &platform_repo) {
+ for (name, p) in platform_repo.borrow().get_disabled_packages() {
+ packages
+ .entry(type_owned.clone())
+ .or_default()
+ .insert(name.clone(), PackageOrName::Pkg(p.clone().into()));
+ }
+ }
+ }
+ }
+
+ let show_all_types = input.borrow().get_option("all")?.as_bool() == Some(true);
+ let show_latest = input.borrow().get_option("latest")?.as_bool() == Some(true);
+ let show_major_only = input.borrow().get_option("major-only")?.as_bool() == Some(true);
+ let show_minor_only = input.borrow().get_option("minor-only")?.as_bool() == Some(true);
+ let show_patch_only = input.borrow().get_option("patch-only")?.as_bool() == Some(true);
+ let ignored_packages_regex = base_package::package_names_to_regexp(
+ &input
+ .borrow()
+ .get_option("ignore")?
+ .as_list()
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(strtolower))
+ .collect::<Vec<_>>()
+ })
+ .unwrap_or_default(),
+ "{^(?:%s)$}iD",
+ );
+ let indent = if show_all_types { " " } else { "" };
+ let mut latest_packages: IndexMap<String, crate::package::PackageInterfaceHandle> =
+ IndexMap::new();
+ let mut exit_code: i64 = 0;
+ let mut view_data: IndexMap<String, Vec<IndexMap<String, PhpMixed>>> = IndexMap::new();
+ let mut view_meta_data: IndexMap<String, ViewMetaData> = IndexMap::new();
+
+ let mut write_version = false;
+ let mut write_description = false;
+
+ let type_order: Vec<(&str, bool)> = vec![
+ ("platform", true),
+ ("locked", true),
+ ("available", false),
+ ("installed", true),
+ ];
+ for (r#type, show_version) in type_order.iter() {
+ if let Some(type_packages) = packages.get_mut(*r#type) {
+ type_packages.sort_keys();
+
+ let mut name_length: usize = 0;
+ let mut version_length: usize = 0;
+ let mut latest_length: usize = 0;
+ let mut release_date_length: usize = 0;
+
+ if show_latest && *show_version {
+ for package_or_name in type_packages.values() {
+ if let PackageOrName::Pkg(package) = package_or_name
+ && !Preg::is_match(&ignored_packages_regex, &package.get_pretty_name())
+ {
+ let latest = self.find_latest_package(
+ package.clone(),
+ composer.as_ref().unwrap(),
+ &platform_repo,
+ show_major_only,
+ show_minor_only,
+ show_patch_only,
+ platform_req_filter.clone(),
+ )?;
+ if latest.is_none() {
+ continue;
+ }
+
+ latest_packages.insert(package.get_pretty_name(), latest.unwrap());
+ }
+ }
+ }
+
+ let write_path = input.borrow().get_option("name-only")?.as_bool() != Some(true)
+ && input.borrow().get_option("path")?.as_bool() == Some(true);
+ write_version = input.borrow().get_option("name-only")?.as_bool() != Some(true)
+ && input.borrow().get_option("path")?.as_bool() != Some(true)
+ && *show_version;
+ let write_latest = write_version && show_latest;
+ write_description = input.borrow().get_option("name-only")?.as_bool() != Some(true)
+ && input.borrow().get_option("path")?.as_bool() != Some(true);
+ let write_release_date = write_latest
+ && (input.borrow().get_option("sort-by-age")?.as_bool() == Some(true)
+ || format == "json");
+
+ let mut has_outdated_packages = false;
+
+ if input.borrow().get_option("sort-by-age")?.as_bool() == Some(true) {
+ type_packages.sort_by(|_ka, a, _kb, b| match (a, b) {
+ (PackageOrName::Pkg(a), PackageOrName::Pkg(b)) => {
+ a.get_release_date().cmp(&b.get_release_date())
+ }
+ _ => std::cmp::Ordering::Equal,
+ });
+ }
+
+ let mut view_type: Vec<IndexMap<String, PhpMixed>> = Vec::new();
+ for package_or_name in type_packages.values() {
+ let mut package_view_data: IndexMap<String, PhpMixed> = IndexMap::new();
+ if let PackageOrName::Pkg(package) = package_or_name {
+ let latest_package = if show_latest
+ && latest_packages.contains_key(&package.get_pretty_name())
+ {
+ latest_packages.get(&package.get_pretty_name())
+ } else {
+ None
+ };
+
+ // Determine if Composer is checking outdated dependencies and if current package should trigger non-default exit code
+ let mut package_is_up_to_date = if let Some(latest) = latest_package {
+ latest.get_full_pretty_version(
+ true,
+ crate::package::DisplayMode::SourceRefIfDev,
+ ) == package.get_full_pretty_version(
+ true,
+ crate::package::DisplayMode::SourceRefIfDev,
+ ) && latest.as_complete().is_none_or(|c| !c.is_abandoned())
+ } else {
+ false
+ };
+ // When using --major-only, and no bigger version than current major is found then it is considered up to date
+ package_is_up_to_date =
+ package_is_up_to_date || (latest_package.is_none() && show_major_only);
+ let package_is_ignored =
+ Preg::is_match(&ignored_packages_regex, &package.get_pretty_name());
+ if input.borrow().get_option("outdated")?.as_bool() == Some(true)
+ && (package_is_up_to_date || package_is_ignored)
+ {
+ continue;
+ }
+
+ if input.borrow().get_option("outdated")?.as_bool() == Some(true)
+ || input.borrow().get_option("strict")?.as_bool() == Some(true)
+ {
+ has_outdated_packages = true;
+ }
+
+ package_view_data.insert(
+ "name".to_string(),
+ PhpMixed::String(package.get_pretty_name()),
+ );
+ package_view_data.insert(
+ "direct-dependency".to_string(),
+ PhpMixed::Bool(in_array_strict(
+ package.get_name(),
+ &self
+ .get_root_requires()
+ .into_iter()
+ .map(PhpMixed::String)
+ .collect::<Vec<_>>(),
+ )),
+ );
+ if format != "json"
+ || input.borrow().get_option("name-only")?.as_bool() != Some(true)
+ {
+ package_view_data.insert(
+ "homepage".to_string(),
+ match package.as_complete() {
+ Some(c) => match c.get_homepage() {
+ Some(h) => PhpMixed::String(h),
+ None => PhpMixed::Null,
+ },
+ None => PhpMixed::Null,
+ },
+ );
+ package_view_data.insert(
+ "source".to_string(),
+ match PackageInfo::get_view_source_url(package.clone()) {
+ Some(s) => PhpMixed::String(s),
+ None => PhpMixed::Null,
+ },
+ );
+ }
+ name_length = name_length.max(package.get_pretty_name().len());
+ if write_version {
+ let mut version_str = package.get_full_pretty_version(
+ true,
+ crate::package::DisplayMode::SourceRefIfDev,
+ );
+ if format == "text" {
+ version_str = version_str.trim_start_matches('v').to_string();
+ }
+ version_length = version_length.max(version_str.len());
+ package_view_data
+ .insert("version".to_string(), PhpMixed::String(version_str));
+ }
+ if write_release_date {
+ if let Some(release_date) = package.get_release_date() {
+ let mut age = self
+ .get_relative_time(&release_date)
+ .replace(" ago", " old");
+ if !age.contains(" old") {
+ age = format!("from {}", age);
+ }
+ release_date_length = release_date_length.max(age.len());
+ package_view_data
+ .insert("release-age".to_string(), PhpMixed::String(age));
+ package_view_data.insert(
+ "release-date".to_string(),
+ PhpMixed::String(release_date.format(DATE_ATOM).to_string()),
+ );
+ } else {
+ package_view_data.insert(
+ "release-age".to_string(),
+ PhpMixed::String(String::new()),
+ );
+ package_view_data.insert(
+ "release-date".to_string(),
+ PhpMixed::String(String::new()),
+ );
+ }
+ }
+ if write_latest && let Some(latest) = latest_package {
+ let mut latest_version_str = latest.get_full_pretty_version(
+ true,
+ crate::package::DisplayMode::SourceRefIfDev,
+ );
+ if format == "text" {
+ latest_version_str =
+ latest_version_str.trim_start_matches('v').to_string();
+ }
+ let update_status =
+ Self::get_update_status(latest.clone(), package.clone())?;
+ latest_length = latest_length.max(latest_version_str.len());
+ package_view_data
+ .insert("latest".to_string(), PhpMixed::String(latest_version_str));
+ package_view_data.insert(
+ "latest-status".to_string(),
+ PhpMixed::String(update_status),
+ );
+
+ if let Some(rd) = latest.get_release_date() {
+ package_view_data.insert(
+ "latest-release-date".to_string(),
+ PhpMixed::String(rd.format(DATE_ATOM).to_string()),
+ );
+ } else {
+ package_view_data.insert(
+ "latest-release-date".to_string(),
+ PhpMixed::String(String::new()),
+ );
+ }
+ } else if write_latest {
+ package_view_data.insert(
+ "latest".to_string(),
+ PhpMixed::String("[none matched]".to_string()),
+ );
+ package_view_data.insert(
+ "latest-status".to_string(),
+ PhpMixed::String("up-to-date".to_string()),
+ );
+ latest_length = latest_length.max("[none matched]".len());
+ }
+ if write_description && let Some(c) = package.as_complete() {
+ package_view_data.insert(
+ "description".to_string(),
+ match c.get_description() {
+ Some(d) => PhpMixed::String(d),
+ None => PhpMixed::Null,
+ },
+ );
+ }
+ if write_path {
+ let installation_manager = composer
+ .as_ref()
+ .unwrap()
+ .borrow_partial()
+ .get_installation_manager();
+ let path: Option<String> = installation_manager
+ .borrow_mut()
+ .get_install_path(package.clone());
+ if let Some(p) = path {
+ let r = realpath(&p).unwrap_or_default();
+ let trimmed = r.split(['\r', '\n']).next().unwrap_or("");
+ package_view_data.insert(
+ "path".to_string(),
+ PhpMixed::String(trimmed.to_string()),
+ );
+ } else {
+ package_view_data.insert("path".to_string(), PhpMixed::Null);
+ }
+ }
+
+ let mut package_is_abandoned: PhpMixed = PhpMixed::Bool(false);
+ if let Some(latest) = latest_package
+ && let Some(c) = latest.as_complete()
+ && c.is_abandoned()
+ {
+ let replacement_package_name = c.get_replacement_package();
+ let replacement = if let Some(ref rp) = replacement_package_name {
+ format!("Use {} instead", rp)
+ } else {
+ "No replacement was suggested".to_string()
+ };
+ let package_warning = format!(
+ "Package {} is abandoned, you should avoid using it. {}.",
+ package.get_pretty_name(),
+ replacement
+ );
+ package_view_data
+ .insert("warning".to_string(), PhpMixed::String(package_warning));
+ package_is_abandoned = match replacement_package_name {
+ Some(rp) => PhpMixed::String(rp),
+ None => PhpMixed::Bool(true),
+ };
+ }
+
+ package_view_data.insert("abandoned".to_string(), package_is_abandoned);
+ } else if let PackageOrName::Name(name) = package_or_name {
+ package_view_data
+ .insert("name".to_string(), PhpMixed::String(name.clone()));
+ name_length = name_length.max(name.len());
+ }
+ view_type.push(package_view_data);
+ }
+ view_data.insert(r#type.to_string(), view_type);
+ view_meta_data.insert(
+ r#type.to_string(),
+ ViewMetaData {
+ name_length,
+ version_length,
+ latest_length,
+ release_date_length,
+ write_latest,
+ write_release_date,
+ },
+ );
+ if input.borrow().get_option("strict")?.as_bool() == Some(true)
+ && has_outdated_packages
+ {
+ exit_code = 1;
+ break;
+ }
+ }
+ }
+
+ if format == "json" {
+ let mut json_map: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (k, v) in view_data.iter() {
+ json_map.insert(
+ k.clone(),
+ PhpMixed::List(
+ v.iter()
+ .map(|m| {
+ PhpMixed::Array(
+ m.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
+ )
+ })
+ .collect(),
+ ),
+ );
+ }
+ let io = self.get_io();
+ io.write(&JsonFile::encode(&PhpMixed::Array(
+ json_map.into_iter().collect(),
+ ))?);
+ } else {
+ if input.borrow().get_option("latest")?.as_bool() == Some(true)
+ && view_data.values().any(|v| !v.is_empty())
+ {
+ let io = self.get_io();
+ if !io.is_decorated() {
+ io.write_error("Legend:");
+ io.write_error("! patch or minor release available - update recommended");
+ io.write_error("~ major release available - update possible");
+ if input.borrow().get_option("outdated")?.as_bool() != Some(true) {
+ io.write_error("= up to date version");
+ }
+ } else {
+ io.write_error("<info>Color legend:</info>");
+ io.write_error("- <highlight>patch or minor</highlight> release available - update recommended");
+ io.write_error(
+ "- <comment>major</comment> release available - update possible",
+ );
+ if input.borrow().get_option("outdated")?.as_bool() != Some(true) {
+ io.write_error("- <info>up to date</info> version");
+ }
+ }
+ }
+
+ let width = self.get_terminal_width();
+
+ for (r#type, packages) in view_data.iter() {
+ let meta = match view_meta_data.get(r#type) {
+ Some(m) => m.clone(),
+ None => continue,
+ };
+ let name_length = meta.name_length;
+ let version_length = meta.version_length;
+ let mut latest_length = meta.latest_length;
+ let release_date_length = meta.release_date_length;
+ let write_latest = meta.write_latest;
+ let write_release_date = meta.write_release_date;
+
+ let width_usize = width as usize;
+ let version_fits = name_length + version_length + 3 <= width_usize;
+ let latest_fits = name_length + version_length + latest_length + 3 <= width_usize;
+ let release_date_fits =
+ name_length + version_length + latest_length + release_date_length + 3
+ <= width_usize;
+ let description_fits =
+ name_length + version_length + latest_length + release_date_length + 24
+ <= width_usize;
+
+ if latest_fits && !self.get_io().is_decorated() {
+ latest_length += 2;
+ }
+
+ if show_all_types {
+ if r#type == "available" {
+ self.get_io()
+ .write(&format!("<comment>{}</comment>:", r#type));
+ } else {
+ self.get_io().write(&format!("<info>{}</info>:", r#type));
+ }
+ }
+
+ if write_latest && input.borrow().get_option("direct")?.as_bool() != Some(true) {
+ let mut direct_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new();
+ let mut transitive_deps: Vec<IndexMap<String, PhpMixed>> = Vec::new();
+ for pkg in packages.iter() {
+ let is_direct = pkg
+ .get("direct-dependency")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+ if is_direct {
+ direct_deps.push(pkg.clone());
+ } else {
+ transitive_deps.push(pkg.clone());
+ }
+ }
+
+ self.get_io().write_error("");
+ self.get_io()
+ .write_error("<info>Direct dependencies required in composer.json:</>");
+ if !direct_deps.is_empty() {
+ self.print_packages(
+ &direct_deps,
+ indent,
+ write_version && version_fits,
+ latest_fits,
+ write_description && description_fits,
+ width_usize,
+ version_length,
+ name_length,
+ latest_length,
+ write_release_date && release_date_fits,
+ release_date_length,
+ );
+ } else {
+ self.get_io().write_error("Everything up to date");
+ }
+ self.get_io().write_error("");
+ self.get_io().write_error(
+ "<info>Transitive dependencies not required in composer.json:</>",
+ );
+ if !transitive_deps.is_empty() {
+ self.print_packages(
+ &transitive_deps,
+ indent,
+ write_version && version_fits,
+ latest_fits,
+ write_description && description_fits,
+ width_usize,
+ version_length,
+ name_length,
+ latest_length,
+ write_release_date && release_date_fits,
+ release_date_length,
+ );
+ } else {
+ self.get_io().write_error("Everything up to date");
+ }
+ } else {
+ if write_latest && packages.is_empty() {
+ self.get_io()
+ .write_error("All your direct dependencies are up to date");
+ } else {
+ self.print_packages(
+ packages,
+ indent,
+ write_version && version_fits,
+ write_latest && latest_fits,
+ write_description && description_fits,
+ width_usize,
+ version_length,
+ name_length,
+ latest_length,
+ write_release_date && release_date_fits,
+ release_date_length,
+ );
+ }
+ }
+
+ if show_all_types {
+ self.get_io().write("");
+ }
+ }
+ }
+
+ Ok(exit_code)
+ }
+
+ fn initialize(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<()> {
+ base_command_initialize(self, input, output)
+ }
+
+ fn complete(
+ &self,
+ input: &shirabe_external_packages::symfony::console::completion::completion_input::CompletionInput,
+ suggestions: &mut shirabe_external_packages::symfony::console::completion::completion_suggestions::CompletionSuggestions,
+ ) -> anyhow::Result<()> {
+ crate::command::base_command::base_command_complete(self, input, suggestions)
+ }
+
+ shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
+}
+
+impl BaseCommand for ShowCommand {
+ fn base_command_data(&self) -> &crate::command::BaseCommandData {
+ &self.base_command_data
+ }
+
+ crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
+}
+
#[derive(Debug)]
pub enum PackageOrName {
Pkg(crate::package::PackageInterfaceHandle),
diff --git a/crates/shirabe/src/command/update_command.rs b/crates/shirabe/src/command/update_command.rs
index 1885e8eb..c928552a 100644
--- a/crates/shirabe/src/command/update_command.rs
+++ b/crates/shirabe/src/command/update_command.rs
@@ -62,6 +62,202 @@ impl UpdateCommand {
.expect("UpdateCommand::configure uses static, valid metadata");
command
}
+
+ fn get_packages_interactively(
+ &self,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ composer: &PartialComposerHandle,
+ packages: Vec<String>,
+ ) -> anyhow::Result<Vec<String>> {
+ if !input.borrow().is_interactive() {
+ return Err(InvalidArgumentException {
+ message: "--interactive cannot be used in non-interactive terminals.".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let composer_ref = crate::composer::composer_full(composer);
+ let platform_req_filter = self.get_platform_requirement_filter(input);
+ let stability_flags = composer_ref.get_package().get_stability_flags();
+ let requires = array_merge_map(
+ composer_ref.get_package().get_requires(),
+ composer_ref.get_package().get_dev_requires(),
+ );
+
+ let filter: Option<String> = if !packages.is_empty() {
+ Some(base_package::package_names_to_regexp(&packages, "%s"))
+ } else {
+ None
+ };
+
+ io.write_error3(
+ "<info>Loading packages that can be updated...</info>",
+ true,
+ io_interface::NORMAL,
+ );
+ let mut autocompleter_values: IndexMap<String, String> = IndexMap::new();
+ let installed_packages: Vec<crate::package::PackageInterfaceHandle> =
+ if composer_ref.get_locker().borrow_mut().is_locked() {
+ let locked_repo = composer_ref
+ .get_locker()
+ .borrow_mut()
+ .get_locked_repository(true)?;
+ locked_repo.borrow_mut().get_canonical_packages()?
+ } else {
+ composer_ref
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository()
+ .get_packages()?
+ };
+ let mut version_selector = self.create_version_selector(composer)?;
+ for package in &installed_packages {
+ if let Some(filter) = &filter
+ && !Preg::is_match(filter, &package.get_name())
+ {
+ continue;
+ }
+ let current_version = package.get_pretty_version();
+ let constraint = requires
+ .get(&package.get_name())
+ .map(|link| link.get_pretty_constraint());
+ let stability = match stability_flags.get(&package.get_name()) {
+ Some(flag) => base_package::STABILITIES
+ .iter()
+ .find(|&(_, v)| v == flag)
+ .map(|(k, _)| k.to_string())
+ .unwrap_or_default(),
+ None => composer_ref.get_package().get_minimum_stability(),
+ };
+ let latest_version = version_selector.find_best_candidate(
+ &package.get_name(),
+ constraint,
+ &stability,
+ None,
+ 0,
+ None,
+ ShowWarnings::Always,
+ )?;
+ let _ = &platform_req_filter;
+ if let Some(latest) = latest_version
+ && (package.get_version() != latest.get_version() || latest.is_dev())
+ {
+ autocompleter_values.insert(
+ package.get_name(),
+ format!(
+ "<comment>{}</comment> => <comment>{}</comment>",
+ current_version,
+ latest.get_pretty_version(),
+ ),
+ );
+ }
+ }
+ if installed_packages.is_empty() {
+ for (req, _constraint) in &requires {
+ if PlatformRepository::is_platform_package(req) {
+ continue;
+ }
+ autocompleter_values.insert(req.to_string(), String::new());
+ }
+ }
+
+ if autocompleter_values.is_empty() {
+ return Err(RuntimeException {
+ message: "Could not find any package with new versions available".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let select_result = io.select(
+ "Select packages: (Select more than one value separated by comma) ".to_string(),
+ PhpMixed::Array(
+ autocompleter_values
+ .iter()
+ .map(|(k, v)| (k.clone(), PhpMixed::String(v.clone())))
+ .collect(),
+ ),
+ PhpMixed::Bool(false),
+ PhpMixed::Int(1),
+ "No package named \"%s\" is installed.".to_string(),
+ true,
+ )?;
+ let packages: Vec<String> = match select_result {
+ PhpMixed::List(l) => l
+ .into_iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect(),
+ _ => Vec::new(),
+ };
+
+ let mut table = Table::new(output);
+ table.set_headers(vec!["Selected packages".into()]);
+ for package in &packages {
+ table.add_row(PhpMixed::List(vec![PhpMixed::String(package.clone())]).into());
+ }
+ table.render();
+
+ if io.ask_confirmation(
+ format!(
+ "Would you like to continue and update the above package{} [<comment>yes</comment>]? ",
+ if 1 == packages.len() { "" } else { "s" },
+ ),
+ true,
+ ) {
+ return Ok(packages);
+ }
+
+ Err(RuntimeException {
+ message: "Installation aborted.".to_string(),
+ code: 0,
+ }
+ .into())
+ }
+
+ fn create_version_selector(
+ &self,
+ composer: &PartialComposerHandle,
+ ) -> anyhow::Result<VersionSelector> {
+ let composer = crate::composer::composer_full(composer);
+ let root_aliases: Vec<crate::repository::RootAliasInput> = composer
+ .get_package()
+ .get_aliases()
+ .into_iter()
+ .map(|alias| crate::repository::RootAliasInput {
+ package: alias.get("package").cloned().unwrap_or_default(),
+ version: alias.get("version").cloned().unwrap_or_default(),
+ alias: alias.get("alias").cloned().unwrap_or_default(),
+ alias_normalized: alias.get("alias_normalized").cloned().unwrap_or_default(),
+ })
+ .collect();
+ let mut repository_set = RepositorySet::new(
+ &composer.get_package().get_minimum_stability(),
+ composer.get_package().get_stability_flags(),
+ root_aliases,
+ composer.get_package().get_references(),
+ IndexMap::new(),
+ IndexMap::new(),
+ );
+ let repositories: Vec<crate::repository::RepositoryInterfaceHandle> = composer
+ .get_repository_manager()
+ .borrow()
+ .get_repositories()
+ .iter()
+ .filter(|repository| !repository.is::<PlatformRepository>())
+ .cloned()
+ .collect();
+ repository_set.add_repository(crate::repository::RepositoryInterfaceHandle::new(
+ CompositeRepository::new(repositories),
+ ))?;
+
+ VersionSelector::new(
+ std::rc::Rc::new(std::cell::RefCell::new(repository_set)),
+ None,
+ )
+ }
}
impl Command for UpdateCommand {
@@ -591,201 +787,3 @@ impl BaseCommand for UpdateCommand {
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
-
-impl UpdateCommand {
- fn get_packages_interactively(
- &self,
- io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- composer: &PartialComposerHandle,
- packages: Vec<String>,
- ) -> anyhow::Result<Vec<String>> {
- if !input.borrow().is_interactive() {
- return Err(InvalidArgumentException {
- message: "--interactive cannot be used in non-interactive terminals.".to_string(),
- code: 0,
- }
- .into());
- }
-
- let composer_ref = crate::composer::composer_full(composer);
- let platform_req_filter = self.get_platform_requirement_filter(input);
- let stability_flags = composer_ref.get_package().get_stability_flags();
- let requires = array_merge_map(
- composer_ref.get_package().get_requires(),
- composer_ref.get_package().get_dev_requires(),
- );
-
- let filter: Option<String> = if !packages.is_empty() {
- Some(base_package::package_names_to_regexp(&packages, "%s"))
- } else {
- None
- };
-
- io.write_error3(
- "<info>Loading packages that can be updated...</info>",
- true,
- io_interface::NORMAL,
- );
- let mut autocompleter_values: IndexMap<String, String> = IndexMap::new();
- let installed_packages: Vec<crate::package::PackageInterfaceHandle> =
- if composer_ref.get_locker().borrow_mut().is_locked() {
- let locked_repo = composer_ref
- .get_locker()
- .borrow_mut()
- .get_locked_repository(true)?;
- locked_repo.borrow_mut().get_canonical_packages()?
- } else {
- composer_ref
- .get_repository_manager()
- .borrow()
- .get_local_repository()
- .get_packages()?
- };
- let mut version_selector = self.create_version_selector(composer)?;
- for package in &installed_packages {
- if let Some(filter) = &filter
- && !Preg::is_match(filter, &package.get_name())
- {
- continue;
- }
- let current_version = package.get_pretty_version();
- let constraint = requires
- .get(&package.get_name())
- .map(|link| link.get_pretty_constraint());
- let stability = match stability_flags.get(&package.get_name()) {
- Some(flag) => base_package::STABILITIES
- .iter()
- .find(|&(_, v)| v == flag)
- .map(|(k, _)| k.to_string())
- .unwrap_or_default(),
- None => composer_ref.get_package().get_minimum_stability(),
- };
- let latest_version = version_selector.find_best_candidate(
- &package.get_name(),
- constraint,
- &stability,
- None,
- 0,
- None,
- ShowWarnings::Always,
- )?;
- let _ = &platform_req_filter;
- if let Some(latest) = latest_version
- && (package.get_version() != latest.get_version() || latest.is_dev())
- {
- autocompleter_values.insert(
- package.get_name(),
- format!(
- "<comment>{}</comment> => <comment>{}</comment>",
- current_version,
- latest.get_pretty_version(),
- ),
- );
- }
- }
- if installed_packages.is_empty() {
- for (req, _constraint) in &requires {
- if PlatformRepository::is_platform_package(req) {
- continue;
- }
- autocompleter_values.insert(req.to_string(), String::new());
- }
- }
-
- if autocompleter_values.is_empty() {
- return Err(RuntimeException {
- message: "Could not find any package with new versions available".to_string(),
- code: 0,
- }
- .into());
- }
-
- let select_result = io.select(
- "Select packages: (Select more than one value separated by comma) ".to_string(),
- PhpMixed::Array(
- autocompleter_values
- .iter()
- .map(|(k, v)| (k.clone(), PhpMixed::String(v.clone())))
- .collect(),
- ),
- PhpMixed::Bool(false),
- PhpMixed::Int(1),
- "No package named \"%s\" is installed.".to_string(),
- true,
- )?;
- let packages: Vec<String> = match select_result {
- PhpMixed::List(l) => l
- .into_iter()
- .filter_map(|v| v.as_string().map(|s| s.to_string()))
- .collect(),
- _ => Vec::new(),
- };
-
- let mut table = Table::new(output);
- table.set_headers(vec!["Selected packages".into()]);
- for package in &packages {
- table.add_row(PhpMixed::List(vec![PhpMixed::String(package.clone())]).into());
- }
- table.render();
-
- if io.ask_confirmation(
- format!(
- "Would you like to continue and update the above package{} [<comment>yes</comment>]? ",
- if 1 == packages.len() { "" } else { "s" },
- ),
- true,
- ) {
- return Ok(packages);
- }
-
- Err(RuntimeException {
- message: "Installation aborted.".to_string(),
- code: 0,
- }
- .into())
- }
-
- fn create_version_selector(
- &self,
- composer: &PartialComposerHandle,
- ) -> anyhow::Result<VersionSelector> {
- let composer = crate::composer::composer_full(composer);
- let root_aliases: Vec<crate::repository::RootAliasInput> = composer
- .get_package()
- .get_aliases()
- .into_iter()
- .map(|alias| crate::repository::RootAliasInput {
- package: alias.get("package").cloned().unwrap_or_default(),
- version: alias.get("version").cloned().unwrap_or_default(),
- alias: alias.get("alias").cloned().unwrap_or_default(),
- alias_normalized: alias.get("alias_normalized").cloned().unwrap_or_default(),
- })
- .collect();
- let mut repository_set = RepositorySet::new(
- &composer.get_package().get_minimum_stability(),
- composer.get_package().get_stability_flags(),
- root_aliases,
- composer.get_package().get_references(),
- IndexMap::new(),
- IndexMap::new(),
- );
- let repositories: Vec<crate::repository::RepositoryInterfaceHandle> = composer
- .get_repository_manager()
- .borrow()
- .get_repositories()
- .iter()
- .filter(|repository| !repository.is::<PlatformRepository>())
- .cloned()
- .collect();
- repository_set.add_repository(crate::repository::RepositoryInterfaceHandle::new(
- CompositeRepository::new(repositories),
- ))?;
-
- VersionSelector::new(
- std::rc::Rc::new(std::cell::RefCell::new(repository_set)),
- None,
- )
- }
-}
diff --git a/crates/shirabe/src/command/validate_command.rs b/crates/shirabe/src/command/validate_command.rs
index 0a89b71d..c663c868 100644
--- a/crates/shirabe/src/command/validate_command.rs
+++ b/crates/shirabe/src/command/validate_command.rs
@@ -41,6 +41,100 @@ impl ValidateCommand {
.expect("ValidateCommand::configure uses static, valid metadata");
command
}
+
+ #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")]
+ fn output_result(
+ &self,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ name: &str,
+ errors: &mut Vec<String>,
+ warnings: &mut Vec<String>,
+ check_publish: bool,
+ publish_errors: &mut Vec<String>,
+ check_lock: bool,
+ lock_errors: &mut Vec<String>,
+ print_schema_url: bool,
+ ) {
+ let mut do_print_schema_url = false;
+
+ if !errors.is_empty() {
+ io.write_error(&format!(
+ "<error>{} is invalid, the following errors/warnings were found:</error>",
+ name
+ ));
+ } else if !publish_errors.is_empty() && check_publish {
+ io.write_error(&format!(
+ "<info>{} is valid for simple usage with Composer but has</info>",
+ name
+ ));
+ io.write_error(
+ "<info>strict errors that make it unable to be published as a package</info>",
+ );
+ do_print_schema_url = print_schema_url;
+ } else if !warnings.is_empty() {
+ io.write_error(&format!(
+ "<info>{} is valid, but with a few warnings</info>",
+ name
+ ));
+ do_print_schema_url = print_schema_url;
+ } else if !lock_errors.is_empty() {
+ io.write(&format!(
+ "<info>{} is valid but your composer.lock has some {}</info>",
+ name,
+ if check_lock { "errors" } else { "warnings" }
+ ));
+ } else {
+ io.write(&format!("<info>{} is valid</info>", name));
+ }
+
+ if do_print_schema_url {
+ io.write_error("<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>");
+ }
+
+ if !errors.is_empty() {
+ *errors = errors.iter().map(|e| format!("- {}", e)).collect();
+ errors.insert(0, "# General errors".to_string());
+ }
+ if !warnings.is_empty() {
+ *warnings = warnings.iter().map(|w| format!("- {}", w)).collect();
+ warnings.insert(0, "# General warnings".to_string());
+ }
+
+ let mut extra_warnings: Vec<String> = vec![];
+
+ if !publish_errors.is_empty() && check_publish {
+ *publish_errors = publish_errors.iter().map(|e| format!("- {}", e)).collect();
+ publish_errors.insert(0, "# Publish errors".to_string());
+ errors.append(publish_errors);
+ }
+
+ if !lock_errors.is_empty() {
+ if check_lock {
+ lock_errors.insert(0, "# Lock file errors".to_string());
+ errors.append(lock_errors);
+ } else {
+ lock_errors.insert(0, "# Lock file warnings".to_string());
+ extra_warnings.append(lock_errors);
+ }
+ }
+
+ let all_warnings: Vec<String> = warnings.iter().cloned().chain(extra_warnings).collect();
+
+ for msg in errors.iter() {
+ if msg.starts_with('#') {
+ io.write_error(&format!("<error>{}</error>", msg));
+ } else {
+ io.write_error(msg);
+ }
+ }
+ for msg in &all_warnings {
+ if msg.starts_with('#') {
+ io.write_error(&format!("<warning>{}</warning>", msg));
+ } else {
+ io.write_error(msg);
+ }
+ }
+ }
}
impl Command for ValidateCommand {
@@ -328,99 +422,3 @@ impl BaseCommand for ValidateCommand {
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
-
-impl ValidateCommand {
- #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")]
- fn output_result(
- &self,
- io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- name: &str,
- errors: &mut Vec<String>,
- warnings: &mut Vec<String>,
- check_publish: bool,
- publish_errors: &mut Vec<String>,
- check_lock: bool,
- lock_errors: &mut Vec<String>,
- print_schema_url: bool,
- ) {
- let mut do_print_schema_url = false;
-
- if !errors.is_empty() {
- io.write_error(&format!(
- "<error>{} is invalid, the following errors/warnings were found:</error>",
- name
- ));
- } else if !publish_errors.is_empty() && check_publish {
- io.write_error(&format!(
- "<info>{} is valid for simple usage with Composer but has</info>",
- name
- ));
- io.write_error(
- "<info>strict errors that make it unable to be published as a package</info>",
- );
- do_print_schema_url = print_schema_url;
- } else if !warnings.is_empty() {
- io.write_error(&format!(
- "<info>{} is valid, but with a few warnings</info>",
- name
- ));
- do_print_schema_url = print_schema_url;
- } else if !lock_errors.is_empty() {
- io.write(&format!(
- "<info>{} is valid but your composer.lock has some {}</info>",
- name,
- if check_lock { "errors" } else { "warnings" }
- ));
- } else {
- io.write(&format!("<info>{} is valid</info>", name));
- }
-
- if do_print_schema_url {
- io.write_error("<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>");
- }
-
- if !errors.is_empty() {
- *errors = errors.iter().map(|e| format!("- {}", e)).collect();
- errors.insert(0, "# General errors".to_string());
- }
- if !warnings.is_empty() {
- *warnings = warnings.iter().map(|w| format!("- {}", w)).collect();
- warnings.insert(0, "# General warnings".to_string());
- }
-
- let mut extra_warnings: Vec<String> = vec![];
-
- if !publish_errors.is_empty() && check_publish {
- *publish_errors = publish_errors.iter().map(|e| format!("- {}", e)).collect();
- publish_errors.insert(0, "# Publish errors".to_string());
- errors.append(publish_errors);
- }
-
- if !lock_errors.is_empty() {
- if check_lock {
- lock_errors.insert(0, "# Lock file errors".to_string());
- errors.append(lock_errors);
- } else {
- lock_errors.insert(0, "# Lock file warnings".to_string());
- extra_warnings.append(lock_errors);
- }
- }
-
- let all_warnings: Vec<String> = warnings.iter().cloned().chain(extra_warnings).collect();
-
- for msg in errors.iter() {
- if msg.starts_with('#') {
- io.write_error(&format!("<error>{}</error>", msg));
- } else {
- io.write_error(msg);
- }
- }
- for msg in &all_warnings {
- if msg.starts_with('#') {
- io.write_error(&format!("<warning>{}</warning>", msg));
- } else {
- io.write_error(msg);
- }
- }
- }
-}
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index 4136a830..3ba6a324 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -2616,9 +2616,7 @@ impl ApplicationHandle {
.borrow_mut()
.get_composer(required, disable_plugins, disable_scripts)
}
-}
-impl ApplicationHandle {
/// Runs the current application (Symfony base; `parent::run`).
pub fn base_run(
&self,
diff --git a/crates/shirabe/src/dependency_resolver/request.rs b/crates/shirabe/src/dependency_resolver/request.rs
index 0a05f3e4..d78900c0 100644
--- a/crates/shirabe/src/dependency_resolver/request.rs
+++ b/crates/shirabe/src/dependency_resolver/request.rs
@@ -25,33 +25,7 @@ impl Request {
pub const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE: i64 =
UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE;
pub const UPDATE_LISTED_WITH_TRANSITIVE_DEPS: i64 = UPDATE_LISTED_WITH_TRANSITIVE_DEPS;
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum UpdateAllowTransitiveDeps {
- /// Corresponds to PHP false.
- False,
- /// \Composer\DependencyResolver\Request::UPDATE_ONLY_LISTED
- UpdateOnlyListed,
- /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE
- UpdateListedWithTransitiveDepsNoRootRequire,
- /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS
- UpdateListedWithTransitiveDeps,
-}
-
-#[derive(Debug)]
-pub struct Request {
- pub(crate) locked_repository: Option<LockArrayRepositoryHandle>,
- pub(crate) requires: IndexMap<String, AnyConstraint>,
- pub(crate) fixed_packages: IndexMap<String, BasePackageHandle>,
- pub(crate) locked_packages: IndexMap<String, BasePackageHandle>,
- pub(crate) fixed_locked_packages: IndexMap<String, BasePackageHandle>,
- pub(crate) update_allow_list: Vec<String>,
- pub(crate) update_allow_transitive_dependencies: UpdateAllowTransitiveDeps,
- restrict_packages: Option<Vec<String>>,
-}
-impl Request {
pub fn new(locked_repository: Option<LockArrayRepositoryHandle>) -> Self {
Self {
locked_repository,
@@ -245,3 +219,27 @@ impl Request {
self.restrict_packages.as_ref()
}
}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum UpdateAllowTransitiveDeps {
+ /// Corresponds to PHP false.
+ False,
+ /// \Composer\DependencyResolver\Request::UPDATE_ONLY_LISTED
+ UpdateOnlyListed,
+ /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE
+ UpdateListedWithTransitiveDepsNoRootRequire,
+ /// \Composer\DependencyResolver\Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS
+ UpdateListedWithTransitiveDeps,
+}
+
+#[derive(Debug)]
+pub struct Request {
+ pub(crate) locked_repository: Option<LockArrayRepositoryHandle>,
+ pub(crate) requires: IndexMap<String, AnyConstraint>,
+ pub(crate) fixed_packages: IndexMap<String, BasePackageHandle>,
+ pub(crate) locked_packages: IndexMap<String, BasePackageHandle>,
+ pub(crate) fixed_locked_packages: IndexMap<String, BasePackageHandle>,
+ pub(crate) update_allow_list: Vec<String>,
+ pub(crate) update_allow_transitive_dependencies: UpdateAllowTransitiveDeps,
+ restrict_packages: Option<Vec<String>>,
+}
diff --git a/crates/shirabe/src/downloader/file_downloader.rs b/crates/shirabe/src/downloader/file_downloader.rs
index d3b08b36..cdcf750e 100644
--- a/crates/shirabe/src/downloader/file_downloader.rs
+++ b/crates/shirabe/src/downloader/file_downloader.rs
@@ -150,6 +150,234 @@ impl FileDownloader {
this
}
+
+ /// Shared body of `ChangeReportInterface::get_local_changes`.
+ ///
+ /// PHP's `getLocalChanges` calls `$this->download()` / `$this->install()`, which late-bind
+ /// to the concrete downloader class (e.g. `ArchiveDownloader::install` extracts the archive
+ /// instead of copying the dist file). The Rust port embeds the parent class as `inner`, so
+ /// delegating downloaders must pass themselves as `this` to preserve that dispatch.
+ pub(crate) fn base_get_local_changes(
+ &self,
+ this: &dyn DownloaderInterface,
+ package: PackageInterfaceHandle,
+ path: &str,
+ ) -> anyhow::Result<Option<String>> {
+ let prev_io = std::mem::replace(
+ &mut *self.io.borrow_mut(),
+ std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())),
+ );
+ self.io
+ .borrow()
+ .borrow_mut()
+ .load_configuration(&mut self.config.borrow_mut())?;
+
+ let target_dir = Filesystem::trim_trailing_slash(path);
+ // PHP attaches an onRejected handler to capture the error and drives the promise via
+ // httpDownloader->wait() / process->wait(); the single-threaded sync bridge block_on's the
+ // download/install futures, so a rejection surfaces directly as the Err captured below.
+ let result: anyhow::Result<String> = (|| -> anyhow::Result<String> {
+ if is_dir(format!("{}_compare", target_dir)) {
+ self.filesystem
+ .borrow_mut()
+ .remove_directory(format!("{}_compare", target_dir))?;
+ }
+
+ sync_executor::block_on(this.download(
+ package.clone(),
+ &format!("{}_compare", target_dir),
+ None,
+ false,
+ ))?;
+ sync_executor::block_on(this.install(
+ package.clone(),
+ &format!("{}_compare", target_dir),
+ false,
+ ))?;
+
+ let mut comparer = Comparer::new();
+ comparer.set_source(format!("{}_compare", target_dir));
+ comparer.set_update(target_dir.clone());
+ comparer.do_compare();
+ let output = comparer.get_changed_as_string(true, false);
+ self.filesystem
+ .borrow_mut()
+ .remove_directory(format!("{}_compare", target_dir))?;
+ Ok(output)
+ })();
+
+ *self.io.borrow_mut() = prev_io;
+
+ let (e, output) = match result {
+ Ok(output) => (None, output),
+ Err(err) => (Some(err), String::new()),
+ };
+
+ if let Some(err) = e {
+ if self.io.borrow().is_debug() {
+ return Err(err);
+ }
+
+ return Ok(Some(format!(
+ "Failed to detect changes: [{}] {}",
+ get_class(&PhpMixed::Null),
+ err
+ )));
+ }
+
+ let output = trim(&output, None);
+
+ Ok(if strlen(&output) > 0 {
+ Some(output)
+ } else {
+ None
+ })
+ }
+
+ /// Shared body of `DownloaderInterface::update`; see `base_get_local_changes` for why the
+ /// concrete downloader is threaded in as `this`. The appendix is computed by the caller
+ /// because `getInstallOperationAppendix` is protected and not part of `DownloaderInterface`.
+ pub(crate) async fn base_update(
+ &self,
+ this: &dyn DownloaderInterface,
+ install_operation_appendix: &str,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ path: &str,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ self.io.borrow().write_error(&format!(
+ " - {}{}",
+ UpdateOperation::format(initial.clone(), target.clone(), false),
+ install_operation_appendix
+ ));
+
+ // PHP: return $this->remove($initial, $path, false)->then(fn () => $this->install($target, $path, false));
+ let _ = this.remove(initial, path, false).await?;
+ this.install(target, path, false).await
+ }
+
+ fn get_dist_path(&self, package: PackageInterfaceHandle, component: i64) -> String {
+ pathinfo(
+ parse_url(
+ &strtr(&package.get_dist_url().unwrap_or_default(), "\\", "/"),
+ PHP_URL_PATH,
+ )
+ .as_string()
+ .unwrap_or(""),
+ component,
+ )
+ }
+
+ pub(crate) fn clear_last_cache_write(&self, package: PackageInterfaceHandle) {
+ let mut last_cache_writes = self.last_cache_writes.lock().unwrap();
+ if let Some(cache) = &self.cache
+ && last_cache_writes.contains_key(&package.get_name())
+ {
+ let key = last_cache_writes.get(&package.get_name()).unwrap().clone();
+ cache.borrow_mut().remove(&key);
+ last_cache_writes.shift_remove(&package.get_name());
+ }
+ }
+
+ pub(crate) fn add_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) {
+ self.additional_cleanup_paths
+ .borrow_mut()
+ .entry(package.get_name())
+ .or_default()
+ .push(path.to_string());
+ }
+
+ pub(crate) fn remove_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) {
+ if let Some(paths) = self
+ .additional_cleanup_paths
+ .borrow_mut()
+ .get_mut(&package.get_name())
+ {
+ // PHP: array_search($path, ..., true)
+ let idx = paths.iter().position(|p| p == path);
+ if let Some(i) = idx {
+ paths.remove(i);
+ }
+ let _ = array_search;
+ }
+ }
+
+ /// Gets file name for specific package
+ pub(crate) fn get_file_name(&self, package: PackageInterfaceHandle, _path: &str) -> String {
+ let extension = self.get_dist_path(package.clone(), PATHINFO_EXTENSION);
+ let extension = if extension.is_empty() {
+ package.get_dist_type().unwrap_or_default()
+ } else {
+ extension
+ };
+
+ rtrim(
+ &format!(
+ "{}/composer/tmp-{}.{}",
+ self.config
+ .borrow_mut()
+ .get("vendor-dir")
+ .as_string()
+ .unwrap_or(""),
+ hash(
+ "md5",
+ &format!("{}{}", package, spl_object_hash(&PhpMixed::Null))
+ ),
+ extension
+ ),
+ Some("."),
+ )
+ }
+
+ /// Gets appendix message to add to the "- Upgrading x" string being output on update
+ fn get_install_operation_appendix(
+ &self,
+ _package: PackageInterfaceHandle,
+ _path: &str,
+ ) -> String {
+ String::new()
+ }
+
+ /// For testing only: invoke the crate-private `get_file_name`.
+ pub fn __get_file_name(&self, package: PackageInterfaceHandle, path: &str) -> String {
+ self.get_file_name(package, path)
+ }
+
+ /// For testing only: invoke the crate-private `process_url`.
+ pub fn __process_url(
+ &self,
+ package: PackageInterfaceHandle,
+ url: &str,
+ ) -> anyhow::Result<String> {
+ self.process_url(package, url)
+ }
+
+ /// Process the download url
+ pub(crate) fn process_url(
+ &self,
+ package: PackageInterfaceHandle,
+ url: &str,
+ ) -> anyhow::Result<String> {
+ if !shirabe_php_shim::extension_loaded("openssl") && Some(0) == strpos(url, "https:") {
+ return Err(RuntimeException {
+ message: "You must enable the openssl extension to download files via https"
+ .to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let mut url = url.to_string();
+ if package.get_dist_reference().is_some() {
+ url = UrlUtil::update_dist_reference(
+ &self.config.borrow(),
+ url,
+ &package.get_dist_reference().unwrap(),
+ );
+ }
+
+ Ok(url)
+ }
}
#[async_trait::async_trait(?Send)]
@@ -604,238 +832,6 @@ impl ChangeReportInterface for FileDownloader {
}
}
-impl FileDownloader {
- /// Shared body of `ChangeReportInterface::get_local_changes`.
- ///
- /// PHP's `getLocalChanges` calls `$this->download()` / `$this->install()`, which late-bind
- /// to the concrete downloader class (e.g. `ArchiveDownloader::install` extracts the archive
- /// instead of copying the dist file). The Rust port embeds the parent class as `inner`, so
- /// delegating downloaders must pass themselves as `this` to preserve that dispatch.
- pub(crate) fn base_get_local_changes(
- &self,
- this: &dyn DownloaderInterface,
- package: PackageInterfaceHandle,
- path: &str,
- ) -> anyhow::Result<Option<String>> {
- let prev_io = std::mem::replace(
- &mut *self.io.borrow_mut(),
- std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())),
- );
- self.io
- .borrow()
- .borrow_mut()
- .load_configuration(&mut self.config.borrow_mut())?;
-
- let target_dir = Filesystem::trim_trailing_slash(path);
- // PHP attaches an onRejected handler to capture the error and drives the promise via
- // httpDownloader->wait() / process->wait(); the single-threaded sync bridge block_on's the
- // download/install futures, so a rejection surfaces directly as the Err captured below.
- let result: anyhow::Result<String> = (|| -> anyhow::Result<String> {
- if is_dir(format!("{}_compare", target_dir)) {
- self.filesystem
- .borrow_mut()
- .remove_directory(format!("{}_compare", target_dir))?;
- }
-
- sync_executor::block_on(this.download(
- package.clone(),
- &format!("{}_compare", target_dir),
- None,
- false,
- ))?;
- sync_executor::block_on(this.install(
- package.clone(),
- &format!("{}_compare", target_dir),
- false,
- ))?;
-
- let mut comparer = Comparer::new();
- comparer.set_source(format!("{}_compare", target_dir));
- comparer.set_update(target_dir.clone());
- comparer.do_compare();
- let output = comparer.get_changed_as_string(true, false);
- self.filesystem
- .borrow_mut()
- .remove_directory(format!("{}_compare", target_dir))?;
- Ok(output)
- })();
-
- *self.io.borrow_mut() = prev_io;
-
- let (e, output) = match result {
- Ok(output) => (None, output),
- Err(err) => (Some(err), String::new()),
- };
-
- if let Some(err) = e {
- if self.io.borrow().is_debug() {
- return Err(err);
- }
-
- return Ok(Some(format!(
- "Failed to detect changes: [{}] {}",
- get_class(&PhpMixed::Null),
- err
- )));
- }
-
- let output = trim(&output, None);
-
- Ok(if strlen(&output) > 0 {
- Some(output)
- } else {
- None
- })
- }
-
- /// Shared body of `DownloaderInterface::update`; see `base_get_local_changes` for why the
- /// concrete downloader is threaded in as `this`. The appendix is computed by the caller
- /// because `getInstallOperationAppendix` is protected and not part of `DownloaderInterface`.
- pub(crate) async fn base_update(
- &self,
- this: &dyn DownloaderInterface,
- install_operation_appendix: &str,
- initial: PackageInterfaceHandle,
- target: PackageInterfaceHandle,
- path: &str,
- ) -> anyhow::Result<Option<PhpMixed>> {
- self.io.borrow().write_error(&format!(
- " - {}{}",
- UpdateOperation::format(initial.clone(), target.clone(), false),
- install_operation_appendix
- ));
-
- // PHP: return $this->remove($initial, $path, false)->then(fn () => $this->install($target, $path, false));
- let _ = this.remove(initial, path, false).await?;
- this.install(target, path, false).await
- }
-}
-
-impl FileDownloader {
- fn get_dist_path(&self, package: PackageInterfaceHandle, component: i64) -> String {
- pathinfo(
- parse_url(
- &strtr(&package.get_dist_url().unwrap_or_default(), "\\", "/"),
- PHP_URL_PATH,
- )
- .as_string()
- .unwrap_or(""),
- component,
- )
- }
-
- pub(crate) fn clear_last_cache_write(&self, package: PackageInterfaceHandle) {
- let mut last_cache_writes = self.last_cache_writes.lock().unwrap();
- if let Some(cache) = &self.cache
- && last_cache_writes.contains_key(&package.get_name())
- {
- let key = last_cache_writes.get(&package.get_name()).unwrap().clone();
- cache.borrow_mut().remove(&key);
- last_cache_writes.shift_remove(&package.get_name());
- }
- }
-
- pub(crate) fn add_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) {
- self.additional_cleanup_paths
- .borrow_mut()
- .entry(package.get_name())
- .or_default()
- .push(path.to_string());
- }
-
- pub(crate) fn remove_cleanup_path(&self, package: PackageInterfaceHandle, path: &str) {
- if let Some(paths) = self
- .additional_cleanup_paths
- .borrow_mut()
- .get_mut(&package.get_name())
- {
- // PHP: array_search($path, ..., true)
- let idx = paths.iter().position(|p| p == path);
- if let Some(i) = idx {
- paths.remove(i);
- }
- let _ = array_search;
- }
- }
-
- /// Gets file name for specific package
- pub(crate) fn get_file_name(&self, package: PackageInterfaceHandle, _path: &str) -> String {
- let extension = self.get_dist_path(package.clone(), PATHINFO_EXTENSION);
- let extension = if extension.is_empty() {
- package.get_dist_type().unwrap_or_default()
- } else {
- extension
- };
-
- rtrim(
- &format!(
- "{}/composer/tmp-{}.{}",
- self.config
- .borrow_mut()
- .get("vendor-dir")
- .as_string()
- .unwrap_or(""),
- hash(
- "md5",
- &format!("{}{}", package, spl_object_hash(&PhpMixed::Null))
- ),
- extension
- ),
- Some("."),
- )
- }
-
- /// Gets appendix message to add to the "- Upgrading x" string being output on update
- fn get_install_operation_appendix(
- &self,
- _package: PackageInterfaceHandle,
- _path: &str,
- ) -> String {
- String::new()
- }
-
- /// For testing only: invoke the crate-private `get_file_name`.
- pub fn __get_file_name(&self, package: PackageInterfaceHandle, path: &str) -> String {
- self.get_file_name(package, path)
- }
-
- /// For testing only: invoke the crate-private `process_url`.
- pub fn __process_url(
- &self,
- package: PackageInterfaceHandle,
- url: &str,
- ) -> anyhow::Result<String> {
- self.process_url(package, url)
- }
-
- /// Process the download url
- pub(crate) fn process_url(
- &self,
- package: PackageInterfaceHandle,
- url: &str,
- ) -> anyhow::Result<String> {
- if !shirabe_php_shim::extension_loaded("openssl") && Some(0) == strpos(url, "https:") {
- return Err(RuntimeException {
- message: "You must enable the openssl extension to download files via https"
- .to_string(),
- code: 0,
- }
- .into());
- }
-
- let mut url = url.to_string();
- if package.get_dist_reference().is_some() {
- url = UrlUtil::update_dist_reference(
- &self.config.borrow(),
- url,
- &package.get_dist_reference().unwrap(),
- );
- }
-
- Ok(url)
- }
-}
-
#[derive(Debug, Clone)]
struct UrlEntry {
base: String,
diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs
index f0399e89..0940183c 100644
--- a/crates/shirabe/src/package/loader/array_loader.rs
+++ b/crates/shirabe/src/package/loader/array_loader.rs
@@ -39,156 +39,7 @@ impl ArrayLoader {
load_options,
}
}
-}
-
-enum CompleteOrRootPackage {
- Complete(CompletePackage),
- Root(RootPackage),
-}
-
-impl CompleteOrRootPackage {
- fn package(&self) -> &Package {
- match self {
- Self::Complete(p) => &p.inner,
- Self::Root(p) => &p.inner.inner,
- }
- }
-
- fn package_mut(&mut self) -> &mut Package {
- match self {
- Self::Complete(p) => &mut p.inner,
- Self::Root(p) => &mut p.inner.inner,
- }
- }
-
- fn complete_mut(&mut self) -> &mut dyn CompletePackageInterface {
- match self {
- Self::Complete(p) => p,
- Self::Root(p) => p,
- }
- }
-
- fn is_root(&self) -> bool {
- matches!(self, Self::Root(_))
- }
-
- fn get_name(&self) -> &str {
- self.package().get_name()
- }
-
- fn get_pretty_version(&self) -> &str {
- self.package().get_pretty_version()
- }
-
- fn into_handle(self) -> PackageInterfaceHandle {
- match self {
- Self::Complete(p) => CompletePackageHandle::from_complete_package(p).into(),
- Self::Root(p) => RootPackageHandle::from_root_package(p).into(),
- }
- }
-}
-
-fn php_to_map(value: &PhpMixed) -> IndexMap<String, PhpMixed> {
- match value {
- PhpMixed::Array(m) => m.clone(),
- _ => IndexMap::new(),
- }
-}
-
-fn php_to_string_vec(value: &PhpMixed) -> Vec<String> {
- match value {
- PhpMixed::List(l) => l.iter().map(strval).collect(),
- PhpMixed::Array(m) => m.values().map(strval).collect(),
- _ => Vec::new(),
- }
-}
-
-fn apply_link_setter(package: &mut Package, method: &str, links: IndexMap<String, Link>) {
- if method == Link::TYPE_REQUIRE {
- package.set_requires(links);
- } else if method == Link::TYPE_DEV_REQUIRE {
- package.set_dev_requires(links);
- } else if method == Link::TYPE_CONFLICT {
- package.set_conflicts(links);
- } else if method == Link::TYPE_PROVIDE {
- package.set_provides(links);
- } else if method == Link::TYPE_REPLACE {
- package.set_replaces(links);
- }
-}
-
-fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> {
- let entries: Vec<&PhpMixed> = match value {
- PhpMixed::List(l) => l.iter().collect(),
- PhpMixed::Array(m) => m.values().collect(),
- _ => Vec::new(),
- };
- entries
- .into_iter()
- .filter_map(|entry| match entry {
- PhpMixed::Array(m) => Some(Mirror {
- url: m
- .get("url")
- .and_then(|v| v.as_string())
- .unwrap_or("")
- .to_string(),
- preferred: m.get("preferred").is_some_and(|v| v.to_bool()),
- }),
- _ => None,
- })
- .collect()
-}
-
-impl LoaderInterface for ArrayLoader {
- fn as_any(&self) -> &dyn std::any::Any {
- self
- }
-
- fn load(
- &self,
- mut config: IndexMap<String, PhpMixed>,
- class: Option<String>,
- ) -> anyhow::Result<PackageInterfaceHandle> {
- let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string());
-
- if class != "Composer\\Package\\CompletePackage"
- && class != "Composer\\Package\\RootPackage"
- {
- trigger_error(
- "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.",
- E_USER_DEPRECATED,
- );
- }
-
- let mut package = self.create_object(&config, &class)?;
-
- for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() {
- let entry = config.get(*r#type);
- let entry_is_array = entry
- .map(|v| matches!(v, PhpMixed::Array(_)))
- .unwrap_or(false);
- if entry.is_none() || !entry_is_array {
- continue;
- }
- let links = self.parse_links(
- package.get_name(),
- package.get_pretty_version(),
- opts.method,
- match entry.unwrap() {
- PhpMixed::Array(arr) => arr.clone(),
- _ => IndexMap::new(),
- },
- )?;
- apply_link_setter(package.package_mut(), opts.method, links);
- }
-
- let package = self.configure_object(package, &mut config)?;
-
- Ok(package)
- }
-}
-impl ArrayLoader {
#[tracing::instrument(skip_all)]
pub fn load_packages(
&self,
@@ -938,3 +789,150 @@ impl ArrayLoader {
Ok(None)
}
}
+
+enum CompleteOrRootPackage {
+ Complete(CompletePackage),
+ Root(RootPackage),
+}
+
+impl CompleteOrRootPackage {
+ fn package(&self) -> &Package {
+ match self {
+ Self::Complete(p) => &p.inner,
+ Self::Root(p) => &p.inner.inner,
+ }
+ }
+
+ fn package_mut(&mut self) -> &mut Package {
+ match self {
+ Self::Complete(p) => &mut p.inner,
+ Self::Root(p) => &mut p.inner.inner,
+ }
+ }
+
+ fn complete_mut(&mut self) -> &mut dyn CompletePackageInterface {
+ match self {
+ Self::Complete(p) => p,
+ Self::Root(p) => p,
+ }
+ }
+
+ fn is_root(&self) -> bool {
+ matches!(self, Self::Root(_))
+ }
+
+ fn get_name(&self) -> &str {
+ self.package().get_name()
+ }
+
+ fn get_pretty_version(&self) -> &str {
+ self.package().get_pretty_version()
+ }
+
+ fn into_handle(self) -> PackageInterfaceHandle {
+ match self {
+ Self::Complete(p) => CompletePackageHandle::from_complete_package(p).into(),
+ Self::Root(p) => RootPackageHandle::from_root_package(p).into(),
+ }
+ }
+}
+
+fn php_to_map(value: &PhpMixed) -> IndexMap<String, PhpMixed> {
+ match value {
+ PhpMixed::Array(m) => m.clone(),
+ _ => IndexMap::new(),
+ }
+}
+
+fn php_to_string_vec(value: &PhpMixed) -> Vec<String> {
+ match value {
+ PhpMixed::List(l) => l.iter().map(strval).collect(),
+ PhpMixed::Array(m) => m.values().map(strval).collect(),
+ _ => Vec::new(),
+ }
+}
+
+fn apply_link_setter(package: &mut Package, method: &str, links: IndexMap<String, Link>) {
+ if method == Link::TYPE_REQUIRE {
+ package.set_requires(links);
+ } else if method == Link::TYPE_DEV_REQUIRE {
+ package.set_dev_requires(links);
+ } else if method == Link::TYPE_CONFLICT {
+ package.set_conflicts(links);
+ } else if method == Link::TYPE_PROVIDE {
+ package.set_provides(links);
+ } else if method == Link::TYPE_REPLACE {
+ package.set_replaces(links);
+ }
+}
+
+fn php_to_mirrors(value: &PhpMixed) -> Vec<Mirror> {
+ let entries: Vec<&PhpMixed> = match value {
+ PhpMixed::List(l) => l.iter().collect(),
+ PhpMixed::Array(m) => m.values().collect(),
+ _ => Vec::new(),
+ };
+ entries
+ .into_iter()
+ .filter_map(|entry| match entry {
+ PhpMixed::Array(m) => Some(Mirror {
+ url: m
+ .get("url")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string(),
+ preferred: m.get("preferred").is_some_and(|v| v.to_bool()),
+ }),
+ _ => None,
+ })
+ .collect()
+}
+
+impl LoaderInterface for ArrayLoader {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ fn load(
+ &self,
+ mut config: IndexMap<String, PhpMixed>,
+ class: Option<String>,
+ ) -> anyhow::Result<PackageInterfaceHandle> {
+ let class = class.unwrap_or_else(|| "Composer\\Package\\CompletePackage".to_string());
+
+ if class != "Composer\\Package\\CompletePackage"
+ && class != "Composer\\Package\\RootPackage"
+ {
+ trigger_error(
+ "The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.",
+ E_USER_DEPRECATED,
+ );
+ }
+
+ let mut package = self.create_object(&config, &class)?;
+
+ for (r#type, opts) in SUPPORTED_LINK_TYPES.iter() {
+ let entry = config.get(*r#type);
+ let entry_is_array = entry
+ .map(|v| matches!(v, PhpMixed::Array(_)))
+ .unwrap_or(false);
+ if entry.is_none() || !entry_is_array {
+ continue;
+ }
+ let links = self.parse_links(
+ package.get_name(),
+ package.get_pretty_version(),
+ opts.method,
+ match entry.unwrap() {
+ PhpMixed::Array(arr) => arr.clone(),
+ _ => IndexMap::new(),
+ },
+ )?;
+ apply_link_setter(package.package_mut(), opts.method, links);
+ }
+
+ let package = self.configure_object(package, &mut config)?;
+
+ Ok(package)
+ }
+}
diff --git a/crates/shirabe/src/package/loader/validating_array_loader.rs b/crates/shirabe/src/package/loader/validating_array_loader.rs
index 0be0ef38..a28df595 100644
--- a/crates/shirabe/src/package/loader/validating_array_loader.rs
+++ b/crates/shirabe/src/package/loader/validating_array_loader.rs
@@ -60,6 +60,282 @@ impl ValidatingArrayLoader {
flags,
}
}
+
+ pub fn get_warnings(&self) -> Vec<String> {
+ self.warnings.borrow().clone()
+ }
+
+ pub fn get_errors(&self) -> Vec<String> {
+ self.errors.borrow().clone()
+ }
+
+ pub fn has_package_naming_error(name: &str, is_link: bool) -> Option<String> {
+ if PlatformRepository::is_platform_package(name) {
+ return None;
+ }
+
+ if !Preg::is_match(
+ php_regex!(
+ "{^[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+$}iD"
+ ),
+ name,
+ ) {
+ return Some(format!(
+ "{} is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match \"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{{0,2}})[a-z0-9]+)*$\".",
+ name
+ ));
+ }
+
+ let reserved_names = [
+ "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7",
+ "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
+ ];
+ let lower = strtolower(name);
+ let bits: Vec<&str> = lower.split('/').collect();
+ if reserved_names.contains(&bits[0]) || reserved_names.contains(&bits[1]) {
+ return Some(format!(
+ "{} is reserved, package and vendor names can not match any of: {}.",
+ name,
+ reserved_names.join(", ")
+ ));
+ }
+
+ if Preg::is_match(php_regex!("{\\.json$}"), name) {
+ return Some(format!(
+ "{} is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.",
+ name
+ ));
+ }
+
+ if Preg::is_match(php_regex!("{[A-Z]}"), name) {
+ if is_link {
+ return Some(format!(
+ "{} is invalid, it should not contain uppercase characters. Please use {} instead.",
+ name,
+ strtolower(name)
+ ));
+ }
+
+ let suggest_name = Preg::replace(
+ php_regex!("{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"),
+ "\\1\\3-\\2\\4",
+ name,
+ );
+ let suggest_name = strtolower(&suggest_name);
+
+ return Some(format!(
+ "{} is invalid, it should not contain uppercase characters. We suggest using {} instead.",
+ name, suggest_name
+ ));
+ }
+
+ None
+ }
+
+ fn validate_regex(&self, property: &str, regex: &str, mandatory: bool) -> bool {
+ if !self.validate_string(property, mandatory) {
+ return false;
+ }
+
+ let value = self.config.borrow()[property]
+ .as_string()
+ .unwrap_or("")
+ .to_string();
+ if !Preg::is_match(format!("{{^{}$}}u", regex), &value) {
+ let message = format!(
+ "{} : invalid value ({}), must match {}",
+ property, value, regex
+ );
+ if mandatory {
+ self.errors.borrow_mut().push(message);
+ } else {
+ self.warnings.borrow_mut().push(message);
+ }
+ self.config.borrow_mut().shift_remove(property);
+
+ return false;
+ }
+
+ true
+ }
+
+ fn validate_string(&self, property: &str, mandatory: bool) -> bool {
+ if self.config.borrow().contains_key(property)
+ && !is_string(&self.config.borrow()[property])
+ {
+ self.errors.borrow_mut().push(format!(
+ "{} : should be a string, {} given",
+ property,
+ get_debug_type(&self.config.borrow()[property])
+ ));
+ self.config.borrow_mut().shift_remove(property);
+
+ return false;
+ }
+
+ let is_empty = !self.config.borrow().contains_key(property)
+ || trim(
+ self.config.borrow()[property].as_string().unwrap_or(""),
+ Some(" \t\n\r\0\u{0B}"),
+ )
+ .is_empty();
+ if is_empty {
+ if mandatory {
+ self.errors
+ .borrow_mut()
+ .push(format!("{} : must be present", property));
+ }
+ self.config.borrow_mut().shift_remove(property);
+
+ return false;
+ }
+
+ true
+ }
+
+ fn validate_array(&self, property: &str, mandatory: bool) -> bool {
+ if self.config.borrow().contains_key(property) && !is_array(&self.config.borrow()[property])
+ {
+ self.errors.borrow_mut().push(format!(
+ "{} : should be an array, {} given",
+ property,
+ get_debug_type(&self.config.borrow()[property])
+ ));
+ self.config.borrow_mut().shift_remove(property);
+
+ return false;
+ }
+
+ let is_empty = !self.config.borrow().contains_key(property)
+ || match &self.config.borrow()[property] {
+ PhpMixed::Array(m) => m.is_empty(),
+ PhpMixed::List(l) => l.is_empty(),
+ // is_array() above guarantees the value is Array or List here.
+ _ => unreachable!("validate_array: non-array value survived the is_array check"),
+ };
+ if is_empty {
+ if mandatory {
+ self.errors.borrow_mut().push(format!(
+ "{} : must be present and contain at least one element",
+ property
+ ));
+ }
+ self.config.borrow_mut().shift_remove(property);
+
+ return false;
+ }
+
+ true
+ }
+
+ fn validate_flat_array(&self, property: &str, regex: Option<&str>, mandatory: bool) -> bool {
+ if !self.validate_array(property, mandatory) {
+ return false;
+ }
+
+ let mut pass = true;
+ let entries: Vec<(String, PhpMixed)> = self.config.borrow()[property]
+ .as_array()
+ .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
+ .unwrap_or_default();
+ for (key, value) in entries {
+ if !is_string(&value) && !is_numeric(&value) {
+ self.errors.borrow_mut().push(format!(
+ "{}.{} : must be a string or int, {} given",
+ property,
+ key,
+ get_debug_type(&value)
+ ));
+ if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) {
+ arr.shift_remove(&key);
+ }
+ pass = false;
+
+ continue;
+ }
+
+ if let Some(regex_str) = regex {
+ let value_str = php_to_string(&value);
+ if !Preg::is_match(format!("{{^{}$}}u", regex_str), &value_str) {
+ self.warnings.borrow_mut().push(format!(
+ "{}.{} : invalid value ({}), must match {}",
+ property, key, value_str, regex_str
+ ));
+ if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) {
+ arr.shift_remove(&key);
+ }
+ pass = false;
+ }
+ }
+ }
+
+ pass
+ }
+
+ fn validate_url(&self, property: &str, mandatory: bool) -> bool {
+ if !self.validate_string(property, mandatory) {
+ return false;
+ }
+
+ let value = self.config.borrow()[property]
+ .as_string()
+ .unwrap_or("")
+ .to_string();
+ if !self.filter_url(&value, &["http", "https"]) {
+ self.warnings.borrow_mut().push(format!(
+ "{} : invalid value ({}), must be an http/https URL",
+ property, value
+ ));
+ self.config.borrow_mut().shift_remove(property);
+
+ return false;
+ }
+
+ true
+ }
+
+ fn filter_url(&self, value: &str, schemes: &[&str]) -> bool {
+ if value.is_empty() {
+ return true;
+ }
+
+ let bits = parse_url_all(value);
+ let bits_map = match bits {
+ PhpMixed::Array(m) => m,
+ _ => return false,
+ };
+ let scheme = bits_map
+ .get("scheme")
+ .and_then(|v| v.as_string())
+ .unwrap_or("");
+ let host = bits_map
+ .get("host")
+ .and_then(|v| v.as_string())
+ .unwrap_or("");
+ if scheme.is_empty() || host.is_empty() {
+ return false;
+ }
+
+ if !schemes.contains(&scheme) {
+ return false;
+ }
+
+ true
+ }
+
+ fn is_empty_array(val: Option<&PhpMixed>) -> bool {
+ match val {
+ Some(v) => match v {
+ PhpMixed::Array(m) => m.is_empty(),
+ PhpMixed::Null => true,
+ PhpMixed::Bool(false) => true,
+ PhpMixed::String(s) => s.is_empty(),
+ PhpMixed::Int(0) => true,
+ _ => false,
+ },
+ None => true,
+ }
+ }
}
impl LoaderInterface for ValidatingArrayLoader {
@@ -1327,281 +1603,3 @@ impl LoaderInterface for ValidatingArrayLoader {
Ok(package)
}
}
-
-impl ValidatingArrayLoader {
- pub fn get_warnings(&self) -> Vec<String> {
- self.warnings.borrow().clone()
- }
-
- pub fn get_errors(&self) -> Vec<String> {
- self.errors.borrow().clone()
- }
-
- pub fn has_package_naming_error(name: &str, is_link: bool) -> Option<String> {
- if PlatformRepository::is_platform_package(name) {
- return None;
- }
-
- if !Preg::is_match(
- php_regex!(
- "{^[a-z0-9](?:[_.-]?[a-z0-9]++)*+/[a-z0-9](?:(?:[_.]|-{1,2})?[a-z0-9]++)*+$}iD"
- ),
- name,
- ) {
- return Some(format!(
- "{} is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match \"^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{{0,2}})[a-z0-9]+)*$\".",
- name
- ));
- }
-
- let reserved_names = [
- "nul", "con", "prn", "aux", "com1", "com2", "com3", "com4", "com5", "com6", "com7",
- "com8", "com9", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9",
- ];
- let lower = strtolower(name);
- let bits: Vec<&str> = lower.split('/').collect();
- if reserved_names.contains(&bits[0]) || reserved_names.contains(&bits[1]) {
- return Some(format!(
- "{} is reserved, package and vendor names can not match any of: {}.",
- name,
- reserved_names.join(", ")
- ));
- }
-
- if Preg::is_match(php_regex!("{\\.json$}"), name) {
- return Some(format!(
- "{} is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.",
- name
- ));
- }
-
- if Preg::is_match(php_regex!("{[A-Z]}"), name) {
- if is_link {
- return Some(format!(
- "{} is invalid, it should not contain uppercase characters. Please use {} instead.",
- name,
- strtolower(name)
- ));
- }
-
- let suggest_name = Preg::replace(
- php_regex!("{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}"),
- "\\1\\3-\\2\\4",
- name,
- );
- let suggest_name = strtolower(&suggest_name);
-
- return Some(format!(
- "{} is invalid, it should not contain uppercase characters. We suggest using {} instead.",
- name, suggest_name
- ));
- }
-
- None
- }
-
- fn validate_regex(&self, property: &str, regex: &str, mandatory: bool) -> bool {
- if !self.validate_string(property, mandatory) {
- return false;
- }
-
- let value = self.config.borrow()[property]
- .as_string()
- .unwrap_or("")
- .to_string();
- if !Preg::is_match(format!("{{^{}$}}u", regex), &value) {
- let message = format!(
- "{} : invalid value ({}), must match {}",
- property, value, regex
- );
- if mandatory {
- self.errors.borrow_mut().push(message);
- } else {
- self.warnings.borrow_mut().push(message);
- }
- self.config.borrow_mut().shift_remove(property);
-
- return false;
- }
-
- true
- }
-
- fn validate_string(&self, property: &str, mandatory: bool) -> bool {
- if self.config.borrow().contains_key(property)
- && !is_string(&self.config.borrow()[property])
- {
- self.errors.borrow_mut().push(format!(
- "{} : should be a string, {} given",
- property,
- get_debug_type(&self.config.borrow()[property])
- ));
- self.config.borrow_mut().shift_remove(property);
-
- return false;
- }
-
- let is_empty = !self.config.borrow().contains_key(property)
- || trim(
- self.config.borrow()[property].as_string().unwrap_or(""),
- Some(" \t\n\r\0\u{0B}"),
- )
- .is_empty();
- if is_empty {
- if mandatory {
- self.errors
- .borrow_mut()
- .push(format!("{} : must be present", property));
- }
- self.config.borrow_mut().shift_remove(property);
-
- return false;
- }
-
- true
- }
-
- fn validate_array(&self, property: &str, mandatory: bool) -> bool {
- if self.config.borrow().contains_key(property) && !is_array(&self.config.borrow()[property])
- {
- self.errors.borrow_mut().push(format!(
- "{} : should be an array, {} given",
- property,
- get_debug_type(&self.config.borrow()[property])
- ));
- self.config.borrow_mut().shift_remove(property);
-
- return false;
- }
-
- let is_empty = !self.config.borrow().contains_key(property)
- || match &self.config.borrow()[property] {
- PhpMixed::Array(m) => m.is_empty(),
- PhpMixed::List(l) => l.is_empty(),
- // is_array() above guarantees the value is Array or List here.
- _ => unreachable!("validate_array: non-array value survived the is_array check"),
- };
- if is_empty {
- if mandatory {
- self.errors.borrow_mut().push(format!(
- "{} : must be present and contain at least one element",
- property
- ));
- }
- self.config.borrow_mut().shift_remove(property);
-
- return false;
- }
-
- true
- }
-
- fn validate_flat_array(&self, property: &str, regex: Option<&str>, mandatory: bool) -> bool {
- if !self.validate_array(property, mandatory) {
- return false;
- }
-
- let mut pass = true;
- let entries: Vec<(String, PhpMixed)> = self.config.borrow()[property]
- .as_array()
- .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
- .unwrap_or_default();
- for (key, value) in entries {
- if !is_string(&value) && !is_numeric(&value) {
- self.errors.borrow_mut().push(format!(
- "{}.{} : must be a string or int, {} given",
- property,
- key,
- get_debug_type(&value)
- ));
- if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) {
- arr.shift_remove(&key);
- }
- pass = false;
-
- continue;
- }
-
- if let Some(regex_str) = regex {
- let value_str = php_to_string(&value);
- if !Preg::is_match(format!("{{^{}$}}u", regex_str), &value_str) {
- self.warnings.borrow_mut().push(format!(
- "{}.{} : invalid value ({}), must match {}",
- property, key, value_str, regex_str
- ));
- if let Some(PhpMixed::Array(arr)) = self.config.borrow_mut().get_mut(property) {
- arr.shift_remove(&key);
- }
- pass = false;
- }
- }
- }
-
- pass
- }
-
- fn validate_url(&self, property: &str, mandatory: bool) -> bool {
- if !self.validate_string(property, mandatory) {
- return false;
- }
-
- let value = self.config.borrow()[property]
- .as_string()
- .unwrap_or("")
- .to_string();
- if !self.filter_url(&value, &["http", "https"]) {
- self.warnings.borrow_mut().push(format!(
- "{} : invalid value ({}), must be an http/https URL",
- property, value
- ));
- self.config.borrow_mut().shift_remove(property);
-
- return false;
- }
-
- true
- }
-
- fn filter_url(&self, value: &str, schemes: &[&str]) -> bool {
- if value.is_empty() {
- return true;
- }
-
- let bits = parse_url_all(value);
- let bits_map = match bits {
- PhpMixed::Array(m) => m,
- _ => return false,
- };
- let scheme = bits_map
- .get("scheme")
- .and_then(|v| v.as_string())
- .unwrap_or("");
- let host = bits_map
- .get("host")
- .and_then(|v| v.as_string())
- .unwrap_or("");
- if scheme.is_empty() || host.is_empty() {
- return false;
- }
-
- if !schemes.contains(&scheme) {
- return false;
- }
-
- true
- }
-
- fn is_empty_array(val: Option<&PhpMixed>) -> bool {
- match val {
- Some(v) => match v {
- PhpMixed::Array(m) => m.is_empty(),
- PhpMixed::Null => true,
- PhpMixed::Bool(false) => true,
- PhpMixed::String(s) => s.is_empty(),
- PhpMixed::Int(0) => true,
- _ => false,
- },
- None => true,
- }
- }
-}