diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php
index dbf41ceb9..28ea81028 100644
--- a/src/Composer/Command/ShowCommand.php
+++ b/src/Composer/Command/ShowCommand.php
@@ -294,6 +294,16 @@ EOT
$output->writeln('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference()));
$output->writeln('names : ' . implode(', ', $package->getNames()));
+ if ($package->isAbandoned()) {
+ $replacement = ($package->getReplacementPackage() !== null)
+ ? ' The author suggests using the ' . $package->getReplacementPackage(). ' package instead.'
+ : null;
+
+ $output->writeln(
+ sprintf('Attention: This package is abandoned and no longer maintained.%s', $replacement)
+ );
+ }
+
if ($package->getSupport()) {
$output->writeln("\nsupport");
foreach ($package->getSupport() as $type => $value) {
diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php
index 294ab0a0a..b76155a5a 100644
--- a/src/Composer/Installer.php
+++ b/src/Composer/Installer.php
@@ -31,6 +31,7 @@ use Composer\Installer\NoopInstaller;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Package\AliasPackage;
+use Composer\Package\CompletePackage;
use Composer\Package\Link;
use Composer\Package\LinkConstraint\VersionConstraint;
use Composer\Package\Locker;
@@ -240,6 +241,25 @@ class Installer
}
}
+ # Find abandoned packages and warn user
+ foreach ($localRepo->getPackages() as $package) {
+ if (!$package instanceof CompletePackage || !$package->isAbandoned()) {
+ continue;
+ }
+
+ $replacement = (is_string($package->getReplacementPackage()))
+ ? 'Use ' . $package->getReplacementPackage() . ' instead'
+ : 'No replacement was suggested';
+
+ $this->io->write(
+ sprintf(
+ "Package %s is abandoned, you should avoid using it. %s.",
+ $package->getPrettyName(),
+ $replacement
+ )
+ );
+ }
+
if (!$this->dryRun) {
// write lock
if ($this->update || !$this->locker->isLocked()) {
diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php
index 183f2c740..3ab9c944d 100644
--- a/src/Composer/Package/AliasPackage.php
+++ b/src/Composer/Package/AliasPackage.php
@@ -333,6 +333,14 @@ class AliasPackage extends BasePackage implements CompletePackageInterface
{
return $this->aliasOf->getArchiveExcludes();
}
+ public function isAbandoned()
+ {
+ return $this->aliasOf->isAbandoned();
+ }
+ public function getReplacementPackage()
+ {
+ return $this->aliasOf->getReplacementPackage();
+ }
public function __toString()
{
return parent::__toString().' (alias of '.$this->aliasOf->getVersion().')';
diff --git a/src/Composer/Package/CompletePackage.php b/src/Composer/Package/CompletePackage.php
index a884174af..27c9abeca 100644
--- a/src/Composer/Package/CompletePackage.php
+++ b/src/Composer/Package/CompletePackage.php
@@ -27,6 +27,7 @@ class CompletePackage extends Package implements CompletePackageInterface
protected $homepage;
protected $scripts = array();
protected $support = array();
+ protected $abandoned = false;
/**
* @param array $scripts
@@ -169,4 +170,30 @@ class CompletePackage extends Package implements CompletePackageInterface
{
return $this->support;
}
+
+ /**
+ * @return boolean
+ */
+ public function isAbandoned()
+ {
+ return (boolean) $this->abandoned;
+ }
+
+ /**
+ * @param boolean|string $abandoned
+ */
+ public function setAbandoned($abandoned)
+ {
+ $this->abandoned = $abandoned;
+ }
+
+ /**
+ * If the package is abandoned and has a suggested replacement, this method returns it
+ *
+ * @return string|null
+ */
+ public function getReplacementPackage()
+ {
+ return is_string($this->abandoned)? $this->abandoned : null;
+ }
}
diff --git a/src/Composer/Package/CompletePackageInterface.php b/src/Composer/Package/CompletePackageInterface.php
index a341766d3..8263a6535 100644
--- a/src/Composer/Package/CompletePackageInterface.php
+++ b/src/Composer/Package/CompletePackageInterface.php
@@ -78,4 +78,18 @@ interface CompletePackageInterface extends PackageInterface
* @return array
*/
public function getSupport();
+
+ /**
+ * Returns if the package is abandoned or not
+ *
+ * @return boolean
+ */
+ public function isAbandoned();
+
+ /**
+ * If the package is abandoned and has a suggested replacement, this method returns it
+ *
+ * @return string
+ */
+ public function getReplacementPackage();
}
diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php
index 67318c04a..714c5183b 100644
--- a/src/Composer/Package/Dumper/ArrayDumper.php
+++ b/src/Composer/Package/Dumper/ArrayDumper.php
@@ -105,6 +105,10 @@ class ArrayDumper
if (isset($data['keywords']) && is_array($data['keywords'])) {
sort($data['keywords']);
}
+
+ if ($package->isAbandoned()) {
+ $data['abandoned'] = $package->getReplacementPackage() ?: true;
+ }
}
if ($package instanceof RootPackageInterface) {
diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php
index d25ea5546..243b7b574 100644
--- a/src/Composer/Package/Loader/ArrayLoader.php
+++ b/src/Composer/Package/Loader/ArrayLoader.php
@@ -195,6 +195,10 @@ class ArrayLoader implements LoaderInterface
if (isset($config['support'])) {
$package->setSupport($config['support']);
}
+
+ if (isset($config['abandoned'])) {
+ $package->setAbandoned($config['abandoned']);
+ }
}
if ($aliasNormalized = $this->getBranchAlias($config)) {
diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test
new file mode 100644
index 000000000..18f47732e
--- /dev/null
+++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test
@@ -0,0 +1,36 @@
+--TEST--
+Abandoned packages are flagged
+--COMPOSER--
+{
+ "repositories": [
+ {
+ "type": "package",
+ "package": [
+ { "name": "a/a", "version": "1.0.0", "abandoned": true }
+ ]
+ },
+ {
+ "type": "package",
+ "package": [
+ { "name": "c/c", "version": "1.0.0", "abandoned": "b/b" }
+ ]
+ }
+ ],
+ "require": {
+ "a/a": "1.0.0",
+ "c/c": "1.0.0"
+ }
+}
+--RUN--
+install
+--EXPECT-OUTPUT--
+Loading composer repositories with package information
+Installing dependencies (including require-dev)
+Package a/a is abandoned, you should avoid using it. No replacement was suggested.
+Package c/c is abandoned, you should avoid using it. Use b/b instead.
+Writing lock file
+Generating autoload files
+
+--EXPECT--
+Installing a/a (1.0.0)
+Installing c/c (1.0.0)
diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test
index 0fc5fe301..bfe0bb8a8 100644
--- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test
+++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test
@@ -70,4 +70,4 @@ update
"platform-dev": []
}
--EXPECT--
-Updating a/a (dev-master 1234) to a/a (dev-master master)
\ No newline at end of file
+Updating a/a (dev-master 1234) to a/a (dev-master master)
diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php
index 03cc10628..40dd03313 100644
--- a/tests/Composer/Test/InstallerTest.php
+++ b/tests/Composer/Test/InstallerTest.php
@@ -306,7 +306,7 @@ class InstallerTest extends TestCase
die(sprintf('Test "%s" is not valid, did not match the expected format.', str_replace($fixturesDir.'/', '', $file)));
}
- $tests[] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode);
+ $tests[basename($file)] = array(str_replace($fixturesDir.'/', '', $file), $message, $condition, $composer, $lock, $installed, $run, $expectLock, $expectOutput, $expect, $expectExitCode);
}
return $tests;
diff --git a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php
index 5576e3d05..f1889a1ce 100644
--- a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php
+++ b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php
@@ -62,12 +62,33 @@ class ArrayDumperTest extends \PHPUnit_Framework_TestCase
$this->assertSame('dev', $config['minimum-stability']);
}
+ public function testDumpAbandoned()
+ {
+ $this->packageExpects('isAbandoned', true);
+ $this->packageExpects('getReplacementPackage', true);
+
+ $config = $this->dumper->dump($this->package);
+
+ $this->assertSame(true, $config['abandoned']);
+ }
+
+ public function testDumpAbandonedReplacement()
+ {
+ $this->packageExpects('isAbandoned', true);
+ $this->packageExpects('getReplacementPackage', 'foo/bar');
+
+ $config = $this->dumper->dump($this->package);
+
+ $this->assertSame('foo/bar', $config['abandoned']);
+ }
+
/**
* @dataProvider getKeys
*/
public function testKeys($key, $value, $method = null, $expectedValue = null)
{
$this->packageExpects('get'.ucfirst($method ?: $key), $value);
+ $this->packageExpects('isAbandoned', $value);
$config = $this->dumper->dump($this->package);
diff --git a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php
index 04a537abd..6e4e2f5ee 100644
--- a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php
+++ b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php
@@ -117,7 +117,8 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase
'archive' => array(
'exclude' => array('/foo/bar', 'baz', '!/foo/bar/baz'),
),
- 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem'))
+ 'transport-options' => array('ssl' => array('local_cert' => '/opt/certs/test.pem')),
+ 'abandoned' => 'foo/bar'
);
$package = $this->loader->load($config);
@@ -138,4 +139,28 @@ class ArrayLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertInstanceOf('Composer\Package\AliasPackage', $package);
$this->assertEquals('1.0.x-dev', $package->getPrettyVersion());
}
+
+ public function testAbandoned()
+ {
+ $config = array(
+ 'name' => 'A',
+ 'version' => '1.2.3.4',
+ 'abandoned' => 'foo/bar'
+ );
+
+ $package = $this->loader->load($config);
+ $this->assertTrue($package->isAbandoned());
+ $this->assertEquals('foo/bar', $package->getReplacementPackage());
+ }
+
+ public function testNotAbandoned()
+ {
+ $config = array(
+ 'name' => 'A',
+ 'version' => '1.2.3.4'
+ );
+
+ $package = $this->loader->load($config);
+ $this->assertFalse($package->isAbandoned());
+ }
}