//! ref: composer/src/Composer/Command/RepositoryCommand.php
use anyhow::Result;
use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_external_packages::symfony::console::command::command::Command;
use shirabe_external_packages::symfony::console::input::InputInterface;
use shirabe_external_packages::symfony::console::output::OutputInterface;
use shirabe_php_shim::{
InvalidArgumentException, PHP_URL_HOST, PhpMixed, RuntimeException, parse_url, strtolower,
};
use std::cell::RefCell;
use std::rc::Rc;
use crate::advisory::AuditConfig;
use crate::command::BaseConfigCommand;
use crate::command::{BaseCommand, BaseCommandData};
use crate::composer::PartialComposerHandle;
use crate::config::Config;
use crate::config::ConfigSourceInterface;
use crate::config::JsonConfigSource;
use crate::console::input::InputArgument;
use crate::console::input::InputOption;
use crate::factory::Factory;
use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface;
use crate::io::IOInterface;
use crate::io::IOInterfaceImmutable;
use crate::json::JsonFile;
#[derive(Debug)]
pub struct RepositoryCommand {
base_command_data: BaseCommandData,
config: std::cell::RefCell>>>,
config_file: std::cell::RefCell >>>,
config_source: std::cell::RefCell >,
}
impl Default for RepositoryCommand {
fn default() -> Self {
Self::new()
}
}
impl RepositoryCommand {
pub fn new() -> Self {
let command = RepositoryCommand {
base_command_data: BaseCommandData::new(None),
config: std::cell::RefCell::new(None),
config_file: std::cell::RefCell::new(None),
config_source: std::cell::RefCell::new(None),
};
command
.configure()
.expect("RepositoryCommand::configure uses static, valid metadata");
command
}
fn list_repositories(&self, mut repos: IndexMap) {
let io = self.get_io();
let mut packagist_present = false;
for (_key, repo) in &repos {
if let PhpMixed::Array(ref repo_map) = *repo {
let has_type_and_url =
repo_map.contains_key("type") && repo_map.contains_key("url");
let is_composer_type =
repo_map.get("type").and_then(|v| v.as_string()) == Some("composer");
let url_host_ends_with_packagist = repo_map
.get("url")
.and_then(|v| v.as_string())
.map(|url| {
parse_url(url, PHP_URL_HOST)
.as_string()
.unwrap_or("")
.ends_with("packagist.org")
})
.unwrap_or(false);
if has_type_and_url && is_composer_type && url_host_ends_with_packagist {
packagist_present = true;
break;
}
}
}
if !packagist_present {
let mut packagist_entry = IndexMap::new();
packagist_entry.insert("packagist.org".to_string(), PhpMixed::Bool(false));
repos.insert(repos.len().to_string(), PhpMixed::Array(packagist_entry));
}
if repos.is_empty() {
io.write("No repositories configured");
return;
}
for (key, repo) in &repos {
if matches!(*repo, PhpMixed::Bool(false)) {
io.write(&format!("[{}] disabled ", key));
continue;
}
if let PhpMixed::Array(ref repo_map) = *repo {
if repo_map.len() == 1
&& let Some(first_val) = repo_map.values().next()
&& matches!(*first_val, PhpMixed::Bool(false))
{
let first_key = repo_map.keys().next().unwrap();
io.write(&format!("[{}] disabled ", first_key));
continue;
}
let name = repo_map
.get("name")
.and_then(|v| v.as_string())
.unwrap_or(key.as_str());
let r#type = repo_map
.get("type")
.and_then(|v| v.as_string())
.unwrap_or("unknown");
let url = repo_map
.get("url")
.and_then(|v| v.as_string())
.map(|s| s.to_string())
.unwrap_or_else(|| JsonFile::encode(repo));
io.write(&format!("[{}] {} {}", name, r#type, url));
}
}
}
// TODO(cli-completion): fn suggest_type_for_add()
// TODO(cli-completion): fn suggest_repo_names(&self)
}
impl Command for RepositoryCommand {
fn configure(&self) -> anyhow::Result<()> {
// TODO(cli-completion): suggest_repo_names() / suggest_type_for_add()
self.set_name("repository")?;
self.set_aliases(vec!["repo".to_string()])?;
self.set_description("Manages repositories");
self.set_definition(&[
InputOption::new(
"global",
Some(PhpMixed::String("g".to_string())),
Some(InputOption::VALUE_NONE),
"Apply command to the global config file",
None,
)
.unwrap()
.into(),
InputOption::new(
"file",
Some(PhpMixed::String("f".to_string())),
Some(InputOption::VALUE_REQUIRED),
"If you want to choose a different composer.json or config.json",
None,
)
.unwrap()
.into(),
InputOption::new(
"append",
None,
Some(InputOption::VALUE_NONE),
"When adding a repository, append it (lower priority) instead of prepending it",
None,
)
.unwrap()
.into(),
InputOption::new(
"before",
None,
Some(InputOption::VALUE_REQUIRED),
"When adding a repository, insert it before the given repository name",
None,
)
.unwrap()
.into(),
InputOption::new(
"after",
None,
Some(InputOption::VALUE_REQUIRED),
"When adding a repository, insert it after the given repository name",
None,
)
.unwrap()
.into(),
InputArgument::new(
"action",
Some(InputArgument::OPTIONAL),
"Action to perform: list, add, remove, set-url, get-url, enable, disable",
Some(PhpMixed::String("list".to_string())),
)
.unwrap()
.into(),
InputArgument::new(
"name",
Some(InputArgument::OPTIONAL),
"Repository name (or special name packagist.org for enable/disable)",
None,
)
.unwrap()
.into(),
InputArgument::new(
"arg1",
Some(InputArgument::OPTIONAL),
"Type for add, or new URL for set-url, or JSON config for add",
None,
)
.unwrap()
.into(),
InputArgument::new(
"arg2",
Some(InputArgument::OPTIONAL),
"URL for add (if not using JSON)",
None,
)
.unwrap()
.into(),
]);
self.set_help(
"This command lets you manage repositories in your composer.json.\n\n\
Examples:\n \
composer repo list\n \
composer repo add foo vcs https://github.com/acme/foo\n \
composer repo add bar composer https://repo.packagist.com/bar\n \
composer repo add zips '{\"type\":\"artifact\",\"url\":\"/path/to/dir/with/zips\"}'\n \
composer repo add baz vcs https://example.org --before foo\n \
composer repo add qux vcs https://example.org --after bar\n \
composer repo remove foo\n \
composer repo set-url foo https://git.example.org/acme/foo\n \
composer repo get-url foo\n \
composer repo disable packagist.org\n \
composer repo enable packagist.org\n\n\
Use --global/-g to alter the global config.json instead.\n\
Use --file to alter a specific file.",
);
Ok(())
}
fn execute(
&self,
input: std::rc::Rc>,
_output: std::rc::Rc>,
) -> anyhow::Result {
let action = strtolower(
input
.borrow()
.get_argument("action")?
.as_string()
.unwrap_or(""),
);
let name = input
.borrow()
.get_argument("name")?
.as_string()
.map(|s| s.to_string());
let arg1 = input
.borrow()
.get_argument("arg1")?
.as_string()
.map(|s| s.to_string());
let arg2 = input
.borrow()
.get_argument("arg2")?
.as_string()
.map(|s| s.to_string());
let config_file = self.config_file.borrow().as_ref().unwrap().clone();
let config_data = config_file.borrow_mut().read()?;
let config_file_path = config_file.borrow().get_path().to_string();
let config_data_map: IndexMap = match config_data {
PhpMixed::Array(m) => m.into_iter().collect(),
_ => IndexMap::new(),
};
let config = self.config.borrow().as_ref().unwrap().clone();
config
.borrow_mut()
.merge(&config_data_map, &config_file_path);
let repos = config.borrow().get_repositories();
match action.as_str() {
"list" | "ls" | "show" => {
self.list_repositories(repos);
Ok(0)
}
"add" => {
if name.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "You must pass a repository name. Example: composer repo add foo vcs https://example.org".to_string(),
code: 0,
}));
}
if arg1.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "You must pass the type and a url, or a JSON string.".to_string(),
code: 0,
}));
}
let arg1_str = arg1.as_deref().unwrap();
let repo_config: PhpMixed = if Preg::is_match(r"{^\s*\{}", arg1_str) {
JsonFile::parse_json(Some(arg1_str), None)?
} else {
if arg2.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "You must pass the type and a url. Example: composer repo add foo vcs https://example.org".to_string(),
code: 0,
}));
}
let mut m = IndexMap::new();
m.insert("type".to_string(), PhpMixed::String(arg1_str.to_string()));
m.insert("url".to_string(), PhpMixed::String(arg2.unwrap()));
PhpMixed::Array(m)
};
let before = input
.borrow()
.get_option("before")?
.as_string()
.map(|s| s.to_string());
let after = input
.borrow()
.get_option("after")?
.as_string()
.map(|s| s.to_string());
if before.is_some() && after.is_some() {
return Err(anyhow::anyhow!(RuntimeException {
message: "You can not combine --before and --after".to_string(),
code: 0,
}));
}
if before.is_some() || after.is_some() {
if matches!(repo_config, PhpMixed::Bool(false)) {
return Err(anyhow::anyhow!(RuntimeException {
message: "Cannot use --before/--after with boolean repository values"
.to_string(),
code: 0,
}));
}
let reference_name = before.as_deref().or(after.as_deref()).unwrap();
let offset: i64 = if after.is_some() { 1 } else { 0 };
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.insert_repository(
name.as_deref().unwrap(),
repo_config.clone(),
reference_name,
offset,
)?;
return Ok(0);
}
let append = input
.borrow()
.get_option("append")?
.as_bool()
.unwrap_or(false);
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.add_repository(name.as_deref().unwrap(), repo_config.clone(), append)?;
Ok(0)
}
"remove" | "rm" | "delete" => {
if name.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "You must pass the repository name to remove.".to_string(),
code: 0,
}));
}
let name_str = name.as_deref().unwrap();
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.remove_repository(name_str)?;
if ["packagist", "packagist.org"].contains(&name_str) {
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.add_repository("packagist.org", PhpMixed::Null, false)?;
}
Ok(0)
}
"set-url" | "seturl" => {
if name.is_none() || arg1.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "Usage: composer repo set-url ".to_string(),
code: 0,
}));
}
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.set_repository_url(name.as_deref().unwrap(), arg1.as_deref().unwrap());
Ok(0)
}
"get-url" | "geturl" => {
if name.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "Usage: composer repo get-url ".to_string(),
code: 0,
}));
}
let name_str = name.as_deref().unwrap();
if let Some(repo) = repos.get(name_str)
&& let PhpMixed::Array(ref repo_map) = *repo
{
let url = repo_map.get("url").and_then(|v| v.as_string());
if let Some(url) = url {
self.get_io().write(url);
return Ok(0);
}
return Err(anyhow::anyhow!(InvalidArgumentException {
message: format!("The {} repository does not have a URL", name_str),
code: 0,
}));
}
for (_key, val) in &repos {
if let PhpMixed::Array(ref repo_map) = *val
&& let Some(n) = repo_map.get("name").and_then(|v| v.as_string())
&& n == name_str
{
let url = repo_map.get("url").and_then(|v| v.as_string());
if let Some(url) = url {
self.get_io().write(url);
return Ok(0);
}
return Err(anyhow::anyhow!(InvalidArgumentException {
message: format!("The {} repository does not have a URL", name_str),
code: 0,
}));
}
}
Err(anyhow::anyhow!(InvalidArgumentException {
message: format!("There is no {} repository defined", name_str),
code: 0,
}))
}
"disable" => {
if name.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "Usage: composer repo disable packagist.org".to_string(),
code: 0,
}));
}
let name_str = name.as_deref().unwrap();
if ["packagist", "packagist.org"].contains(&name_str) {
let append = input
.borrow()
.get_option("append")?
.as_bool()
.unwrap_or(false);
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.add_repository("packagist.org", PhpMixed::Bool(false), append);
return Ok(0);
}
Err(anyhow::anyhow!(RuntimeException {
message: "Only packagist.org can be enabled/disabled using this command. Use add/remove for other repositories.".to_string(),
code: 0,
}))
}
"enable" => {
if name.is_none() {
return Err(anyhow::anyhow!(RuntimeException {
message: "Usage: composer repo enable packagist.org".to_string(),
code: 0,
}));
}
let name_str = name.as_deref().unwrap();
if ["packagist", "packagist.org"].contains(&name_str) {
self.config_source
.borrow_mut()
.as_mut()
.unwrap()
.remove_repository("packagist.org");
return Ok(0);
}
Err(anyhow::anyhow!(RuntimeException {
message: "Only packagist.org can be enabled/disabled using this command."
.to_string(),
code: 0,
}))
}
_ => Err(anyhow::anyhow!(InvalidArgumentException {
message: format!(
"Unknown action \"{}\". Use list, add, remove, set-url, get-url, enable, disable",
action
),
code: 0,
})),
}
}
fn initialize(
&self,
input: Rc>,
output: Rc>,
) -> anyhow::Result<()> {
::initialize(
self, input, output,
)
}
shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
}
impl BaseCommand for RepositoryCommand {
fn command_data(
&self,
) -> &shirabe_external_packages::symfony::console::command::command::CommandData {
self.base_command_data.command_data()
}
crate::delegate_base_command_trait_impls_to_inner!(base_command_data);
}
impl BaseConfigCommand for RepositoryCommand {
fn config(&self) -> Option>> {
self.config.borrow().clone()
}
fn set_config(&self, config: Option>>) {
*self.config.borrow_mut() = config;
}
fn config_file(&self) -> Option>> {
self.config_file.borrow().clone()
}
fn set_config_file(&self, file: Option>>) {
*self.config_file.borrow_mut() = file;
}
fn set_config_source(&self, source: Option) {
*self.config_source.borrow_mut() = source;
}
}