diff --git a/.gitattributes b/.gitattributes
index 8068826..243b015 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,6 +2,6 @@
/.github export-ignore
.gitattributes export-ignore
.gitignore export-ignore
-.php_cs.dist export-ignore
+.php-cs-fixer.dist.php export-ignore
phpunit.xml.dist export-ignore
infection.json.dist export-ignore
diff --git a/README.md b/README.md
index da799b7..abd6bab 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,21 @@ composer diff -p # include platform dependencies
composer diff -f json # Output as JSON instead of table
```
+### Strict mode
+
+To help you control your dependencies, you may pass `--strict` option when running in CI. If there are any changes detected, a non-zero exit code will be returned.
+
+Exit code of the command is built using following bit flags:
+
+* `0` - OK.
+* `1` - General error.
+* `2` - There were changes in prod packages.
+* `4` - There were changes is dev packages.
+* `8` - There were downgrades in prod packages.
+* `16` - There were downgrades in dev packages.
+
+You may check for individual flags or simply check if the status is greater or equal 8 if you don't want to downgrade any package.
+
# Similar packages
While there are several existing packages offering similar functionality:
diff --git a/src/Command/DiffCommand.php b/src/Command/DiffCommand.php
index 98f2346..4b92702 100644
--- a/src/Command/DiffCommand.php
+++ b/src/Command/DiffCommand.php
@@ -3,6 +3,8 @@
namespace IonBazan\ComposerDiff\Command;
use Composer\Command\BaseCommand;
+use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\DependencyResolver\Operation\UpdateOperation;
use IonBazan\ComposerDiff\Formatter\Formatter;
use IonBazan\ComposerDiff\Formatter\JsonFormatter;
use IonBazan\ComposerDiff\Formatter\MarkdownListFormatter;
@@ -16,6 +18,10 @@
class DiffCommand extends BaseCommand
{
+ const CHANGES_PROD = 2;
+ const CHANGES_DEV = 4;
+ const DOWNGRADES_PROD = 8;
+ const DOWNGRADES_DEV = 16;
/**
* @var PackageDiff
*/
@@ -54,43 +60,61 @@ protected function configure()
->addOption('with-links', 'l', InputOption::VALUE_NONE, 'Include compare/release URLs')
->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format (mdtable, mdlist, json)', 'mdtable')
->addOption('gitlab-domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Extra Gitlab domains (inherited from Composer config by default)', array())
+ ->addOption('strict', 's', InputOption::VALUE_NONE, 'Return non-zero exit code if there are any changes')
->setHelp(<<<'EOF'
-The %command.name% command displays all dependency changes between two composer.lock files.
-By default, it will compare current filesystem changes with git HEAD:
+The %command.name% command displays all dependency changes between two composer.lock files.
- %command.full_name%
+By default, it will compare current filesystem changes with git HEAD:
+
+ %command.full_name%
To compare with specific branch, pass its name as argument:
- %command.full_name% master
+ %command.full_name% master
You can specify any valid git refs to compare with:
- %command.full_name% HEAD~3 be4aabc
+ %command.full_name% HEAD~3 be4aabc
You can also use more verbose syntax for base and target options:
- %command.full_name% --base master --target composer.lock
+ %command.full_name% --base master --target composer.lock
To compare files in specific path, use following syntax:
- %command.full_name% master:subdirectory/composer.lock /path/to/another/composer.lock
+ %command.full_name% master:subdirectory/composer.lock /path/to/another/composer.lock
-By default, platform dependencies are hidden. Add --with-platform option to include them in the report:
+By default, platform dependencies are hidden. Add --with-platform option to include them in the report:
- %command.full_name% --with-platform
+ %command.full_name% --with-platform
-Use --with-links to include release and compare URLs in the report:
+Use --with-links to include release and compare URLs in the report:
- %command.full_name% --with-links
+ %command.full_name% --with-links
-You can customize output format by specifying it with --format option. Choose between mdtable, mdlist and json:
+You can customize output format by specifying it with --format option. Choose between mdtable, mdlist and json:
+
+ %command.full_name% --format=json
+
+Hide dev dependencies using --no-dev option:
+
+ %command.full_name% --no-dev
+
+Passing --strict option may help you to disallow changes or downgrades by returning non-zero exit code:
+
+ %command.full_name% --strict
- %command.full_name% --format=json
+Exit code
+---------
-Hide dev dependencies using --no-dev option:
+Exit code of the command is built using following bit flags:
- %command.full_name% --no-dev
+* 0 - OK.
+* 1 - General error.
+* 2 - There were changes in prod packages.
+* 4 - There were changes is dev packages.
+* 8 - There were downgrades in prod packages.
+* 16 - There were downgrades in dev packages.
EOF
)
;
@@ -122,7 +146,50 @@ protected function execute(InputInterface $input, OutputInterface $output)
$formatter->render($prodOperations, $devOperations, $withUrls);
- return 0;
+ return $input->getOption('strict') ? $this->getExitCode($prodOperations, $devOperations) : 0;
+ }
+
+ /**
+ * @param OperationInterface[] $prodOperations
+ * @param OperationInterface[] $devOperations
+ *
+ * @return int Exit code
+ */
+ private function getExitCode(array $prodOperations, array $devOperations)
+ {
+ $exitCode = 0;
+
+ if (!empty($prodOperations)) {
+ $exitCode = self::CHANGES_PROD;
+
+ if ($this->hasDowngrades($prodOperations)) {
+ $exitCode |= self::DOWNGRADES_PROD;
+ }
+ }
+
+ if (!empty($devOperations)) {
+ $exitCode |= self::CHANGES_DEV;
+
+ if ($this->hasDowngrades($devOperations)) {
+ $exitCode |= self::DOWNGRADES_DEV;
+ }
+ }
+
+ return $exitCode;
+ }
+
+ /**
+ * @param OperationInterface[] $operations
+ *
+ * @return bool
+ */
+ private function hasDowngrades(array $operations)
+ {
+ $downgrades = array_filter($operations, function (OperationInterface $operation) {
+ return $operation instanceof UpdateOperation && !PackageDiff::isUpgrade($operation);
+ });
+
+ return !empty($downgrades);
}
/**
diff --git a/src/Formatter/AbstractFormatter.php b/src/Formatter/AbstractFormatter.php
index c623bd2..71c8de5 100644
--- a/src/Formatter/AbstractFormatter.php
+++ b/src/Formatter/AbstractFormatter.php
@@ -7,11 +7,8 @@
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\Package\PackageInterface;
-use Composer\Semver\Semver;
-use Composer\Semver\VersionParser;
use IonBazan\ComposerDiff\Url\GeneratorContainer;
use Symfony\Component\Console\Output\OutputInterface;
-use UnexpectedValueException;
abstract class AbstractFormatter implements Formatter
{
@@ -74,21 +71,4 @@ private function getReleaseUrl(PackageInterface $package)
return $generator->getReleaseUrl($package);
}
-
- /**
- * @return bool
- */
- protected static function isUpgrade(UpdateOperation $operation)
- {
- $versionParser = new VersionParser();
- try {
- $normalizedFrom = $versionParser->normalize($operation->getInitialPackage()->getVersion());
- $normalizedTo = $versionParser->normalize($operation->getTargetPackage()->getVersion());
- } catch (UnexpectedValueException $e) {
- return true; // Consider as upgrade if versions are not parsable
- }
- $sorted = Semver::sort(array($normalizedTo, $normalizedFrom));
-
- return $sorted[0] === $normalizedFrom;
- }
}
diff --git a/src/Formatter/JsonFormatter.php b/src/Formatter/JsonFormatter.php
index f9923ad..d6638a9 100644
--- a/src/Formatter/JsonFormatter.php
+++ b/src/Formatter/JsonFormatter.php
@@ -6,6 +6,7 @@
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
+use IonBazan\ComposerDiff\PackageDiff;
class JsonFormatter extends AbstractFormatter
{
@@ -78,7 +79,7 @@ private function transformOperation(OperationInterface $operation)
if ($operation instanceof UpdateOperation) {
return array(
'name' => $operation->getInitialPackage()->getName(),
- 'operation' => self::isUpgrade($operation) ? 'upgrade' : 'downgrade',
+ 'operation' => PackageDiff::isUpgrade($operation) ? 'upgrade' : 'downgrade',
'version_base' => $operation->getInitialPackage()->getFullPrettyVersion(),
'version_target' => $operation->getTargetPackage()->getFullPrettyVersion(),
);
diff --git a/src/Formatter/MarkdownListFormatter.php b/src/Formatter/MarkdownListFormatter.php
index 06edc36..e95e9a5 100644
--- a/src/Formatter/MarkdownListFormatter.php
+++ b/src/Formatter/MarkdownListFormatter.php
@@ -6,6 +6,7 @@
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
+use IonBazan\ComposerDiff\PackageDiff;
class MarkdownListFormatter extends MarkdownFormatter
{
@@ -58,7 +59,7 @@ private function getRow(OperationInterface $operation, $withUrls)
}
if ($operation instanceof UpdateOperation) {
- $isUpgrade = self::isUpgrade($operation);
+ $isUpgrade = PackageDiff::isUpgrade($operation);
return sprintf(
' - %s %s> (%s> => %s>)%s',
diff --git a/src/Formatter/MarkdownTableFormatter.php b/src/Formatter/MarkdownTableFormatter.php
index 1727dbf..761f689 100644
--- a/src/Formatter/MarkdownTableFormatter.php
+++ b/src/Formatter/MarkdownTableFormatter.php
@@ -7,6 +7,7 @@
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use IonBazan\ComposerDiff\Formatter\Helper\Table;
+use IonBazan\ComposerDiff\PackageDiff;
class MarkdownTableFormatter extends MarkdownFormatter
{
@@ -68,7 +69,7 @@ private function getTableRow(OperationInterface $operation)
if ($operation instanceof UpdateOperation) {
return array(
$operation->getInitialPackage()->getName(),
- self::isUpgrade($operation) ? 'Upgraded>' : 'Downgraded>',
+ PackageDiff::isUpgrade($operation) ? 'Upgraded>' : 'Downgraded>',
$operation->getInitialPackage()->getFullPrettyVersion(),
$operation->getTargetPackage()->getFullPrettyVersion(),
);
diff --git a/src/PackageDiff.php b/src/PackageDiff.php
index 66f8413..8b76abb 100644
--- a/src/PackageDiff.php
+++ b/src/PackageDiff.php
@@ -9,6 +9,9 @@
use Composer\Package\CompletePackage;
use Composer\Package\Loader\ArrayLoader;
use Composer\Repository\ArrayRepository;
+use Composer\Semver\Semver;
+use Composer\Semver\VersionParser;
+use UnexpectedValueException;
class PackageDiff
{
@@ -50,6 +53,23 @@ public function getPackageDiff($from, $to, $dev, $withPlatform)
return $operations;
}
+ /**
+ * @return bool
+ */
+ public static function isUpgrade(UpdateOperation $operation)
+ {
+ $versionParser = new VersionParser();
+ try {
+ $normalizedFrom = $versionParser->normalize($operation->getInitialPackage()->getVersion());
+ $normalizedTo = $versionParser->normalize($operation->getTargetPackage()->getVersion());
+ } catch (UnexpectedValueException $e) {
+ return true; // Consider as upgrade if versions are not parsable
+ }
+ $sorted = Semver::sort(array($normalizedTo, $normalizedFrom));
+
+ return $sorted[0] === $normalizedFrom;
+ }
+
/**
* @param string $path
* @param bool $dev
diff --git a/tests/Command/DiffCommandTest.php b/tests/Command/DiffCommandTest.php
index b7579f3..acf24b4 100644
--- a/tests/Command/DiffCommandTest.php
+++ b/tests/Command/DiffCommandTest.php
@@ -3,6 +3,7 @@
namespace IonBazan\ComposerDiff\Tests\Command;
use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use IonBazan\ComposerDiff\Command\DiffCommand;
@@ -30,6 +31,7 @@ public function testItGeneratesReportInGivenFormat($expectedOutput, array $optio
new UninstallOperation($this->getPackageWithSource('a/package-4', '0.1.1', 'gitlab.org')),
new UninstallOperation($this->getPackageWithSource('a/package-5', '0.1.1', 'gitlab2.org')),
new UninstallOperation($this->getPackageWithSource('a/package-6', '0.1.1', 'gitlab3.org')),
+ new UpdateOperation($this->getPackageWithSource('a/package-7', '1.2.0', 'github.com'), $this->getPackageWithSource('a/package-7', '1.0.0', 'github.com')),
))
;
$result = $tester->execute($options);
@@ -37,19 +39,90 @@ public function testItGeneratesReportInGivenFormat($expectedOutput, array $optio
$this->assertSame($expectedOutput, $tester->getDisplay());
}
+ /**
+ * @param int $exitCode
+ * @param OperationInterface[] $prodOperations
+ * @param OperationInterface[] $devOperations
+ *
+ * @dataProvider strictDataProvider
+ */
+ public function testStrictMode($exitCode, array $prodOperations, array $devOperations)
+ {
+ $diff = $this->getMockBuilder('IonBazan\ComposerDiff\PackageDiff')->getMock();
+ $tester = new CommandTester(new DiffCommand($diff, array('gitlab2.org')));
+ $diff->expects($this->exactly(2))
+ ->method('getPackageDiff')
+ ->with($this->isType('string'), $this->isType('string'), $this->isType('boolean'), false)
+ ->willReturnOnConsecutiveCalls($prodOperations, $devOperations)
+ ;
+ $this->assertSame($exitCode, $tester->execute(array('--strict' => null)));
+ }
+
+ public function strictDataProvider()
+ {
+ return array(
+ 'No changes' => array(0, array(), array()),
+ 'Changes in prod and dev' => array(
+ 6,
+ array(
+ new InstallOperation($this->getPackageWithSource('a/package-1', '1.0.0', 'github.com')),
+ new UpdateOperation($this->getPackageWithSource('a/package-2', '1.0.0', 'github.com'), $this->getPackageWithSource('a/package-2', '1.2.0', 'github.com')),
+ ),
+ array(
+ new UpdateOperation($this->getPackageWithSource('a/package-3', '1.0.0', 'github.com'), $this->getPackageWithSource('a/package-3', '1.2.0', 'github.com')),
+ new InstallOperation($this->getPackageWithSource('a/package-4', '1.0.0', 'github.com')),
+ ),
+ ),
+ 'Downgrades in prod and changes in dev' => array(
+ 14,
+ array(
+ new InstallOperation($this->getPackageWithSource('a/package-1', '1.0.0', 'github.com')),
+ new UpdateOperation($this->getPackageWithSource('a/package-2', '1.2.0', 'github.com'), $this->getPackageWithSource('a/package-2', '1.0.0', 'github.com')),
+ ),
+ array(
+ new UpdateOperation($this->getPackageWithSource('a/package-3', '1.0.0', 'github.com'), $this->getPackageWithSource('a/package-3', '1.2.0', 'github.com')),
+ new InstallOperation($this->getPackageWithSource('a/package-4', '1.0.0', 'github.com')),
+ ),
+ ),
+ 'Changes in prod and downgrades in dev' => array(
+ 22,
+ array(
+ new InstallOperation($this->getPackageWithSource('a/package-1', '1.0.0', 'github.com')),
+ new UpdateOperation($this->getPackageWithSource('a/package-2', '1.0.0', 'github.com'), $this->getPackageWithSource('a/package-2', '1.2.0', 'github.com')),
+ ),
+ array(
+ new UpdateOperation($this->getPackageWithSource('a/package-3', '1.2.0', 'github.com'), $this->getPackageWithSource('a/package-3', '1.0.0', 'github.com')),
+ new InstallOperation($this->getPackageWithSource('a/package-4', '1.0.0', 'github.com')),
+ ),
+ ),
+ 'Downgrades in both' => array(
+ 30,
+ array(
+ new InstallOperation($this->getPackageWithSource('a/package-1', '1.0.0', 'github.com')),
+ new UpdateOperation($this->getPackageWithSource('a/package-2', '1.2.0', 'github.com'), $this->getPackageWithSource('a/package-2', '1.0.0', 'github.com')),
+ ),
+ array(
+ new UpdateOperation($this->getPackageWithSource('a/package-3', '1.2.0', 'github.com'), $this->getPackageWithSource('a/package-3', '1.0.0', 'github.com')),
+ new InstallOperation($this->getPackageWithSource('a/package-4', '1.0.0', 'github.com')),
+ ),
+ ),
+ );
+ }
+
public function outputDataProvider()
{
return array(
'Markdown table' => array(
<<