diff --git a/features/package-install.feature b/features/package-install.feature
index 9aaaf2c1e..96b1e2206 100644
--- a/features/package-install.feature
+++ b/features/package-install.feature
@@ -128,7 +128,7 @@ Feature: Install WP-CLI packages
Then the return code should be 0
And STDERR should contain:
"""
- Warning: Package name mismatch...Updating the name with correct value.
+ Warning: Package name mismatch...Updating from git name 'wp-cli-test/repository-name' to composer.json name 'wp-cli-test/package-name'.
"""
And STDOUT should contain:
"""
@@ -195,24 +195,39 @@ Feature: Install WP-CLI packages
"""
@github-api
- Scenario: Install a package from a Git URL with mixed case git name but lower case composer.json name
+ Scenario: Install a package from a Git URL with mixed-case git name but lowercase composer.json name
Given an empty directory
When I try `wp package install https://github.com/CapitalWPCLI/examplecommand.git`
Then the return code should be 0
And STDERR should contain:
"""
- Warning: Package name mismatch...Updating the name with correct value.
+ Warning: Package name mismatch...Updating from git name 'CapitalWPCLI/examplecommand' to composer.json name 'capitalwpcli/examplecommand'.
+ """
+ And STDOUT should contain:
+ """
+ Installing package capitalwpcli/examplecommand (dev-master)
+ Updating {PACKAGE_PATH}composer.json to require the package...
+ Registering https://github.com/CapitalWPCLI/examplecommand.git as a VCS repository...
+ Using Composer to install the package...
"""
And STDOUT should contain:
"""
Success: Package installed.
"""
+ And the {PACKAGE_PATH}composer.json file should contain:
+ """
+ "capitalwpcli/examplecommand"
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ "CapitalWPCLI/examplecommand"
+ """
- When I run `wp package list --fields=name,pretty_name`
+ When I run `wp package list --fields=name`
Then STDOUT should be a table containing rows:
- | name | pretty_name |
- | capitalwpcli/examplecommand | capitalwpcli/examplecommand |
+ | name |
+ | capitalwpcli/examplecommand |
When I run `wp hello-world`
Then STDOUT should contain:
@@ -221,7 +236,7 @@ Feature: Install WP-CLI packages
"""
@github-api
- Scenario: Install a package from a Git URL with mixed case git name and the same mixed case composer.json name
+ Scenario: Install a package from a Git URL with mixed-case git name and the same mixed-case composer.json name
Given an empty directory
When I run `wp package install https://github.com/gitlost/TestMixedCaseCommand.git`
@@ -230,11 +245,19 @@ Feature: Install WP-CLI packages
"""
Success: Package installed.
"""
+ And the {PACKAGE_PATH}composer.json file should contain:
+ """
+ "gitlost/TestMixedCaseCommand"
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ mixed
+ """
- When I run `wp package list --fields=name,pretty_name`
+ When I run `wp package list --fields=name`
Then STDOUT should be a table containing rows:
- | name | pretty_name |
- | gitlost/testmixedcasecommand | gitlost/TestMixedCaseCommand |
+ | name |
+ | gitlost/TestMixedCaseCommand |
When I run `wp TestMixedCaseCommand`
Then STDOUT should contain:
@@ -467,6 +490,103 @@ Feature: Install WP-CLI packages
schlessera/test-command
"""
+ Scenario: Install a package from the wp-cli package index with a mixed-case name
+ Given an empty directory
+
+ # Install and uninstall with case-sensitive name
+ When I run `wp package install GeekPress/wp-rocket-cli`
+ Then STDERR should be empty
+ And STDOUT should contain:
+ """
+ Installing package GeekPress/wp-rocket-cli (dev-master)
+ Updating {PACKAGE_PATH}composer.json to require the package...
+ Using Composer to install the package...
+ """
+ And STDOUT should contain:
+ """
+ Success: Package installed.
+ """
+ And the {PACKAGE_PATH}composer.json file should contain:
+ """
+ GeekPress/wp-rocket-cli
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ geek
+ """
+
+ When I run `wp package list --fields=name`
+ Then STDOUT should be a table containing rows:
+ | name |
+ | GeekPress/wp-rocket-cli |
+
+ When I run `wp help rocket`
+ Then STDOUT should contain:
+ """
+ wp rocket
+ """
+
+ When I run `wp package uninstall GeekPress/wp-rocket-cli`
+ Then STDOUT should contain:
+ """
+ Removing require statement from {PACKAGE_PATH}composer.json
+ """
+ And STDOUT should contain:
+ """
+ Success: Uninstalled package.
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ rocket
+ """
+
+ # Install with lowercase name (for BC - no warning) and uninstall with lowercase name (for BC and convenience)
+ When I run `wp package install geekpress/wp-rocket-cli`
+ Then STDERR should be empty
+ And STDOUT should contain:
+ """
+ Installing package GeekPress/wp-rocket-cli (dev-master)
+ Updating {PACKAGE_PATH}composer.json to require the package...
+ Using Composer to install the package...
+ """
+ And STDOUT should contain:
+ """
+ Success: Package installed.
+ """
+ And the {PACKAGE_PATH}composer.json file should contain:
+ """
+ GeekPress/wp-rocket-cli
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ geek
+ """
+
+ When I run `wp package list --fields=name`
+ Then STDOUT should be a table containing rows:
+ | name |
+ | GeekPress/wp-rocket-cli |
+
+ When I run `wp help rocket`
+ Then STDOUT should contain:
+ """
+ wp rocket
+ """
+
+ When I run `wp package uninstall geekpress/wp-rocket-cli`
+ Then STDOUT should contain:
+ """
+ Removing require statement from {PACKAGE_PATH}composer.json
+ """
+ And STDOUT should contain:
+ """
+ Success: Uninstalled package.
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ rocket
+ """
+
Scenario: Install a package in a local zip
Given an empty directory
And I run `wget -O google-sitemap-generator-cli.zip https://github.com/wp-cli/google-sitemap-generator-cli/archive/master.zip`
@@ -511,6 +631,60 @@ Feature: Install WP-CLI packages
wp-cli/google-sitemap-generator-cli
"""
+ Scenario: Install a package from Git using a shortened mixed-case package identifier but lowercase composer.json name
+ Given an empty directory
+
+ When I try `wp package install CapitalWPCLI/examplecommand`
+ Then the return code should be 0
+ And STDERR should contain:
+ """
+ Warning: Package name mismatch...Updating from git name 'CapitalWPCLI/examplecommand' to composer.json name 'capitalwpcli/examplecommand'.
+ """
+ And STDOUT should contain:
+ """
+ Installing package capitalwpcli/examplecommand (dev-master)
+ Updating {PACKAGE_PATH}composer.json to require the package...
+ Registering https://github.com/CapitalWPCLI/examplecommand.git as a VCS repository...
+ Using Composer to install the package...
+ """
+ And STDOUT should contain:
+ """
+ Success: Package installed.
+ """
+ And the {PACKAGE_PATH}composer.json file should contain:
+ """
+ "capitalwpcli/examplecommand"
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ "CapitalWPCLI/examplecommand"
+ """
+
+ When I run `wp package list --fields=name`
+ Then STDOUT should be a table containing rows:
+ | name |
+ | capitalwpcli/examplecommand |
+
+ When I run `wp hello-world`
+ Then STDOUT should contain:
+ """
+ Success: Hello world.
+ """
+
+ When I run `wp package uninstall capitalwpcli/examplecommand`
+ Then STDOUT should contain:
+ """
+ Removing require statement from {PACKAGE_PATH}composer.json
+ """
+ And STDOUT should contain:
+ """
+ Success: Uninstalled package.
+ """
+ And the {PACKAGE_PATH}composer.json file should not contain:
+ """
+ capital
+ """
+
Scenario: Install a package from a remote ZIP
Given an empty directory
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 000000000..529dae0ef
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,11 @@
+
+
+
+ tests/
+ tests/
+
+
+
diff --git a/src/Package_Command.php b/src/Package_Command.php
index f1287b52a..f313920fd 100644
--- a/src/Package_Command.php
+++ b/src/Package_Command.php
@@ -8,7 +8,6 @@
use \Composer\IO\NullIO;
use \Composer\Installer;
use \Composer\Json\JsonFile;
-use \Composer\Json\JsonManipulator;
use \Composer\Package;
use \Composer\Package\BasePackage;
use \Composer\Package\PackageInterface;
@@ -22,6 +21,7 @@
use \WP_CLI\ComposerIO;
use \WP_CLI\Extractor;
use \WP_CLI\Utils;
+use \WP_CLI\JsonManipulator;
/**
* Runs WP-CLI package manager commands.
@@ -114,9 +114,7 @@ class Package_Command extends WP_CLI_Command {
* * authors
* * version
*
- * These fields are optionally available:
- *
- * * pretty_name
+ * There are no optionally available fields.
*
* ## EXAMPLES
*
@@ -213,24 +211,7 @@ public function install( $args, $assoc_args ) {
$git_package = $package_name;
preg_match( '#([^:\/]+\/[^\/]+)\.git#', $package_name, $matches );
if ( ! empty( $matches[1] ) ) {
- $package_name = $matches[1];
-
- // Generate raw git URL of composer.json file.
- $raw_content_url = 'https://raw.githubusercontent.com/' . $package_name . '/master/composer.json';
- $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
- $headers = $github_token ? array( 'Authorization' => 'token ' . $github_token ) : array();
-
- // Convert composer.json JSON to Array.
- $composer_content_as_array = json_decode( WP_CLI\Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers )->body, true );
-
- // Package name in composer.json that is hosted on GitHub.
- $package_name_on_repo = $composer_content_as_array['name'];
-
- // If package name and repository name are not identical, then fix it.
- if ( $package_name !== $package_name_on_repo ) {
- $package_name = $package_name_on_repo;
- WP_CLI::warning( 'Package name mismatch...Updating the name with correct value.' );
- }
+ $package_name = $this->check_git_package_name( $matches[1] );
} else {
WP_CLI::error( "Couldn't parse package name from expected path '/'." );
}
@@ -275,12 +256,18 @@ public function install( $args, $assoc_args ) {
list( $package_name, $version ) = explode( ':', $package_name );
}
$package = $this->get_package_by_shortened_identifier( $package_name );
- if ( $this->is_git_repository( $package ) ) {
- $git_package = $package;
- }
if ( ! $package ) {
WP_CLI::error( "Invalid package." );
}
+ if ( is_string( $package ) ) {
+ if ( $this->is_git_repository( $package ) ) {
+ $git_package = $package;
+ $package_name = $this->check_git_package_name( $package_name );
+ }
+ } elseif ( $package_name !== $package->getPrettyName() ) {
+ // BC support for specifying lowercase names for mixed-case package index packages - don't bother warning.
+ $package_name = $package->getPrettyName();
+ }
}
WP_CLI::log( sprintf( "Installing package %s (%s)", $package_name, $version ) );
@@ -293,15 +280,15 @@ public function install( $args, $assoc_args ) {
$json_manipulator = new JsonManipulator( $composer_backup );
$json_manipulator->addMainKey( 'name', 'wp-cli/wp-cli' );
$json_manipulator->addMainKey( 'version', self::get_wp_cli_version_composer() );
- $json_manipulator->addLink( 'require', $package_name, $version );
+ $json_manipulator->addLink( 'require', $package_name, $version, false /*sortPackages*/, true /*caseInsensitive*/ );
$json_manipulator->addConfigSetting( 'secure-http', true );
if ( $git_package ) {
WP_CLI::log( sprintf( 'Registering %s as a VCS repository...', $git_package ) );
- $json_manipulator->addRepository( $package_name, array( 'type' => 'vcs', 'url' => $git_package ) );
+ $json_manipulator->addSubNode( 'repositories', $package_name, array( 'type' => 'vcs', 'url' => $git_package ), true /*caseInsensitive*/ );
} else if ( $dir_package ) {
WP_CLI::log( sprintf( 'Registering %s as a path repository...', $dir_package ) );
- $json_manipulator->addRepository( $package_name, array( 'type' => 'path', 'url' => $dir_package ) );
+ $json_manipulator->addSubNode( 'repositories', $package_name, array( 'type' => 'path', 'url' => $dir_package ), true /*caseInsensitive*/ );
}
$composer_backup_decoded = json_decode( $composer_backup, true );
// If the composer file does not contain the current package index repository, refresh the repository definition.
@@ -378,7 +365,6 @@ public function install( $args, $assoc_args ) {
* These fields are optionally available:
*
* * description
- * * pretty_name
*
* ## EXAMPLES
*
@@ -507,6 +493,7 @@ public function uninstall( $args ) {
if ( false === ( $package = $this->get_installed_package_by_name( $package_name ) ) ) {
WP_CLI::error( "Package not installed." );
}
+ $package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json.
$composer_json_obj = $this->get_composer_json();
@@ -515,14 +502,11 @@ public function uninstall( $args ) {
WP_CLI::log( sprintf( 'Removing require statement from %s', $json_path ) );
$composer_backup = file_get_contents( $composer_json_obj->getPath() );
$manipulator = new JsonManipulator( $composer_backup );
- $manipulator->removeSubNode( 'require', $package_name );
- $composer_json_array = json_decode( $composer_backup );
+ $manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ );
// Remove the 'repository' details from composer.json.
- if ( is_object( $composer_json_array ) && property_exists( $composer_json_array->repositories, $package_name ) ) {
- WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) );
- $manipulator->removeRepository( $package_name );
- }
+ WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) );
+ $manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ );
file_put_contents( $composer_json_obj->getPath(), $manipulator->getContents() );
@@ -664,12 +648,12 @@ private function show_packages( $context, $packages, $assoc_args ) {
$list = array();
foreach ( $packages as $package ) {
- $name = $package->getName();
+ $name = $package->getPrettyName();
if ( isset( $list[ $name ] ) ) {
$list[ $name ]['version'][] = $package->getPrettyVersion();
} else {
$package_output = array();
- $package_output['name'] = $package->getName();
+ $package_output['name'] = $package->getPrettyName();
$package_output['description'] = $package->getDescription();
$package_output['authors'] = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) );
$package_output['version'] = array( $package->getPrettyVersion() );
@@ -684,7 +668,7 @@ private function show_packages( $context, $packages, $assoc_args ) {
}
$package_output['update'] = $update;
$package_output['update_version'] = $update_version;
- $package_output['pretty_name'] = $package->getPrettyName();
+ $package_output['pretty_name'] = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8.
$list[ $package_output['name'] ] = $package_output;
}
}
@@ -711,8 +695,13 @@ private function show_packages( $context, $packages, $assoc_args ) {
*/
private function get_package_by_shortened_identifier( $package_name ) {
// Check the package index first, so we don't break existing behavior.
+ $lc_package_name = strtolower( $package_name ); // For BC check.
foreach( $this->get_community_packages() as $package ) {
- if ( $package_name == $package->getName() ) {
+ if ( $package_name === $package->getPrettyName() ) {
+ return $package;
+ }
+ // For BC allow getting by lowercase name.
+ if ( $lc_package_name === $package->getName() ) {
return $package;
}
}
@@ -744,11 +733,16 @@ private function get_installed_packages() {
if ( empty( $installed_package_keys ) ) {
return array();
}
+ // For use by legacy incorrect name check.
+ $lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys );
$installed_packages = array();
foreach( $repo->getCanonicalPackages() as $package ) {
- // Use pretty name as it's case sensitive.
+ // Use pretty name as it's case sensitive and what's in composer.json (or at least should be).
if ( in_array( $package->getPrettyName(), $installed_package_keys, true ) ) {
$installed_packages[] = $package;
+ } elseif ( false !== ( $idx = array_search( $package->getName(), $lc_installed_package_keys, true ) ) ) { // Legacy incorrect name check.
+ WP_CLI::warning( sprintf( "Found package '%s' misnamed '%s' in '%s'.", $package->getPrettyName(), $installed_package_keys[ $idx ], $this->get_composer_json_path() ) );
+ $installed_packages[] = $package;
}
}
return $installed_packages;
@@ -759,7 +753,11 @@ private function get_installed_packages() {
*/
private function get_installed_package_by_name( $package_name ) {
foreach( $this->get_installed_packages() as $package ) {
- if ( $package_name == $package->getName() ) {
+ if ( $package_name === $package->getPrettyName() ) {
+ return $package;
+ }
+ // Also check non-pretty (lowercase) name in case of legacy incorrect name.
+ if ( $package_name === $package->getName() ) {
return $package;
}
}
@@ -911,7 +909,7 @@ private function create_default_composer_json( $composer_path ) {
*/
private function find_latest_package( PackageInterface $package, Composer $composer, $phpVersion, $minorOnly = false ) {
// find the latest version allowed in this pool
- $name = $package->getName();
+ $name = $package->getPrettyName();
$versionSelector = new VersionSelector($this->get_pool($composer));
$stability = $composer->getPackage()->getMinimumStability();
$flags = $composer->getPackage()->getStabilityFlags();
@@ -951,6 +949,37 @@ private function is_git_repository( $package ) {
return '.git' === strtolower( substr( $package, -4, 4 ) );
}
+ /**
+ * Check that `$package_name` matches the name in the repo composer.json, and return corrected value if not.
+ */
+ private function check_git_package_name( $package_name ) {
+ // Generate raw git URL of composer.json file.
+ $raw_content_url = 'https://raw.githubusercontent.com/' . $package_name . '/master/composer.json';
+ $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting.
+ $headers = $github_token ? array( 'Authorization' => 'token ' . $github_token ) : array();
+
+ $response = WP_CLI\Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers );
+ if ( 20 != substr( $response->status_code, 0, 2 ) ) {
+ WP_CLI::error( sprintf( "Couldn't download package from '%s' (HTTP code %d).", $raw_content_url, $response->status_code ) );
+ }
+
+ // Convert composer.json JSON to Array.
+ $composer_content_as_array = json_decode( $response->body, true );
+ if ( null === $composer_content_as_array ) {
+ WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $raw_content_url ) );
+ }
+
+ // Package name in composer.json that is hosted on GitHub.
+ $package_name_on_repo = $composer_content_as_array['name'];
+
+ // If package name and repository name are not identical, then fix it.
+ if ( $package_name !== $package_name_on_repo ) {
+ WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) );
+ $package_name = $package_name_on_repo;
+ }
+ return $package_name;
+ }
+
/**
* Set `COMPOSER_AUTH` environment variable (which Composer merges into the config setup in `Composer\Factory::createConfig()`) depending on available environment variables.
* Avoids authorization failures when accessing various sites.
diff --git a/src/WP_CLI/JsonManipulator.php b/src/WP_CLI/JsonManipulator.php
new file mode 100644
index 000000000..1a4e6dc70
--- /dev/null
+++ b/src/WP_CLI/JsonManipulator.php
@@ -0,0 +1,558 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace WP_CLI; // WP_CLI
+
+use Composer\Json\JsonFile; // WP_CLI
+use Composer\Repository\PlatformRepository;
+
+/**
+ * @author Jordi Boggiano
+ */
+class JsonManipulator
+{
+ private static $DEFINES = '(?(DEFINE)
+ (? -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? )
+ (? true | false | null )
+ (? " ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " )
+ (? \[ (?: (?&json) \s* (?: , (?&json) \s* )* )? \s* \] )
+ (? \s* (?&string) \s* : (?&json) \s* )
+ (?